Add spirv-tools fuzzer

This change adds a new tint fuzzer that uses SPIRV-Tools to fuzz SPIR-V binaries.
The fuzzer works on a corpus of SPIR-V shaders. For each shader from the corpus it uses
one of `spirv-fuzz`, `spirv-reduce` or `spirv-opt` to mutate and then runs the shader through
the Tint compiler in two steps:
- Converts the mutated shader to WGSL.
- Converts WGSL to some target language specified in the CLI arguments.

The list of all supported CLI arguments and their description is in the cli.h file.

Change-Id: I95c0741b78ccc600dd9a73c371d520bdf7814352
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/41945
Kokoro: Kokoro <noreply+kokoro@google.com>
Commit-Queue: Vasyl Teliman <vasniktel@gmail.com>
Reviewed-by: David Neto <dneto@google.com>
Reviewed-by: Alastair Donaldson <allydonaldson@googlemail.com>
diff --git a/.gitignore b/.gitignore
index 31c9126..319c459 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,6 +19,7 @@
 third_party/googletest
 third_party/gpuweb-cts
 third_party/llvm-build
+third_party/protobuf
 third_party/spirv-headers
 third_party/spirv-tools
 tools/clang
diff --git a/AUTHORS b/AUTHORS
index b422353..a66d09e 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -5,3 +5,4 @@
 # of contributors, see the revision history in source control.
 
 Google LLC
+Vasyl Teliman
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 8eb4e45..61a7fff 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -49,6 +49,7 @@
 option(TINT_BUILD_SPV_WRITER "Build the SPIR-V output writer" ON)
 option(TINT_BUILD_WGSL_WRITER "Build the WGSL output writer" ON)
 option(TINT_BUILD_FUZZERS "Build fuzzers" OFF)
+option(TINT_BUILD_SPIRV_TOOLS_FUZZER "Build SPIRV-Tools fuzzer" OFF)
 option(TINT_BUILD_TESTS "Build tests" ${TINT_BUILD_TESTS_DEFAULT})
 option(TINT_BUILD_AS_OTHER_OS "Override OS detection to force building of *_other.cc files" OFF)
 
@@ -68,6 +69,7 @@
 message(STATUS "Tint build SPIR-V writer: ${TINT_BUILD_SPV_WRITER}")
 message(STATUS "Tint build WGSL writer: ${TINT_BUILD_WGSL_WRITER}")
 message(STATUS "Tint build fuzzers: ${TINT_BUILD_FUZZERS}")
+message(STATUS "Tint build SPIRV-Tools fuzzer: ${TINT_BUILD_SPIRV_TOOLS_FUZZER}")
 message(STATUS "Tint build tests: ${TINT_BUILD_TESTS}")
 message(STATUS "Tint build with ASAN: ${TINT_ENABLE_ASAN}")
 message(STATUS "Tint build with MSAN: ${TINT_ENABLE_MSAN}")
@@ -77,12 +79,30 @@
 message(STATUS "Using python3")
 find_package(PythonInterp 3 REQUIRED)
 
+if (${TINT_BUILD_SPIRV_TOOLS_FUZZER})
+  message(STATUS "TINT_BUILD_SPIRV_TOOLS_FUZZER is ON - setting
+          TINT_BUILD_FUZZERS,
+          TINT_BUILD_SPV_READER,
+          TINT_BUILD_WGSL_READER,
+          TINT_BUILD_WGSL_WRITER,
+          TINT_BUILD_HLSL_WRITER,
+          TINT_BUILD_MSL_WRITER,
+          TINT_BUILD_SPV_WRITER to ON")
+  set(TINT_BUILD_FUZZERS ON)
+  set(TINT_BUILD_SPV_READER ON)
+  set(TINT_BUILD_WGSL_READER ON)
+  set(TINT_BUILD_WGSL_WRITER ON)
+  set(TINT_BUILD_HLSL_WRITER ON)
+  set(TINT_BUILD_MSL_WRITER ON)
+  set(TINT_BUILD_SPV_WRITER ON)
+endif()
+
 # CMake < 3.15 sets /W3 in CMAKE_CXX_FLAGS. Remove it if it's there.
 # See https://gitlab.kitware.com/cmake/cmake/-/issues/18317
 if (MSVC)
-    if (CMAKE_CXX_FLAGS MATCHES "/W3")
-        string(REPLACE "/W3" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
-    endif()
+  if (CMAKE_CXX_FLAGS MATCHES "/W3")
+    string(REPLACE "/W3" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
+  endif()
 endif()
 
 if (${TINT_CHECK_CHROMIUM_STYLE})
diff --git a/DEPS b/DEPS
index 110c2b4..d3b3dcb 100644
--- a/DEPS
+++ b/DEPS
@@ -11,6 +11,7 @@
   'clang_revision': 'eb5ab41f3801e2085208204fd71a490573d72dfd',
   'googletest_revision': '5c8ca58edfb304b2dd5e6061f83387470826dd87',
   'gpuweb_cts_revision': '177a4faf0a7ce6f8c64b42a715c634e363912a74',
+  'protobuf_revision': 'fde7cf7358ec7cd69e8db9be4f1fa6a5c431386a',
   'spirv_headers_revision': 'f5417a4b6633c3217c9a1bc2f0c70b1454975ba7',
   'spirv_tools_revision': 'ecdd9a3e6bd384bf51d096b507291faa10f14685',
   'testing_revision': '2691851e49de541c3fe42fa8692ddcdee938162f',
@@ -42,6 +43,9 @@
 
   'third_party/googletest': Var('chromium_git') + Var('github') +
       '/google/googletest.git@' + Var('googletest_revision'),
+
+  'third_party/protobuf': Var('chromium_git') + Var('github') +
+        '/protocolbuffers/protobuf.git@' + Var('protobuf_revision'),
 }
 
 hooks = [
diff --git a/Doxyfile b/Doxyfile
index 028d0ef..2e1c7aa 100644
--- a/Doxyfile
+++ b/Doxyfile
@@ -786,7 +786,8 @@
 # Note: If this tag is empty the current directory is searched.
 
 INPUT                  = CODE_OF_CONDUCT.md \
-                         src
+                         src \
+                         fuzzers/tint_spirv_tools_fuzzer
 
 # This tag can be used to specify the character encoding of the source files
 # that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses
diff --git a/fuzzers/CMakeLists.txt b/fuzzers/CMakeLists.txt
index 34e927e..8588b45 100644
--- a/fuzzers/CMakeLists.txt
+++ b/fuzzers/CMakeLists.txt
@@ -76,3 +76,7 @@
 if (${TINT_BUILD_WGSL_READER} AND ${TINT_BUILD_WGSL_WRITER})
   add_tint_fuzzer(tint_ast_clone_fuzzer)
 endif()
+
+if (${TINT_BUILD_SPIRV_TOOLS_FUZZER})
+  add_subdirectory(tint_spirv_tools_fuzzer)
+endif()
diff --git a/fuzzers/tint_common_fuzzer.cc b/fuzzers/tint_common_fuzzer.cc
index 6c92306..7d5879c 100644
--- a/fuzzers/tint_common_fuzzer.cc
+++ b/fuzzers/tint_common_fuzzer.cc
@@ -21,6 +21,7 @@
 #include <vector>
 
 #include "src/ast/module.h"
+#include "src/diagnostic/formatter.h"
 #include "src/program.h"
 
 namespace tint {
@@ -191,6 +192,7 @@
   }
 
   if (!program.IsValid()) {
+    errors_ = diag::Formatter().format(program.Diagnostics());
     return 0;
   }
 
@@ -199,58 +201,68 @@
 
     auto entry_points = inspector.GetEntryPoints();
     if (inspector.has_error()) {
+      errors_ = inspector.error();
       return 0;
     }
 
     for (auto& ep : entry_points) {
       auto remapped_name = inspector.GetRemappedNameForEntryPoint(ep.name);
       if (inspector.has_error()) {
+        errors_ = inspector.error();
         return 0;
       }
 
       auto constant_ids = inspector.GetConstantIDs();
       if (inspector.has_error()) {
+        errors_ = inspector.error();
         return 0;
       }
 
       auto uniform_bindings =
           inspector.GetUniformBufferResourceBindings(ep.name);
       if (inspector.has_error()) {
+        errors_ = inspector.error();
         return 0;
       }
 
       auto storage_bindings =
           inspector.GetStorageBufferResourceBindings(ep.name);
       if (inspector.has_error()) {
+        errors_ = inspector.error();
         return 0;
       }
 
       auto readonly_bindings =
           inspector.GetReadOnlyStorageBufferResourceBindings(ep.name);
       if (inspector.has_error()) {
+        errors_ = inspector.error();
         return 0;
       }
 
       auto sampler_bindings = inspector.GetSamplerResourceBindings(ep.name);
       if (inspector.has_error()) {
+        errors_ = inspector.error();
         return 0;
       }
 
       auto comparison_sampler_bindings =
           inspector.GetComparisonSamplerResourceBindings(ep.name);
       if (inspector.has_error()) {
+        errors_ = inspector.error();
         return 0;
       }
 
       auto sampled_texture_bindings =
           inspector.GetSampledTextureResourceBindings(ep.name);
       if (inspector.has_error()) {
+        errors_ = inspector.error();
         return 0;
       }
 
       auto multisampled_texture_bindings =
           inspector.GetMultisampledTextureResourceBindings(ep.name);
       if (inspector.has_error()) {
+        errors_ = inspector.error();
         return 0;
       }
     }
@@ -272,39 +284,44 @@
     program = std::move(out.program);
   }
 
-  std::unique_ptr<writer::Writer> writer;
-
   switch (output_) {
     case OutputFormat::kWGSL:
 #if TINT_BUILD_WGSL_WRITER
-      writer = std::make_unique<writer::wgsl::Generator>(&program);
+      writer_ = std::make_unique<writer::wgsl::Generator>(&program);
 #endif  // TINT_BUILD_WGSL_WRITER
       break;
     case OutputFormat::kSpv:
 #if TINT_BUILD_SPV_WRITER
-      writer = std::make_unique<writer::spirv::Generator>(&program);
+      writer_ = std::make_unique<writer::spirv::Generator>(&program);
 #endif  // TINT_BUILD_SPV_WRITER
       break;
     case OutputFormat::kHLSL:
 #if TINT_BUILD_HLSL_WRITER
-      writer = std::make_unique<writer::hlsl::Generator>(&program);
+      writer_ = std::make_unique<writer::hlsl::Generator>(&program);
 #endif  // TINT_BUILD_HLSL_WRITER
       break;
     case OutputFormat::kMSL:
 #if TINT_BUILD_MSL_WRITER
-      writer = std::make_unique<writer::msl::Generator>(&program);
+      writer_ = std::make_unique<writer::msl::Generator>(&program);
 #endif  // TINT_BUILD_MSL_WRITER
       break;
     case OutputFormat::kNone:
       break;
   }
 
-  if (writer) {
-    writer->Generate();
+  if (writer_) {
+    if (!writer_->Generate()) {
+      errors_ = writer_->error();
+      return 0;
+    }
   }
 
   return 0;
 }
 
+const writer::Writer* CommonFuzzer::GetWriter() const {
+  return writer_.get();
+}
+
 }  // namespace fuzzers
 }  // namespace tint
diff --git a/fuzzers/tint_common_fuzzer.h b/fuzzers/tint_common_fuzzer.h
index 372df35..9201850 100644
--- a/fuzzers/tint_common_fuzzer.h
+++ b/fuzzers/tint_common_fuzzer.h
@@ -16,6 +16,7 @@
 #define FUZZERS_TINT_COMMON_FUZZER_H_
 
 #include <cstring>
+#include <memory>
 #include <string>
 #include <utility>
 #include <vector>
@@ -108,12 +109,20 @@
 
   int Run(const uint8_t* data, size_t size);
 
+  const writer::Writer* GetWriter() const;
+
+  const std::string& GetErrors() const { return errors_; }
+
+  bool HasErrors() const { return !errors_.empty(); }
+
  private:
   InputFormat input_;
   OutputFormat output_;
+  std::unique_ptr<writer::Writer> writer_;
   transform::Manager* transform_manager_;
   transform::DataMap transform_inputs_;
   bool inspector_enabled_;
+  std::string errors_;
 };
 
 }  // namespace fuzzers
diff --git a/fuzzers/tint_spirv_tools_fuzzer/CMakeLists.txt b/fuzzers/tint_spirv_tools_fuzzer/CMakeLists.txt
new file mode 100644
index 0000000..92790c1
--- /dev/null
+++ b/fuzzers/tint_spirv_tools_fuzzer/CMakeLists.txt
@@ -0,0 +1,81 @@
+# Copyright 2021 The Tint Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+set(FUZZER_SOURCES
+        cli.cc
+        fuzzer.cc
+        mutator.cc
+        mutator_cache.cc
+        spirv_fuzz_mutator.cc
+        spirv_opt_mutator.cc
+        spirv_reduce_mutator.cc
+        util.cc)
+
+set(FUZZER_SOURCES ${FUZZER_SOURCES}
+        cli.h
+        mutator.h
+        mutator_cache.h
+        spirv_fuzz_mutator.h
+        spirv_opt_mutator.h
+        spirv_reduce_mutator.h
+        util.h)
+
+set(FUZZER_SOURCES ${FUZZER_SOURCES}
+        ../tint_common_fuzzer.h
+        ../tint_common_fuzzer.cc)
+
+function(configure_spirv_tools_fuzzer_target NAME SOURCES)
+    add_executable(${NAME} ${SOURCES})
+    target_link_libraries(${NAME} SPIRV-Tools SPIRV-Tools-opt SPIRV-Tools-fuzz SPIRV-Tools-reduce)
+    tint_default_compile_options(${NAME})
+    target_compile_options(${NAME} PRIVATE
+            -Wno-missing-prototypes
+            -Wno-zero-as-null-pointer-constant
+            -Wno-reserved-id-macro
+            -Wno-sign-conversion
+            -Wno-extra-semi-stmt
+            -Wno-inconsistent-missing-destructor-override
+            -Wno-newline-eof
+            -Wno-old-style-cast
+            -Wno-weak-vtables
+            -Wno-undef)
+    target_include_directories(${NAME} PRIVATE
+            ${spirv-tools_SOURCE_DIR}
+            ${spirv-tools_BINARY_DIR})
+endfunction()
+
+configure_spirv_tools_fuzzer_target(tint_spirv_tools_fuzzer "${FUZZER_SOURCES}")
+target_compile_definitions(tint_spirv_tools_fuzzer PUBLIC CUSTOM_MUTATOR)
+target_compile_definitions(tint_spirv_tools_fuzzer PRIVATE TARGET_FUZZER)
+target_link_libraries(tint_spirv_tools_fuzzer libtint-fuzz)
+
+set(DEBUGGER_SOURCES
+        cli.cc
+        mutator.cc
+        mutator_debugger.cc
+        spirv_fuzz_mutator.cc
+        spirv_opt_mutator.cc
+        spirv_reduce_mutator.cc
+        util.cc)
+
+set(DEBUGGER_SOURCES ${DEBUGGER_SOURCES}
+        cli.h
+        mutator.h
+        spirv_fuzz_mutator.h
+        spirv_opt_mutator.h
+        spirv_reduce_mutator.h
+        util.h)
+
+configure_spirv_tools_fuzzer_target(tint_spirv_tools_mutator_debugger "${DEBUGGER_SOURCES}")
+target_compile_definitions(tint_spirv_tools_mutator_debugger PRIVATE TARGET_DEBUGGER)
diff --git a/fuzzers/tint_spirv_tools_fuzzer/cli.cc b/fuzzers/tint_spirv_tools_fuzzer/cli.cc
new file mode 100644
index 0000000..cfb6633
--- /dev/null
+++ b/fuzzers/tint_spirv_tools_fuzzer/cli.cc
@@ -0,0 +1,467 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "fuzzers/tint_spirv_tools_fuzzer/cli.h"
+
+#include <fstream>
+#include <limits>
+#include <sstream>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "fuzzers/tint_spirv_tools_fuzzer/util.h"
+#include "source/opt/build_module.h"
+
+namespace tint {
+namespace fuzzers {
+namespace spvtools_fuzzer {
+namespace {
+
+const char* const kMutatorParameters = R"(
+Mutators' parameters:
+
+  --donors=
+                       A path to the text file with a list of paths to the
+                       SPIR-V donor files. Check out the doc for the spirv-fuzz
+                       to learn more about donor binaries. Donors are not used
+                       by default.
+
+  --enable_all_fuzzer_passes=
+                       Whether to use all fuzzer passes or a randomly selected subset
+                       of them. This must be one of `true` or `false` (without `).
+                       By default it's `false`.
+
+  --enable_all_reduce_passes=
+                       Whether to use all reduction passes or a randomly selected subset
+                       of them. This must be one of `true` or `false` (without `).
+                       By default it's `false`.
+
+  --opt_batch_size=
+                       The maximum number of spirv-opt optimizations that
+                       will be applied in a single mutation session (i.e.
+                       a call to LLVMFuzzerCustomMutator). This must fit in
+                       uint32_t. By default it's 6.
+
+  --reduction_batch_size=
+                       The maximum number of spirv-reduce reductions that
+                       will be applied in a single mutation session (i.e.
+                       a call to LLVMFuzzerCustomMutator). This must fit in
+                       uint32_t. By default it's 3.
+
+  --repeated_pass_strategy=
+                       The strategy that will be used to recommend the next fuzzer
+                       pass. This must be one of `simple`, `looped` or `random`
+                       (without `). By default it's `simple`. Check out the doc for
+                       spirv-fuzz to learn more.
+
+  --transformation_batch_size=
+                       The maximum number of spirv-fuzz transformations
+                       that will be applied during a single mutation
+                       session (i.e. a call to LLVMFuzzerCustomMutator).
+                       This must fit in uint32_t. By default it's 3.
+
+  --validate_after_each_fuzzer_pass=
+                       Whether to validate SPIR-V binary after each fuzzer pass.
+                       This must be one of `true` or `false` (without `).
+                       By default it's `true`. Switch this to `false` if you experience
+                       bad performance.
+
+  --validate_after_each_opt_pass=
+                       Whether to validate SPIR-V binary after each optimization pass.
+                       This must be one of `true` or `false` (without `).
+                       By default it's `true`. Switch this to `false` if you experience
+                       bad performance.
+
+  --validate_after_each_reduce_pass=
+                       Whether to validate SPIR-V binary after each reduction pass.
+                       This must be one of `true` or `false` (without `).
+                       By default it's `true`. Switch this to `false` if you experience
+                       bad performance.
+)";
+
+const char* const kFuzzerHelpMessage = R"(
+This fuzzer uses SPIR-V binaries to fuzz the Tint compiler. It uses SPIRV-Tools
+to mutate those binaries. The fuzzer works on a corpus of SPIR-V shaders.
+For each shader from the corpus it uses one of `spirv-fuzz`, `spirv-reduce` or
+`spirv-opt` to mutate it and then runs the shader through the Tint compiler in
+two steps:
+- Converts the mutated shader to WGSL.
+- Converts WGSL to some target language specified in the CLI arguments.
+
+Below is a list of all supported parameters for this fuzzer. You may want to
+run it with -help=1 to check out libfuzzer parameters.
+
+Fuzzer parameters:
+
+  --error_dir
+                       The directory that will be used to output invalid SPIR-V
+                       binaries to. This is especially useful during debugging
+                       mutators. The directory must have the following subdirectories:
+                       - spv/ - will be used to output errors, produced during
+                         the conversion from the SPIR-V to WGSL.
+                       - wgsl/ - will be used to output errors, produced during
+                         the conversion from the WGSL to `--fuzzing_target`.
+                       - mutator/ - will be used to output errors, produced by
+                         the mutators.
+                       By default invalid files are not printed out.
+
+  --fuzzing_target
+                       The type of backend to target during fuzzing. This must
+                       be one or a combination of `wgsl`, `spv`, `msl` or `hlsl`
+                       (without `) separated by commas. By default it's
+                       `wgsl,spv,msl,hlsl`.
+
+  --help
+                       Show this message. Note that there is also a -help=1
+                       parameter that will display libfuzzer's help message.
+
+  --mutator_cache_size=
+                       The maximum size of the cache that stores
+                       mutation sessions. This must fit in uint32_t.
+                       By default it's 20.
+
+  --mutator_type=
+                       Determines types of the mutators to run. This must be one or
+                       a combination of `fuzz`, `opt`, `reduce` (without `) separated by
+                       comma. If a combination is specified, each element in the
+                       combination will have an equal chance of mutating a SPIR-V
+                       binary during a mutation session (i.e. if no mutator exists
+                       for that binary in the mutator cache). By default, the
+                       parameter's value is `fuzz,opt,reduce`.
+)";
+
+const char* const kMutatorDebuggerHelpMessage = R"(
+This tool is used to debug *mutators*. It uses CLI arguments similar to the
+ones used by the fuzzer. To debug some mutator you just need to specify the
+mutator type, the seed and the path to the SPIR-V binary that triggered the
+error. This tool will run the mutator on the binary until the error is
+produced or the mutator returns `kLimitReached`.
+
+Note that this is different from debugging the fuzzer by specifying input
+files to test. The difference is that the latter will not execute any
+mutator (it will only run the LLVMFuzzerTestOneInput function) whereas this
+tool is useful when one of the SPIRV-Tools mutators crashes or produces an
+invalid binary in LLVMFuzzerCustomMutator.
+
+Debugger parameters:
+
+  --help
+                       Show this message.
+
+  --mutator_type=
+                       Determines the type of the mutator to debug. This must be
+                       one of `fuzz`, `reduce` or `opt` (without `). This parameter
+                       is REQUIRED.
+
+  --original_binary=
+                       The path to the SPIR-V binary that the faulty mutator was
+                       initialized with. This will be dumped on errors by the fuzzer
+                       if `--error_dir` is specified. This parameter is REQUIRED.
+
+  --seed=
+                       The seed for the random number generator that was used to
+                       initialize the mutator. This value is usually printed to
+                       the console when the mutator produces an invalid binary.
+                       It is also dumped into the log file if `--error_dir` is
+                       specified. This must fit in uint32_t. This parameter is
+                       REQUIRED.
+)";
+
+void PrintHelpMessage(const char* help_message) {
+  std::cout << help_message << std::endl << kMutatorParameters << std::endl;
+}
+
+[[noreturn]] void InvalidParameter(const char* help_message,
+                                   const char* param) {
+  std::cout << "Invalid value for " << param << std::endl;
+  PrintHelpMessage(help_message);
+  exit(1);
+}
+
+bool ParseUint32(const char* param, uint32_t* out) {
+  auto value = strtoul(param, nullptr, 10);
+  if (value > std::numeric_limits<uint32_t>::max()) {
+    return false;
+  }
+  *out = static_cast<uint32_t>(value);
+  return true;
+}
+
+std::vector<spvtools::fuzz::fuzzerutil::ModuleSupplier> ParseDonors(
+    const char* file_name) {
+  std::ifstream fin(file_name);
+  if (!fin) {
+    std::cout << "Can't open donors list file: " << file_name << std::endl;
+    exit(1);
+  }
+
+  std::vector<spvtools::fuzz::fuzzerutil::ModuleSupplier> result;
+  for (std::string donor_file_name; fin >> donor_file_name;) {
+    if (!std::ifstream(donor_file_name)) {
+      std::cout << "Can't open donor file: " << donor_file_name << std::endl;
+      exit(1);
+    }
+
+    result.emplace_back([donor_file_name] {
+      std::vector<uint32_t> binary;
+      if (!util::ReadBinary(donor_file_name, &binary)) {
+        std::cout << "Failed to read donor from: " << donor_file_name
+                  << std::endl;
+        exit(1);
+      }
+      return spvtools::BuildModule(
+          kDefaultTargetEnv, spvtools::fuzz::fuzzerutil::kSilentMessageConsumer,
+          binary.data(), binary.size());
+    });
+  }
+
+  return result;
+}
+
+bool ParseRepeatedPassStrategy(const char* param,
+                               spvtools::fuzz::RepeatedPassStrategy* out) {
+  if (!strcmp(param, "simple")) {
+    *out = spvtools::fuzz::RepeatedPassStrategy::kSimple;
+  } else if (!strcmp(param, "looped")) {
+    *out = spvtools::fuzz::RepeatedPassStrategy::kLoopedWithRecommendations;
+  } else if (!strcmp(param, "random")) {
+    *out = spvtools::fuzz::RepeatedPassStrategy::kRandomWithRecommendations;
+  } else {
+    return false;
+  }
+  return true;
+}
+
+bool ParseBool(const char* param, bool* out) {
+  if (!strcmp(param, "true")) {
+    *out = true;
+  } else if (!strcmp(param, "false")) {
+    *out = false;
+  } else {
+    return false;
+  }
+  return true;
+}
+
+bool ParseMutatorType(const char* param, MutatorType* out) {
+  if (!strcmp(param, "fuzz")) {
+    *out = MutatorType::kFuzz;
+  } else if (!strcmp(param, "opt")) {
+    *out = MutatorType::kOpt;
+  } else if (!strcmp(param, "reduce")) {
+    *out = MutatorType::kReduce;
+  } else {
+    return false;
+  }
+  return true;
+}
+
+bool ParseFuzzingTarget(const char* param, FuzzingTarget* out) {
+  if (!strcmp(param, "wgsl")) {
+    *out = FuzzingTarget::kWgsl;
+  } else if (!strcmp(param, "spv")) {
+    *out = FuzzingTarget::kSpv;
+  } else if (!strcmp(param, "msl")) {
+    *out = FuzzingTarget::kMsl;
+  } else if (!strcmp(param, "hlsl")) {
+    *out = FuzzingTarget::kHlsl;
+  } else {
+    return false;
+  }
+  return true;
+}
+
+bool HasPrefix(const char* str, const char* prefix) {
+  return strncmp(str, prefix, strlen(prefix)) == 0;
+}
+
+void ParseMutatorCliParam(const char* param,
+                          const char* help_message,
+                          MutatorCliParams* out) {
+  if (HasPrefix(param, "--transformation_batch_size=")) {
+    if (!ParseUint32(param + sizeof("--transformation_batch_size=") - 1,
+                     &out->transformation_batch_size)) {
+      InvalidParameter(help_message, param);
+    }
+  } else if (HasPrefix(param, "--reduction_batch_size=")) {
+    if (!ParseUint32(param + sizeof("--reduction_batch_size=") - 1,
+                     &out->reduction_batch_size)) {
+      InvalidParameter(help_message, param);
+    }
+  } else if (HasPrefix(param, "--opt_batch_size=")) {
+    if (!ParseUint32(param + sizeof("--opt_batch_size=") - 1,
+                     &out->opt_batch_size)) {
+      InvalidParameter(help_message, param);
+    }
+  } else if (HasPrefix(param, "--donors=")) {
+    out->donors = ParseDonors(param + sizeof("--donors=") - 1);
+  } else if (HasPrefix(param, "--repeated_pass_strategy=")) {
+    if (!ParseRepeatedPassStrategy(
+            param + sizeof("--repeated_pass_strategy=") - 1,
+            &out->repeated_pass_strategy)) {
+      InvalidParameter(help_message, param);
+    }
+  } else if (HasPrefix(param, "--enable_all_fuzzer_passes=")) {
+    if (!ParseBool(param + sizeof("--enable_all_fuzzer_passes=") - 1,
+                   &out->enable_all_fuzzer_passes)) {
+      InvalidParameter(help_message, param);
+    }
+  } else if (HasPrefix(param, "--enable_all_reduce_passes=")) {
+    if (!ParseBool(param + sizeof("--enable_all_reduce_passes=") - 1,
+                   &out->enable_all_reduce_passes)) {
+      InvalidParameter(help_message, param);
+    }
+  } else if (HasPrefix(param, "--validate_after_each_opt_pass=")) {
+    if (!ParseBool(param + sizeof("--validate_after_each_opt_pass=") - 1,
+                   &out->validate_after_each_opt_pass)) {
+      InvalidParameter(help_message, param);
+    }
+  } else if (HasPrefix(param, "--validate_after_each_fuzzer_pass=")) {
+    if (!ParseBool(param + sizeof("--validate_after_each_fuzzer_pass=") - 1,
+                   &out->validate_after_each_fuzzer_pass)) {
+      InvalidParameter(help_message, param);
+    }
+  } else if (HasPrefix(param, "--validate_after_each_reduce_pass=")) {
+    if (!ParseBool(param + sizeof("--validate_after_each_reduce_pass=") - 1,
+                   &out->validate_after_each_reduce_pass)) {
+      InvalidParameter(help_message, param);
+    }
+  }
+}
+
+}  // namespace
+
+FuzzerCliParams ParseFuzzerCliParams(int argc, const char* const* argv) {
+  FuzzerCliParams cli_params;
+  const auto* help_message = kFuzzerHelpMessage;
+  auto help = false;
+
+  for (int i = 0; i < argc; ++i) {
+    auto param = argv[i];
+    ParseMutatorCliParam(param, help_message, &cli_params.mutator_params);
+
+    if (HasPrefix(param, "--mutator_cache_size=")) {
+      if (!ParseUint32(param + sizeof("--mutator_cache_size=") - 1,
+                       &cli_params.mutator_cache_size)) {
+        InvalidParameter(help_message, param);
+      }
+    } else if (HasPrefix(param, "--mutator_type=")) {
+      auto result = MutatorType::kNone;
+
+      std::stringstream ss(param + sizeof("--mutator_type=") - 1);
+      for (std::string value; std::getline(ss, value, ',');) {
+        auto out = MutatorType::kNone;
+        if (!ParseMutatorType(value.c_str(), &out)) {
+          InvalidParameter(help_message, param);
+        }
+        result = result | out;
+      }
+
+      if (result == MutatorType::kNone) {
+        InvalidParameter(help_message, param);
+      }
+
+      cli_params.mutator_type = result;
+    } else if (HasPrefix(param, "--fuzzing_target=")) {
+      auto result = FuzzingTarget::kNone;
+
+      std::stringstream ss(param + sizeof("--fuzzing_target=") - 1);
+      for (std::string value; std::getline(ss, value, ',');) {
+        auto tmp = FuzzingTarget::kNone;
+        if (!ParseFuzzingTarget(value.c_str(), &tmp)) {
+          InvalidParameter(help_message, param);
+        }
+        result = result | tmp;
+      }
+
+      if (result == FuzzingTarget::kNone) {
+        InvalidParameter(help_message, param);
+      }
+
+      cli_params.fuzzing_target = result;
+    } else if (HasPrefix(param, "--error_dir=")) {
+      cli_params.error_dir = param + sizeof("--error_dir=") - 1;
+    } else if (!strcmp(param, "--help")) {
+      help = true;
+    }
+  }
+
+  if (help) {
+    PrintHelpMessage(help_message);
+    exit(0);
+  }
+
+  return cli_params;
+}
+
+MutatorDebuggerCliParams ParseMutatorDebuggerCliParams(
+    int argc,
+    const char* const* argv) {
+  MutatorDebuggerCliParams cli_params;
+  bool seed_param_present = false;
+  bool original_binary_param_present = false;
+  bool mutator_type_param_present = false;
+  const auto* help_message = kMutatorDebuggerHelpMessage;
+  auto help = false;
+
+  for (int i = 0; i < argc; ++i) {
+    auto param = argv[i];
+    ParseMutatorCliParam(param, help_message, &cli_params.mutator_params);
+
+    if (HasPrefix(param, "--mutator_type=")) {
+      if (!ParseMutatorType(param + sizeof("--mutator_type=") - 1,
+                            &cli_params.mutator_type)) {
+        InvalidParameter(help_message, param);
+      }
+      mutator_type_param_present = true;
+    } else if (HasPrefix(param, "--original_binary=")) {
+      if (!util::ReadBinary(param + sizeof("--original_binary=") - 1,
+                            &cli_params.original_binary)) {
+        InvalidParameter(help_message, param);
+      }
+      original_binary_param_present = true;
+    } else if (HasPrefix(param, "--seed=")) {
+      if (!ParseUint32(param + sizeof("--seed=") - 1, &cli_params.seed)) {
+        InvalidParameter(help_message, param);
+      }
+      seed_param_present = true;
+    } else if (!strcmp(param, "--help")) {
+      help = true;
+    }
+  }
+
+  if (help) {
+    PrintHelpMessage(help_message);
+    exit(0);
+  }
+
+  std::pair<bool, const char*> required_params[] = {
+      {seed_param_present, "--seed"},
+      {original_binary_param_present, "--original_binary"},
+      {mutator_type_param_present, "--mutator_type"}};
+
+  for (auto required_param : required_params) {
+    if (!required_param.first) {
+      std::cout << required_param.second << " is missing" << std::endl;
+      exit(1);
+    }
+  }
+
+  return cli_params;
+}
+
+}  // namespace spvtools_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/fuzzers/tint_spirv_tools_fuzzer/cli.h b/fuzzers/tint_spirv_tools_fuzzer/cli.h
new file mode 100644
index 0000000..7913a6b
--- /dev/null
+++ b/fuzzers/tint_spirv_tools_fuzzer/cli.h
@@ -0,0 +1,124 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef FUZZERS_TINT_SPIRV_TOOLS_FUZZER_CLI_H_
+#define FUZZERS_TINT_SPIRV_TOOLS_FUZZER_CLI_H_
+
+#include <string>
+#include <vector>
+
+#include "source/fuzz/fuzzer.h"
+
+namespace tint {
+namespace fuzzers {
+namespace spvtools_fuzzer {
+
+/// Default SPIR-V environment that will be used during fuzzing.
+const auto kDefaultTargetEnv = SPV_ENV_VULKAN_1_1;
+
+/// The type of the mutator to run.
+enum class MutatorType {
+  kNone = 0,
+  kFuzz = 1 << 0,
+  kReduce = 1 << 1,
+  kOpt = 1 << 2,
+  kAll = kFuzz | kReduce | kOpt
+};
+
+inline MutatorType operator|(MutatorType a, MutatorType b) {
+  return static_cast<MutatorType>(static_cast<int>(a) | static_cast<int>(b));
+}
+
+inline MutatorType operator&(MutatorType a, MutatorType b) {
+  return static_cast<MutatorType>(static_cast<int>(a) & static_cast<int>(b));
+}
+
+/// Shading language to target during fuzzing.
+enum class FuzzingTarget {
+  kNone = 0,
+  kHlsl = 1 << 0,
+  kMsl = 1 << 1,
+  kSpv = 1 << 2,
+  kWgsl = 1 << 3,
+  kAll = kHlsl | kMsl | kSpv | kWgsl
+};
+
+inline FuzzingTarget operator|(FuzzingTarget a, FuzzingTarget b) {
+  return static_cast<FuzzingTarget>(static_cast<int>(a) | static_cast<int>(b));
+}
+
+inline FuzzingTarget operator&(FuzzingTarget a, FuzzingTarget b) {
+  return static_cast<FuzzingTarget>(static_cast<int>(a) & static_cast<int>(b));
+}
+
+/// These parameters are accepted by various mutators and thus they are accepted
+/// by both the fuzzer and the mutator debugger.
+struct MutatorCliParams {
+  spv_target_env target_env = kDefaultTargetEnv;
+  uint32_t transformation_batch_size = 3;
+  uint32_t reduction_batch_size = 3;
+  uint32_t opt_batch_size = 6;
+  std::vector<spvtools::fuzz::fuzzerutil::ModuleSupplier> donors = {};
+  spvtools::fuzz::RepeatedPassStrategy repeated_pass_strategy =
+      spvtools::fuzz::RepeatedPassStrategy::kSimple;
+  bool enable_all_fuzzer_passes = false;
+  bool enable_all_reduce_passes = false;
+  bool validate_after_each_opt_pass = true;
+  bool validate_after_each_fuzzer_pass = true;
+  bool validate_after_each_reduce_pass = true;
+};
+
+/// Parameters specific to the fuzzer.
+struct FuzzerCliParams {
+  uint32_t mutator_cache_size = 20;
+  MutatorType mutator_type = MutatorType::kAll;
+  FuzzingTarget fuzzing_target = FuzzingTarget::kAll;
+  std::string error_dir;
+  MutatorCliParams mutator_params;
+};
+
+/// Parameters specific to the mutator debugger.
+struct MutatorDebuggerCliParams {
+  MutatorType mutator_type = MutatorType::kNone;
+  uint32_t seed = 0;
+  std::vector<uint32_t> original_binary;
+  MutatorCliParams mutator_params;
+};
+
+/// Parses CLI parameters for the fuzzer. This function exits with an error code
+/// and a message is printed to the console if some parameter has invalid
+/// format. You can pass `--help` to check out all available parameters.
+///
+/// @param argc - the number of parameters (identical to the `argc` in `main`
+///     function).
+/// @param argv - array of C strings of parameters.
+/// @return the parsed parameters.
+FuzzerCliParams ParseFuzzerCliParams(int argc, const char* const* argv);
+
+/// Parses CLI parameters for the mutator debugger. This function exits with an
+/// error code and a message is printed to the console if some parameter has
+/// invalid format. You can pass `--help` to check out all available parameters.
+///
+/// @param argc - the number of parameters (identical to the `argc` in `main`
+///     function).
+/// @param argv - array of C strings of parameters.
+/// @return the parsed parameters.
+MutatorDebuggerCliParams ParseMutatorDebuggerCliParams(int argc,
+                                                       const char* const* argv);
+
+}  // namespace spvtools_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
+
+#endif  // FUZZERS_TINT_SPIRV_TOOLS_FUZZER_CLI_H_
diff --git a/fuzzers/tint_spirv_tools_fuzzer/fuzzer.cc b/fuzzers/tint_spirv_tools_fuzzer/fuzzer.cc
new file mode 100644
index 0000000..30acdf4
--- /dev/null
+++ b/fuzzers/tint_spirv_tools_fuzzer/fuzzer.cc
@@ -0,0 +1,216 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <iostream>
+#include <memory>
+#include <random>
+#include <string>
+#include <vector>
+
+#include "fuzzers/tint_common_fuzzer.h"
+#include "fuzzers/tint_spirv_tools_fuzzer/cli.h"
+#include "fuzzers/tint_spirv_tools_fuzzer/mutator_cache.h"
+#include "fuzzers/tint_spirv_tools_fuzzer/spirv_fuzz_mutator.h"
+#include "fuzzers/tint_spirv_tools_fuzzer/spirv_opt_mutator.h"
+#include "fuzzers/tint_spirv_tools_fuzzer/spirv_reduce_mutator.h"
+#include "fuzzers/tint_spirv_tools_fuzzer/util.h"
+
+namespace tint {
+namespace fuzzers {
+namespace spvtools_fuzzer {
+namespace {
+
+struct Context {
+  const FuzzerCliParams params;
+  std::unique_ptr<MutatorCache> mutator_cache;
+};
+
+Context* context = nullptr;
+
+extern "C" int LLVMFuzzerInitialize(int* argc, char*** argv) {
+  auto params = ParseFuzzerCliParams(*argc, *argv);
+  auto mutator_cache =
+      params.mutator_cache_size
+          ? std::make_unique<MutatorCache>(params.mutator_cache_size)
+          : nullptr;
+  context = new Context{std::move(params), std::move(mutator_cache)};
+  return 0;
+}
+
+std::unique_ptr<Mutator> CreateMutator(const std::vector<uint32_t>& binary,
+                                       unsigned seed) {
+  std::vector<MutatorType> types;
+  types.reserve(3);
+
+  // Determine which mutator we will be using for `binary` at random.
+  auto cli_mutator_type = context->params.mutator_type;
+  if ((MutatorType::kFuzz & cli_mutator_type) == MutatorType::kFuzz) {
+    types.push_back(MutatorType::kFuzz);
+  }
+  if ((MutatorType::kReduce & cli_mutator_type) == MutatorType::kReduce) {
+    types.push_back(MutatorType::kReduce);
+  }
+  if ((MutatorType::kOpt & cli_mutator_type) == MutatorType::kOpt) {
+    types.push_back(MutatorType::kOpt);
+  }
+
+  assert(!types.empty() && "At least one mutator type must be specified");
+  std::mt19937 rng(seed);
+  auto mutator_type =
+      types[std::uniform_int_distribution<size_t>(0, types.size() - 1)(rng)];
+
+  const auto& mutator_params = context->params.mutator_params;
+  switch (mutator_type) {
+    case MutatorType::kFuzz:
+      return std::make_unique<SpirvFuzzMutator>(
+          mutator_params.target_env, binary, seed, mutator_params.donors,
+          mutator_params.enable_all_fuzzer_passes,
+          mutator_params.repeated_pass_strategy,
+          mutator_params.validate_after_each_fuzzer_pass,
+          mutator_params.transformation_batch_size);
+    case MutatorType::kReduce:
+      return std::make_unique<SpirvReduceMutator>(
+          mutator_params.target_env, binary, seed,
+          mutator_params.reduction_batch_size,
+          mutator_params.enable_all_reduce_passes,
+          mutator_params.validate_after_each_reduce_pass);
+    case MutatorType::kOpt:
+      return std::make_unique<SpirvOptMutator>(
+          mutator_params.target_env, seed, binary,
+          mutator_params.validate_after_each_opt_pass,
+          mutator_params.opt_batch_size);
+    default:
+      assert(false && "All mutator types must be handled above");
+      return nullptr;
+  }
+}
+
+extern "C" size_t LLVMFuzzerCustomMutator(uint8_t* data,
+                                          size_t size,
+                                          size_t max_size,
+                                          unsigned seed) {
+  std::vector<uint32_t> binary(size / sizeof(uint32_t));
+  std::memcpy(binary.data(), data, size);
+
+  MutatorCache dummy_cache(1);
+  auto* mutator_cache = context->mutator_cache.get();
+  if (!mutator_cache) {
+    // Use a dummy cache if the user has decided not to use a real cache.
+    // The dummy cache will be destroyed when we return from this function but
+    // it will save us from writing all the `if (mutator_cache)` below.
+    mutator_cache = &dummy_cache;
+  }
+
+  if (!mutator_cache->Get(binary)) {
+    // Assign a mutator to the binary if it doesn't have one yet.
+    mutator_cache->Put(binary, CreateMutator(binary, seed));
+  }
+
+  auto* mutator = mutator_cache->Get(binary);
+  assert(mutator && "Mutator must be present in the cache");
+
+  auto result = mutator->Mutate();
+
+  if (result.GetStatus() == Mutator::Status::kInvalid) {
+    // The binary is invalid - log the error and remove the mutator from the
+    // cache.
+    util::LogMutatorError(*mutator, context->params.error_dir);
+    mutator_cache->Remove(binary);
+    return 0;
+  }
+
+  if (!result.IsChanged()) {
+    // The mutator didn't change the binary this time. This could be due to the
+    // fact that we've reached the number of mutations we can apply (e.g. the
+    // number of transformations in spirv-fuzz) or the mutator was just unlucky.
+    // Either way, there is no harm in destroying mutator and maybe trying again
+    // later (i.e. if libfuzzer decides to do so).
+    mutator_cache->Remove(binary);
+    return 0;
+  }
+
+  // At this point the binary is valid and was changed by the mutator.
+
+  auto mutated = mutator->GetBinary();
+  auto mutated_bytes_size = mutated.size() * sizeof(uint32_t);
+  if (mutated_bytes_size > max_size) {
+    // The binary is too big. It's unlikely that we'll reduce its size by
+    // applying the mutator one more time.
+    mutator_cache->Remove(binary);
+    return 0;
+  }
+
+  if (result.GetStatus() == Mutator::Status::kComplete) {
+    // Reassign the mutator to the mutated binary in the cache so that we can
+    // access later.
+    mutator_cache->Put(mutated, mutator_cache->Remove(binary));
+  } else {
+    // If the binary is valid and was changed but is not `kComplete`, then the
+    // mutator has reached some limit on the number of mutations.
+    mutator_cache->Remove(binary);
+  }
+
+  std::memcpy(data, mutated.data(), mutated_bytes_size);
+  return mutated_bytes_size;
+}
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+  if (size == 0) {
+    return 0;
+  }
+
+  CommonFuzzer spv_to_wgsl(InputFormat::kSpv, OutputFormat::kWGSL);
+  spv_to_wgsl.EnableInspector();
+  spv_to_wgsl.Run(data, size);
+  if (spv_to_wgsl.HasErrors()) {
+    util::LogSpvError(spv_to_wgsl.GetErrors(), data, size,
+                      context->params.error_dir);
+    return 0;
+  }
+
+  const auto* writer =
+      static_cast<const writer::wgsl::Generator*>(spv_to_wgsl.GetWriter());
+
+  assert(writer && writer->error().empty() &&
+         "Errors should have already been handled");
+
+  auto wgsl = writer->result();
+
+  std::pair<FuzzingTarget, OutputFormat> targets[] = {
+      {FuzzingTarget::kHlsl, OutputFormat::kHLSL},
+      {FuzzingTarget::kMsl, OutputFormat::kMSL},
+      {FuzzingTarget::kSpv, OutputFormat::kSpv},
+      {FuzzingTarget::kWgsl, OutputFormat::kWGSL}};
+
+  for (auto target : targets) {
+    if ((target.first & context->params.fuzzing_target) != target.first) {
+      continue;
+    }
+
+    CommonFuzzer fuzzer(InputFormat::kWGSL, target.second);
+    fuzzer.EnableInspector();
+    fuzzer.Run(reinterpret_cast<const uint8_t*>(wgsl.data()), wgsl.size());
+    if (fuzzer.HasErrors()) {
+      util::LogWgslError(fuzzer.GetErrors(), data, size, wgsl, target.second,
+                         context->params.error_dir);
+    }
+  }
+
+  return 0;
+}
+
+}  // namespace
+}  // namespace spvtools_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/fuzzers/tint_spirv_tools_fuzzer/mutator.cc b/fuzzers/tint_spirv_tools_fuzzer/mutator.cc
new file mode 100644
index 0000000..76cae68
--- /dev/null
+++ b/fuzzers/tint_spirv_tools_fuzzer/mutator.cc
@@ -0,0 +1,34 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "fuzzers/tint_spirv_tools_fuzzer/mutator.h"
+
+namespace tint {
+namespace fuzzers {
+namespace spvtools_fuzzer {
+
+// We need to define constructor here so that vtable is produced in this
+// translation unit (see -Wweak-vtables clang flag).
+Mutator::~Mutator() = default;
+
+Mutator::Result::Result(Status status, bool is_changed)
+    : status_(status), is_changed_(is_changed) {
+  assert((is_changed || status == Status::kStuck ||
+          status == Status::kLimitReached) &&
+         "Returning invalid result state");
+}
+
+}  // namespace spvtools_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/fuzzers/tint_spirv_tools_fuzzer/mutator.h b/fuzzers/tint_spirv_tools_fuzzer/mutator.h
new file mode 100644
index 0000000..ee3eecf
--- /dev/null
+++ b/fuzzers/tint_spirv_tools_fuzzer/mutator.h
@@ -0,0 +1,108 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef FUZZERS_TINT_SPIRV_TOOLS_FUZZER_MUTATOR_H_
+#define FUZZERS_TINT_SPIRV_TOOLS_FUZZER_MUTATOR_H_
+
+#include <cassert>
+#include <cstdint>
+#include <string>
+#include <vector>
+
+namespace tint {
+namespace fuzzers {
+namespace spvtools_fuzzer {
+
+/// This is an interface that is used to define custom mutators based on the
+/// SPIR-V tools.
+class Mutator {
+ public:
+  /// The status of the mutation.
+  enum class Status {
+    /// Binary is valid, the limit is not reached - can mutate further.
+    kComplete,
+
+    /// The binary is valid, the limit of mutations has been reached -
+    /// can't mutate further.
+    kLimitReached,
+
+    /// The binary is valid, the limit is not reached but the mutator has spent
+    /// too much time without mutating anything - better to restart to make sure
+    /// we can make any progress.
+    kStuck,
+
+    /// The binary is invalid - this is likely a bug in the mutator - must
+    /// abort.
+    kInvalid
+  };
+
+  /// Represents the result of the mutation. The following states are possible:
+  /// - if `IsChanged() == false`, then `GetStatus()` can be either
+  ///   `kLimitReached` or `kStuck`.
+  /// - otherwise, any value of `Status` is possible.
+  class Result {
+   public:
+    /// Constructor.
+    /// @param status - the status of the mutation.
+    /// @param is_changed - whether the module was changed during mutation.
+    Result(Status status, bool is_changed);
+
+    /// @return the status of the mutation.
+    Status GetStatus() const { return status_; }
+
+    /// @return whether the module was changed during mutation.
+    bool IsChanged() const { return is_changed_; }
+
+   private:
+    Status status_;
+    bool is_changed_;
+  };
+
+  /// Virtual destructor.
+  virtual ~Mutator();
+
+  /// Causes the mutator to apply a mutation. This method can be called
+  /// multiple times as long as the previous call didn't return
+  /// `Status::kInvalid`.
+  ///
+  /// @return the status of the mutation (e.g. success, error etc) and whether
+  ///     the binary was changed during mutation.
+  virtual Result Mutate() = 0;
+
+  /// Returns the mutated binary. The returned binary is guaranteed to be valid
+  /// iff the previous call to the `Mutate` method returned didn't return
+  /// `Status::kInvalid`.
+  ///
+  /// @return the mutated SPIR-V binary. It might be identical to the original
+  ///     binary if `Result::IsChanged` returns `false`.
+  virtual std::vector<uint32_t> GetBinary() const = 0;
+
+  /// Returns errors, produced by the mutator.
+  ///
+  /// @param path - the directory to which the errors are printed to. No files
+  ///     are created if the `path` is nullptr.
+  /// @param count - the number of the error. Files for this error will be
+  ///     prefixed with `count`.
+  virtual void LogErrors(const std::string* path, uint32_t count) const = 0;
+
+  /// @return errors encountered during the mutation. The returned string is
+  ///     if there were no errors during mutation.
+  virtual std::string GetErrors() const = 0;
+};
+
+}  // namespace spvtools_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
+
+#endif  // FUZZERS_TINT_SPIRV_TOOLS_FUZZER_MUTATOR_H_
diff --git a/fuzzers/tint_spirv_tools_fuzzer/mutator_cache.cc b/fuzzers/tint_spirv_tools_fuzzer/mutator_cache.cc
new file mode 100644
index 0000000..03849f7
--- /dev/null
+++ b/fuzzers/tint_spirv_tools_fuzzer/mutator_cache.cc
@@ -0,0 +1,78 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "fuzzers/tint_spirv_tools_fuzzer/mutator_cache.h"
+
+namespace tint {
+namespace fuzzers {
+namespace spvtools_fuzzer {
+
+MutatorCache::MutatorCache(size_t max_size)
+    : map_(), entries_(), max_size_(max_size) {
+  assert(max_size && "`max_size` may not be 0");
+}
+
+MutatorCache::Value::pointer MutatorCache::Get(const Key& key) {
+  auto it = map_.find(key);
+  if (it == map_.end()) {
+    return nullptr;
+  }
+  UpdateUsage(it);
+  return entries_.front().second.get();
+}
+
+void MutatorCache::Put(const Key& key, Value value) {
+  assert(value && "Mutator cache can't have nullptr unique_ptr");
+  auto it = map_.find(key);
+  if (it != map_.end()) {
+    it->second->second = std::move(value);
+    UpdateUsage(it);
+  } else {
+    if (map_.size() == max_size_) {
+      Remove(*entries_.back().first);
+    }
+
+    entries_.emplace_front(nullptr, std::move(value));
+    auto pair = map_.emplace(key, entries_.begin());
+    assert(pair.second && "The key must be unique");
+    entries_.front().first = &pair.first->first;
+  }
+}
+
+MutatorCache::Value MutatorCache::Remove(const Key& key) {
+  auto it = map_.find(key);
+  if (it == map_.end()) {
+    return nullptr;
+  }
+  auto result = std::move(it->second->second);
+  entries_.erase(it->second);
+  map_.erase(it);
+  return result;
+}
+
+size_t MutatorCache::KeyHash::operator()(
+    const std::vector<uint32_t>& vec) const {
+  return std::hash<std::u32string>()({vec.begin(), vec.end()});
+}
+
+void MutatorCache::UpdateUsage(Map::iterator it) {
+  auto entry = std::move(*it->second);
+  entries_.erase(it->second);
+  entries_.push_front(std::move(entry));
+  it->second = entries_.begin();
+}
+
+}  // namespace spvtools_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/fuzzers/tint_spirv_tools_fuzzer/mutator_cache.h b/fuzzers/tint_spirv_tools_fuzzer/mutator_cache.h
new file mode 100644
index 0000000..90dd5ce
--- /dev/null
+++ b/fuzzers/tint_spirv_tools_fuzzer/mutator_cache.h
@@ -0,0 +1,99 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef FUZZERS_TINT_SPIRV_TOOLS_FUZZER_MUTATOR_CACHE_H_
+#define FUZZERS_TINT_SPIRV_TOOLS_FUZZER_MUTATOR_CACHE_H_
+
+#include <cassert>
+#include <list>
+#include <memory>
+#include <unordered_map>
+#include <utility>
+#include <vector>
+
+#include "fuzzers/tint_spirv_tools_fuzzer/mutator.h"
+
+namespace tint {
+namespace fuzzers {
+namespace spvtools_fuzzer {
+
+/// Implementation of a fixed size LRU cache. That is, when the number of
+/// elements reaches a certain threshold, the element that wasn't used for the
+/// longest period of time is removed from the cache when a new element is
+/// inserted. All operations have amortized constant time complexity.
+class MutatorCache {
+ public:
+  /// SPIR-V binary that is being mutated.
+  using Key = std::vector<uint32_t>;
+
+  /// Mutator that is used to mutate the `Key`.
+  using Value = std::unique_ptr<Mutator>;
+
+  /// Constructor.
+  /// @param max_size - the maximum number of elements the cache can store. May
+  ///     not be equal to 0.
+  explicit MutatorCache(size_t max_size);
+
+  /// Retrieves a pointer to a value, associated with a given `key`.
+  ///
+  /// If the key is present in the cache, its usage is updated and the
+  /// (non-null) pointer to the value is returned. Otherwise, `nullptr` is
+  /// returned.
+  ///
+  /// @param key - may not exist in this cache.
+  /// @return non-`nullptr` pointer to a value if `key` exists in the cache.
+  /// @return `nullptr` if `key` doesn't exist in this cache.
+  Value::pointer Get(const Key& key);
+
+  /// Inserts a `key`-`value` pair into the cache.
+  ///
+  /// If the `key` is already present, the `value` replaces the old value and
+  /// the usage of `key` is updated. If the `key` is not present, then:
+  /// - if the number of elements in the cache is equal to `max_size`, the
+  ///   key-value pair, where the usage of the key wasn't updated for the
+  ///   longest period of time, is removed from the cache.
+  /// - a new `key`-`value` pair is inserted into the cache.
+  ///
+  /// @param key - a key.
+  /// @param value - may not be a `nullptr`.
+  void Put(const Key& key, Value value);
+
+  /// Removes `key` and an associated value from the cache.
+  ///
+  /// @param key - a key.
+  /// @return a non-`nullptr` pointer to the removed value, associated with
+  ///     `key`.
+  /// @return `nullptr` if `key` is not present in the cache.
+  Value Remove(const Key& key);
+
+ private:
+  struct KeyHash {
+    size_t operator()(const std::vector<uint32_t>& vec) const;
+  };
+
+  using Entry = std::pair<const Key*, Value>;
+  using Map = std::unordered_map<Key, std::list<Entry>::iterator, KeyHash>;
+
+  void UpdateUsage(Map::iterator it);
+
+  Map map_;
+  std::list<Entry> entries_;
+  const size_t max_size_;
+};
+
+}  // namespace spvtools_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
+
+#endif  // FUZZERS_TINT_SPIRV_TOOLS_FUZZER_MUTATOR_CACHE_H_
diff --git a/fuzzers/tint_spirv_tools_fuzzer/mutator_debugger.cc b/fuzzers/tint_spirv_tools_fuzzer/mutator_debugger.cc
new file mode 100644
index 0000000..26f0419
--- /dev/null
+++ b/fuzzers/tint_spirv_tools_fuzzer/mutator_debugger.cc
@@ -0,0 +1,84 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <memory>
+#include <string>
+
+#include "fuzzers/tint_spirv_tools_fuzzer/cli.h"
+#include "fuzzers/tint_spirv_tools_fuzzer/mutator.h"
+#include "fuzzers/tint_spirv_tools_fuzzer/spirv_fuzz_mutator.h"
+#include "fuzzers/tint_spirv_tools_fuzzer/spirv_opt_mutator.h"
+#include "fuzzers/tint_spirv_tools_fuzzer/spirv_reduce_mutator.h"
+#include "fuzzers/tint_spirv_tools_fuzzer/util.h"
+
+/// This tool is used to debug *mutators*. It uses CLI arguments similar to the
+/// ones used by the fuzzer. To debug some mutator you just need to specify the
+/// mutator type, the seed and the path to the SPIR-V binary that triggered the
+/// error. This tool will run the mutator on the binary until the error is
+/// produced or the mutator returns `kLimitReached`.
+///
+/// Note that this is different from debugging the fuzzer by specifying input
+/// files to test. The difference is that the latter will not execute any
+/// mutator (it will only run the LLVMFuzzerTestOneInput function) whereas this
+/// tool is useful when one of the spirv-tools mutators crashes or produces an
+/// invalid binary in LLVMFuzzerCustomMutator.
+int main(int argc, const char** argv) {
+  auto params =
+      tint::fuzzers::spvtools_fuzzer::ParseMutatorDebuggerCliParams(argc, argv);
+
+  std::unique_ptr<tint::fuzzers::spvtools_fuzzer::Mutator> mutator;
+  const auto& mutator_params = params.mutator_params;
+  switch (params.mutator_type) {
+    case tint::fuzzers::spvtools_fuzzer::MutatorType::kFuzz:
+      mutator =
+          std::make_unique<tint::fuzzers::spvtools_fuzzer::SpirvFuzzMutator>(
+              mutator_params.target_env, params.original_binary, params.seed,
+              mutator_params.donors, mutator_params.enable_all_fuzzer_passes,
+              mutator_params.repeated_pass_strategy,
+              mutator_params.validate_after_each_fuzzer_pass,
+              mutator_params.transformation_batch_size);
+      break;
+    case tint::fuzzers::spvtools_fuzzer::MutatorType::kReduce:
+      mutator =
+          std::make_unique<tint::fuzzers::spvtools_fuzzer::SpirvReduceMutator>(
+              mutator_params.target_env, params.original_binary, params.seed,
+              mutator_params.reduction_batch_size,
+              mutator_params.enable_all_reduce_passes,
+              mutator_params.validate_after_each_reduce_pass);
+      break;
+    case tint::fuzzers::spvtools_fuzzer::MutatorType::kOpt:
+      mutator =
+          std::make_unique<tint::fuzzers::spvtools_fuzzer::SpirvOptMutator>(
+              mutator_params.target_env, params.seed, params.original_binary,
+              mutator_params.validate_after_each_opt_pass,
+              mutator_params.opt_batch_size);
+      break;
+    default:
+      assert(false && "All mutator types must've been handled");
+      return 1;
+  }
+
+  while (true) {
+    auto result = mutator->Mutate();
+    if (result.GetStatus() ==
+        tint::fuzzers::spvtools_fuzzer::Mutator::Status::kInvalid) {
+      std::cerr << mutator->GetErrors() << std::endl;
+      return 0;
+    }
+    if (result.GetStatus() ==
+        tint::fuzzers::spvtools_fuzzer::Mutator::Status::kLimitReached) {
+      break;
+    }
+  }
+}
diff --git a/fuzzers/tint_spirv_tools_fuzzer/spirv_fuzz_mutator.cc b/fuzzers/tint_spirv_tools_fuzzer/spirv_fuzz_mutator.cc
new file mode 100644
index 0000000..dca10f1
--- /dev/null
+++ b/fuzzers/tint_spirv_tools_fuzzer/spirv_fuzz_mutator.cc
@@ -0,0 +1,127 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "fuzzers/tint_spirv_tools_fuzzer/spirv_fuzz_mutator.h"
+
+#include <fstream>
+#include <utility>
+
+#include "fuzzers/tint_spirv_tools_fuzzer/util.h"
+#include "source/opt/build_module.h"
+
+namespace tint {
+namespace fuzzers {
+namespace spvtools_fuzzer {
+
+SpirvFuzzMutator::SpirvFuzzMutator(
+    spv_target_env target_env,
+    std::vector<uint32_t> binary,
+    unsigned seed,
+    const std::vector<spvtools::fuzz::fuzzerutil::ModuleSupplier>& donors,
+    bool enable_all_passes,
+    spvtools::fuzz::RepeatedPassStrategy repeated_pass_strategy,
+    bool validate_after_each_pass,
+    uint32_t transformation_batch_size)
+    : transformation_batch_size_(transformation_batch_size),
+      errors_(std::make_unique<std::stringstream>()),
+      fuzzer_(nullptr),
+      validator_options_(),
+      original_binary_(std::move(binary)),
+      seed_(seed) {
+  auto ir_context = spvtools::BuildModule(
+      target_env, spvtools::fuzz::fuzzerutil::kSilentMessageConsumer,
+      original_binary_.data(), original_binary_.size());
+  assert(ir_context && "|binary| is invalid");
+
+  auto transformation_context =
+      std::make_unique<spvtools::fuzz::TransformationContext>(
+          std::make_unique<spvtools::fuzz::FactManager>(ir_context.get()),
+          validator_options_);
+
+  auto fuzzer_context = std::make_unique<spvtools::fuzz::FuzzerContext>(
+      std::make_unique<spvtools::fuzz::PseudoRandomGenerator>(seed),
+      spvtools::fuzz::FuzzerContext::GetMinFreshId(ir_context.get()), false);
+  fuzzer_ = std::make_unique<spvtools::fuzz::Fuzzer>(
+      std::move(ir_context), std::move(transformation_context),
+      std::move(fuzzer_context), util::GetBufferMessageConsumer(errors_.get()),
+      donors, enable_all_passes, repeated_pass_strategy,
+      validate_after_each_pass, validator_options_);
+}
+
+Mutator::Result SpirvFuzzMutator::Mutate() {
+  // The assertion will fail in |fuzzer_->Run| if the previous fuzzing led to
+  // invalid module.
+  auto result = fuzzer_->Run(transformation_batch_size_);
+  switch (result.status) {
+    case spvtools::fuzz::Fuzzer::Status::kComplete:
+      return {Mutator::Status::kComplete, result.is_changed};
+    case spvtools::fuzz::Fuzzer::Status::kModuleTooBig:
+    case spvtools::fuzz::Fuzzer::Status::kTransformationLimitReached:
+      return {Mutator::Status::kLimitReached, result.is_changed};
+    case spvtools::fuzz::Fuzzer::Status::kFuzzerStuck:
+      return {Mutator::Status::kStuck, result.is_changed};
+    case spvtools::fuzz::Fuzzer::Status::kFuzzerPassLedToInvalidModule:
+      return {Mutator::Status::kInvalid, result.is_changed};
+  }
+}
+
+std::vector<uint32_t> SpirvFuzzMutator::GetBinary() const {
+  std::vector<uint32_t> result;
+  fuzzer_->GetIRContext()->module()->ToBinary(&result, true);
+  return result;
+}
+
+std::string SpirvFuzzMutator::GetErrors() const {
+  return errors_->str();
+}
+
+void SpirvFuzzMutator::LogErrors(const std::string* path,
+                                 uint32_t count) const {
+  auto message = GetErrors();
+  std::cout << count << " | SpirvFuzzMutator (seed: " << seed_ << ")"
+            << std::endl;
+  std::cout << message << std::endl;
+
+  if (path) {
+    auto prefix = *path + std::to_string(count);
+
+    // Write errors to file.
+    std::ofstream(prefix + ".fuzzer.log") << "seed: " << seed_ << std::endl
+                                          << message << std::endl;
+
+    // Write the invalid SPIR-V binary.
+    util::WriteBinary(prefix + ".fuzzer.invalid.spv", GetBinary());
+
+    // Write the original SPIR-V binary.
+    util::WriteBinary(prefix + ".fuzzer.original.spv", original_binary_);
+
+    // Write transformations.
+    google::protobuf::util::JsonOptions options;
+    options.add_whitespace = true;
+    std::string json;
+    google::protobuf::util::MessageToJsonString(
+        fuzzer_->GetTransformationSequence(), &json, options);
+    std::ofstream(prefix + ".fuzzer.transformations.json") << json << std::endl;
+
+    std::ofstream binary_transformations(
+        prefix + ".fuzzer.transformations.binary",
+        std::ios::binary | std::ios::out);
+    fuzzer_->GetTransformationSequence().SerializeToOstream(
+        &binary_transformations);
+  }
+}
+
+}  // namespace spvtools_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/fuzzers/tint_spirv_tools_fuzzer/spirv_fuzz_mutator.h b/fuzzers/tint_spirv_tools_fuzzer/spirv_fuzz_mutator.h
new file mode 100644
index 0000000..79aa07d
--- /dev/null
+++ b/fuzzers/tint_spirv_tools_fuzzer/spirv_fuzz_mutator.h
@@ -0,0 +1,95 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef FUZZERS_TINT_SPIRV_TOOLS_FUZZER_SPIRV_FUZZ_MUTATOR_H_
+#define FUZZERS_TINT_SPIRV_TOOLS_FUZZER_SPIRV_FUZZ_MUTATOR_H_
+
+#include <memory>
+#include <sstream>
+#include <string>
+#include <vector>
+
+#include "fuzzers/tint_spirv_tools_fuzzer/mutator.h"
+
+#include "source/fuzz/fuzzer.h"
+#include "source/fuzz/protobufs/spirvfuzz_protobufs.h"
+#include "source/fuzz/pseudo_random_generator.h"
+
+namespace tint {
+namespace fuzzers {
+namespace spvtools_fuzzer {
+
+/// The mutator that uses spirv-fuzz to mutate SPIR-V.
+///
+/// The initial `binary` must be valid according to `target_env`. All other
+/// parameters (except for the `seed` which just initializes the RNG) are from
+/// the `spvtools::fuzz::Fuzzer` class.
+class SpirvFuzzMutator : public Mutator {
+ public:
+  /// Constructor.
+  /// @param target_env - the target environment for the `binary`.
+  /// @param binary - the SPIR-V binary. Must be valid.
+  /// @param seed - seed for the RNG.
+  /// @param donors - vector of donor suppliers.
+  /// @param enable_all_passes - whether to use all fuzzer passes.
+  /// @param repeated_pass_strategy - the strategy to use when selecting the
+  ///     next fuzzer pass.
+  /// @param validate_after_each_pass - whether to validate the binary after
+  ///     each fuzzer pass.
+  /// @param transformation_batch_size - the maximum number of transformations
+  ///     that will be applied during a single call to `Mutate`. It it's equal
+  ///     to 0 then we apply as much transformations as we can until the
+  ///     threshold in the spvtools::fuzz::Fuzzer is reached (see the doc for
+  ///     that class for more info).
+  SpirvFuzzMutator(
+      spv_target_env target_env,
+      std::vector<uint32_t> binary,
+      uint32_t seed,
+      const std::vector<spvtools::fuzz::fuzzerutil::ModuleSupplier>& donors,
+      bool enable_all_passes,
+      spvtools::fuzz::RepeatedPassStrategy repeated_pass_strategy,
+      bool validate_after_each_pass,
+      uint32_t transformation_batch_size);
+
+  Result Mutate() override;
+  std::vector<uint32_t> GetBinary() const override;
+  void LogErrors(const std::string* path, uint32_t count) const override;
+  std::string GetErrors() const override;
+
+ private:
+  // The number of transformations that will be applied during a single call to
+  // the `Mutate` method. Is this only a lower bound since transformations are
+  // applied in batches by fuzzer passes (see docs for the
+  // `spvtools::fuzz::Fuzzer` for more info).
+  const uint32_t transformation_batch_size_;
+
+  // The errors produced by the `spvtools::fuzz::Fuzzer`.
+  std::unique_ptr<std::stringstream> errors_;
+  std::unique_ptr<spvtools::fuzz::Fuzzer> fuzzer_;
+  spvtools::ValidatorOptions validator_options_;
+
+  // The following fields are useful for debugging.
+
+  // The binary that the mutator is constructed with.
+  const std::vector<uint32_t> original_binary_;
+
+  // The seed that the mutator is constructed with.
+  const uint32_t seed_;
+};
+
+}  // namespace spvtools_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
+
+#endif  // FUZZERS_TINT_SPIRV_TOOLS_FUZZER_SPIRV_FUZZ_MUTATOR_H_
diff --git a/fuzzers/tint_spirv_tools_fuzzer/spirv_opt_mutator.cc b/fuzzers/tint_spirv_tools_fuzzer/spirv_opt_mutator.cc
new file mode 100644
index 0000000..360f1d9
--- /dev/null
+++ b/fuzzers/tint_spirv_tools_fuzzer/spirv_opt_mutator.cc
@@ -0,0 +1,159 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "fuzzers/tint_spirv_tools_fuzzer/spirv_opt_mutator.h"
+
+#include <fstream>
+#include <iostream>
+#include <unordered_set>
+#include <utility>
+
+#include "fuzzers/tint_spirv_tools_fuzzer/util.h"
+#include "spirv-tools/optimizer.hpp"
+
+namespace tint {
+namespace fuzzers {
+namespace spvtools_fuzzer {
+
+SpirvOptMutator::SpirvOptMutator(spv_target_env target_env,
+                                 uint32_t seed,
+                                 std::vector<uint32_t> binary,
+                                 bool validate_after_each_opt,
+                                 uint32_t opt_batch_size)
+    : num_executions_(0),
+      is_valid_(true),
+      target_env_(target_env),
+      original_binary_(std::move(binary)),
+      seed_(seed),
+      opt_passes_({"--combine-access-chains",
+                   "--loop-unroll",
+                   "--merge-blocks",
+                   "--cfg-cleanup",
+                   "--eliminate-dead-functions",
+                   "--merge-return",
+                   "--wrap-opkill",
+                   "--eliminate-dead-code-aggressive",
+                   "--if-conversion",
+                   "--eliminate-local-single-store",
+                   "--eliminate-local-single-block",
+                   "--eliminate-dead-branches",
+                   "--scalar-replacement=0",
+                   "--eliminate-dead-inserts",
+                   "--eliminate-dead-members",
+                   "--simplify-instructions",
+                   "--private-to-local",
+                   "--ssa-rewrite",
+                   "--ccp",
+                   "--reduce-load-size",
+                   "--vector-dce",
+                   "--scalar-replacement=100",
+                   "--inline-entry-points-exhaustive",
+                   "--redundancy-elimination",
+                   "--convert-local-access-chains",
+                   "--copy-propagate-arrays",
+                   "--fix-storage-class"}),
+      optimized_binary_(),
+      validate_after_each_opt_(validate_after_each_opt),
+      opt_batch_size_(opt_batch_size),
+      rng_(seed) {
+  assert(spvtools::SpirvTools(target_env).Validate(original_binary_) &&
+         "Initial binary is invalid");
+  assert(!opt_passes_.empty() && "Must be at least one pass");
+}
+
+SpirvOptMutator::Result SpirvOptMutator::Mutate() {
+  assert(is_valid_ && "The optimizer is not longer valid");
+
+  const uint32_t kMaxNumExecutions = 100;
+  const uint32_t kMaxNumStuck = 10;
+
+  if (num_executions_ == kMaxNumExecutions) {
+    // We've applied this mutator many times already. Indicate to the user that
+    // it might be better to try a different mutator.
+    return {Status::kLimitReached, false};
+  }
+
+  num_executions_++;
+
+  // Get the input binary. If this is the first time we run this mutator, use
+  // the `original_binary_`. Otherwise, one of the following will be true:
+  // - the `optimized_binary_` is not empty.
+  // - the previous call to the `Mutate` method returned `kStuck`.
+  auto binary = num_executions_ == 1 ? original_binary_ : optimized_binary_;
+  optimized_binary_.clear();
+
+  assert(!binary.empty() && "Can't run the optimizer on an empty binary");
+
+  // Number of times spirv-opt wasn't able to produce any new result.
+  uint32_t num_stuck = 0;
+  do {
+    // Randomly select `opt_batch_size` optimization passes. If `opt_batch_size`
+    // is equal to 0, we will use the number of passes equal to the number of
+    // all available passes.
+    auto num_of_passes = opt_batch_size_ ? opt_batch_size_ : opt_passes_.size();
+    std::vector<std::string> passes;
+
+    while (passes.size() < num_of_passes) {
+      auto idx = std::uniform_int_distribution<size_t>(
+          0, opt_passes_.size() - 1)(rng_);
+      passes.push_back(opt_passes_[idx]);
+    }
+
+    // Run the `binary` into the `optimized_binary_`.
+    spvtools::Optimizer optimizer(target_env_);
+    optimizer.SetMessageConsumer(util::GetBufferMessageConsumer(&errors_));
+    optimizer.SetValidateAfterAll(validate_after_each_opt_);
+    optimizer.RegisterPassesFromFlags(passes);
+    if (!optimizer.Run(binary.data(), binary.size(), &optimized_binary_)) {
+      is_valid_ = false;
+      return {Status::kInvalid, true};
+    }
+  } while (optimized_binary_.empty() && ++num_stuck < kMaxNumStuck);
+
+  return {optimized_binary_.empty() ? Status::kStuck : Status::kComplete,
+          !optimized_binary_.empty()};
+}
+
+std::vector<uint32_t> SpirvOptMutator::GetBinary() const {
+  return optimized_binary_;
+}
+
+std::string SpirvOptMutator::GetErrors() const {
+  return errors_.str();
+}
+
+void SpirvOptMutator::LogErrors(const std::string* path, uint32_t count) const {
+  auto message = GetErrors();
+  std::cout << count << " | SpirvOptMutator (seed: " << seed_ << ")"
+            << std::endl;
+  std::cout << message << std::endl;
+
+  if (path) {
+    auto prefix = *path + std::to_string(count);
+
+    // Write errors to file.
+    std::ofstream(prefix + ".opt.log") << "seed: " << seed_ << std::endl
+                                       << message << std::endl;
+
+    // Write the invalid SPIR-V binary.
+    util::WriteBinary(prefix + ".opt.invalid.spv", optimized_binary_);
+
+    // Write the original SPIR-V binary.
+    util::WriteBinary(prefix + ".opt.original.spv", original_binary_);
+  }
+}
+
+}  // namespace spvtools_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/fuzzers/tint_spirv_tools_fuzzer/spirv_opt_mutator.h b/fuzzers/tint_spirv_tools_fuzzer/spirv_opt_mutator.h
new file mode 100644
index 0000000..6d055e4
--- /dev/null
+++ b/fuzzers/tint_spirv_tools_fuzzer/spirv_opt_mutator.h
@@ -0,0 +1,96 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef FUZZERS_TINT_SPIRV_TOOLS_FUZZER_SPIRV_OPT_MUTATOR_H_
+#define FUZZERS_TINT_SPIRV_TOOLS_FUZZER_SPIRV_OPT_MUTATOR_H_
+
+#include <random>
+#include <sstream>
+#include <string>
+#include <vector>
+
+#include "fuzzers/tint_spirv_tools_fuzzer/mutator.h"
+#include "spirv-tools/libspirv.h"
+
+namespace tint {
+namespace fuzzers {
+namespace spvtools_fuzzer {
+
+/// Mutates the SPIR-V module using the spirv-opt tool.
+///
+/// The initial `binary` must be valid according to `target_env`. On each call
+/// to the `Mutate` method the mutator selects `opt_batch_size` random
+/// optimization passes (with substitutions) and applies them to the binary.
+class SpirvOptMutator : public Mutator {
+ public:
+  /// Constructor.
+  /// @param target_env - target environment for the `binary`.
+  /// @param seed - seed for the RNG.
+  /// @param binary - SPIR-V binary. Must be valid.
+  /// @param validate_after_each_opt - whether to validate the binary after each
+  ///     optimization pass.
+  /// @param opt_batch_size - the maximum number of optimization passes that
+  ///     will be applied in a single call to `Mutate`. If it's equal to 0 then
+  ///     all available optimization passes are applied.
+  SpirvOptMutator(spv_target_env target_env,
+                  uint32_t seed,
+                  std::vector<uint32_t> binary,
+                  bool validate_after_each_opt,
+                  uint32_t opt_batch_size);
+
+  Result Mutate() override;
+  std::vector<uint32_t> GetBinary() const override;
+  void LogErrors(const std::string* path, uint32_t count) const override;
+  std::string GetErrors() const override;
+
+ private:
+  // Number of times this mutator was executed.
+  uint32_t num_executions_;
+
+  // Whether the last execution left it in a valid state.
+  bool is_valid_;
+
+  // Target environment for the SPIR-V binary.
+  const spv_target_env target_env_;
+
+  // The original SPIR-V binary. Useful for debugging.
+  const std::vector<uint32_t> original_binary_;
+
+  // The seed for the RNG. Useful for debugging.
+  const uint32_t seed_;
+
+  // All the optimization passes available.
+  const std::vector<std::string> opt_passes_;
+
+  // The result of the optimization.
+  std::vector<uint32_t> optimized_binary_;
+
+  // Whether we need to validate the binary after each optimization pass.
+  const bool validate_after_each_opt_;
+
+  // The number of optimization passes to apply at once.
+  const uint32_t opt_batch_size_;
+
+  // All the errors produced by the optimizer.
+  std::stringstream errors_;
+
+  // The random number generator initialized with `seed_`.
+  std::mt19937 rng_;
+};
+
+}  // namespace spvtools_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
+
+#endif  // FUZZERS_TINT_SPIRV_TOOLS_FUZZER_SPIRV_OPT_MUTATOR_H_
diff --git a/fuzzers/tint_spirv_tools_fuzzer/spirv_reduce_mutator.cc b/fuzzers/tint_spirv_tools_fuzzer/spirv_reduce_mutator.cc
new file mode 100644
index 0000000..dccbfe3
--- /dev/null
+++ b/fuzzers/tint_spirv_tools_fuzzer/spirv_reduce_mutator.cc
@@ -0,0 +1,190 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "fuzzers/tint_spirv_tools_fuzzer/spirv_reduce_mutator.h"
+
+#include <fstream>
+
+#include "fuzzers/tint_spirv_tools_fuzzer/util.h"
+#include "source/fuzz/fuzzer_util.h"
+#include "source/opt/build_module.h"
+#include "source/reduce/conditional_branch_to_simple_conditional_branch_opportunity_finder.h"
+#include "source/reduce/merge_blocks_reduction_opportunity_finder.h"
+#include "source/reduce/operand_to_const_reduction_opportunity_finder.h"
+#include "source/reduce/operand_to_dominating_id_reduction_opportunity_finder.h"
+#include "source/reduce/operand_to_undef_reduction_opportunity_finder.h"
+#include "source/reduce/remove_block_reduction_opportunity_finder.h"
+#include "source/reduce/remove_function_reduction_opportunity_finder.h"
+#include "source/reduce/remove_selection_reduction_opportunity_finder.h"
+#include "source/reduce/remove_unused_instruction_reduction_opportunity_finder.h"
+#include "source/reduce/remove_unused_struct_member_reduction_opportunity_finder.h"
+#include "source/reduce/simple_conditional_branch_to_branch_opportunity_finder.h"
+#include "source/reduce/structured_loop_to_selection_reduction_opportunity_finder.h"
+
+namespace tint {
+namespace fuzzers {
+namespace spvtools_fuzzer {
+
+SpirvReduceMutator::SpirvReduceMutator(spv_target_env target_env,
+                                       std::vector<uint32_t> binary,
+                                       uint32_t seed,
+                                       uint32_t reductions_batch_size,
+                                       bool enable_all_reductions,
+                                       bool validate_after_each_reduction)
+    : ir_context_(nullptr),
+      finders_(),
+      rng_(seed),
+      errors_(),
+      is_valid_(true),
+      reductions_batch_size_(reductions_batch_size),
+      total_applied_reductions_(0),
+      enable_all_reductions_(enable_all_reductions),
+      validate_after_each_reduction_(validate_after_each_reduction),
+      original_binary_(std::move(binary)),
+      seed_(seed) {
+  ir_context_ = spvtools::BuildModule(
+      target_env, spvtools::fuzz::fuzzerutil::kSilentMessageConsumer,
+      original_binary_.data(), original_binary_.size());
+  assert(ir_context_ && "|binary| is invalid");
+
+  do {
+    MaybeAddFinder<
+        spvtools::reduce::
+            ConditionalBranchToSimpleConditionalBranchOpportunityFinder>();
+    MaybeAddFinder<spvtools::reduce::MergeBlocksReductionOpportunityFinder>();
+    MaybeAddFinder<
+        spvtools::reduce::OperandToConstReductionOpportunityFinder>();
+    MaybeAddFinder<
+        spvtools::reduce::OperandToDominatingIdReductionOpportunityFinder>();
+    MaybeAddFinder<
+        spvtools::reduce::OperandToUndefReductionOpportunityFinder>();
+    MaybeAddFinder<spvtools::reduce::RemoveBlockReductionOpportunityFinder>();
+    MaybeAddFinder<
+        spvtools::reduce::RemoveFunctionReductionOpportunityFinder>();
+    MaybeAddFinder<
+        spvtools::reduce::RemoveSelectionReductionOpportunityFinder>();
+    MaybeAddFinder<
+        spvtools::reduce::RemoveUnusedInstructionReductionOpportunityFinder>(
+        true);
+    MaybeAddFinder<
+        spvtools::reduce::RemoveUnusedStructMemberReductionOpportunityFinder>();
+    MaybeAddFinder<
+        spvtools::reduce::SimpleConditionalBranchToBranchOpportunityFinder>();
+    MaybeAddFinder<spvtools::reduce::
+                       StructuredLoopToSelectionReductionOpportunityFinder>();
+  } while (finders_.empty());
+}
+
+Mutator::Result SpirvReduceMutator::Mutate() {
+  assert(is_valid_ && "Can't mutate invalid module");
+
+  // The upper limit on the number of applied reduction passes.
+  const uint32_t kMaxAppliedReductions = 500;
+  const auto old_applied_reductions = total_applied_reductions_;
+
+  // The upper limit on the number of failed attempts to apply reductions (i.e.
+  // when no reduction was returned by the reduction finder).
+  const uint32_t kMaxConsecutiveFailures = 10;
+  uint32_t num_consecutive_failures = 0;
+
+  // Iterate while we haven't exceeded the limit on the total number of applied
+  // reductions, the limit on the number of reductions applied at once and limit
+  // on the number of consecutive failed attempts.
+  while (total_applied_reductions_ < kMaxAppliedReductions &&
+         (reductions_batch_size_ == 0 ||
+          total_applied_reductions_ - old_applied_reductions <
+              reductions_batch_size_) &&
+         num_consecutive_failures < kMaxConsecutiveFailures) {
+    // Select an opportunity finder and get some reduction opportunities from
+    // it.
+    auto finder = GetRandomElement(&finders_);
+    auto reduction_opportunities =
+        finder->GetAvailableOpportunities(ir_context_.get(), 0);
+
+    if (reduction_opportunities.empty()) {
+      // There is nothing to reduce. We increase the counter to make sure we
+      // don't stuck in this situation.
+      num_consecutive_failures++;
+    } else {
+      // Apply a random reduction opportunity. The latter should be applicable.
+      auto opportunity = GetRandomElement(&reduction_opportunities);
+      assert(opportunity->PreconditionHolds() && "Preconditions should hold");
+      total_applied_reductions_++;
+      num_consecutive_failures = 0;
+      if (!ApplyReduction(opportunity)) {
+        // The module became invalid as a result of the applied reduction.
+        is_valid_ = false;
+        return {Mutator::Status::kInvalid,
+                total_applied_reductions_ != old_applied_reductions};
+      }
+    }
+  }
+
+  auto is_changed = total_applied_reductions_ != old_applied_reductions;
+  if (total_applied_reductions_ == kMaxAppliedReductions) {
+    return {Mutator::Status::kLimitReached, is_changed};
+  }
+
+  if (num_consecutive_failures == kMaxConsecutiveFailures) {
+    return {Mutator::Status::kStuck, is_changed};
+  }
+
+  assert(is_changed && "This is the only way left to break the loop");
+  return {Mutator::Status::kComplete, is_changed};
+}
+
+bool SpirvReduceMutator::ApplyReduction(
+    spvtools::reduce::ReductionOpportunity* reduction_opportunity) {
+  reduction_opportunity->TryToApply();
+  return !validate_after_each_reduction_ ||
+         spvtools::fuzz::fuzzerutil::IsValidAndWellFormed(
+             ir_context_.get(), spvtools::ValidatorOptions(),
+             util::GetBufferMessageConsumer(&errors_));
+}
+
+std::vector<uint32_t> SpirvReduceMutator::GetBinary() const {
+  std::vector<uint32_t> result;
+  ir_context_->module()->ToBinary(&result, true);
+  return result;
+}
+
+std::string SpirvReduceMutator::GetErrors() const {
+  return errors_.str();
+}
+
+void SpirvReduceMutator::LogErrors(const std::string* path,
+                                   uint32_t count) const {
+  auto message = GetErrors();
+  std::cout << count << " | SpirvReduceMutator (seed: " << seed_ << ")"
+            << std::endl;
+  std::cout << message << std::endl;
+
+  if (path) {
+    auto prefix = *path + std::to_string(count);
+
+    // Write errors to file.
+    std::ofstream(prefix + ".reducer.log") << "seed: " << seed_ << std::endl
+                                           << message << std::endl;
+
+    // Write the invalid SPIR-V binary.
+    util::WriteBinary(prefix + ".reducer.invalid.spv", GetBinary());
+
+    // Write the original SPIR-V binary.
+    util::WriteBinary(prefix + ".reducer.original.spv", original_binary_);
+  }
+}
+
+}  // namespace spvtools_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/fuzzers/tint_spirv_tools_fuzzer/spirv_reduce_mutator.h b/fuzzers/tint_spirv_tools_fuzzer/spirv_reduce_mutator.h
new file mode 100644
index 0000000..4d656d8
--- /dev/null
+++ b/fuzzers/tint_spirv_tools_fuzzer/spirv_reduce_mutator.h
@@ -0,0 +1,134 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef FUZZERS_TINT_SPIRV_TOOLS_FUZZER_SPIRV_REDUCE_MUTATOR_H_
+#define FUZZERS_TINT_SPIRV_TOOLS_FUZZER_SPIRV_REDUCE_MUTATOR_H_
+
+#include <memory>
+#include <random>
+#include <sstream>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "fuzzers/tint_spirv_tools_fuzzer/mutator.h"
+
+#include "source/reduce/reduction_opportunity_finder.h"
+
+namespace tint {
+namespace fuzzers {
+namespace spvtools_fuzzer {
+
+/// Mutates SPIR-V binary by running spirv-reduce tool.
+///
+/// The initial `binary` must be valid according to `target_env`. Applies at
+/// most `reductions_batch_size` reductions at a time. This parameter is ignored
+/// if its value is 0. Uses a random subset of reduction opportunity finders by
+/// default. This can be overridden with the `enable_all_reductions` parameter.
+class SpirvReduceMutator : public Mutator {
+ public:
+  /// Constructor.
+  /// @param target_env - the target environment for the `binary`.
+  /// @param binary - SPIR-V binary. Must be valid.
+  /// @param seed - the seed for the RNG.
+  /// @param reductions_batch_size - the number of reduction passes that will be
+  ///     applied during a single call to `Mutate`. If it's equal to 0 then we
+  ///     apply the passes until we reach the threshold for the total number of
+  ///     applied passes.
+  /// @param enable_all_reductions - whether to use all reduction passes or only
+  ///     a randomly selected subset of them.
+  /// @param validate_after_each_reduction - whether to validate after each
+  ///     applied reduction.
+  SpirvReduceMutator(spv_target_env target_env,
+                     std::vector<uint32_t> binary,
+                     uint32_t seed,
+                     uint32_t reductions_batch_size,
+                     bool enable_all_reductions,
+                     bool validate_after_each_reduction);
+
+  Result Mutate() override;
+  std::vector<uint32_t> GetBinary() const override;
+  void LogErrors(const std::string* path, uint32_t count) const override;
+  std::string GetErrors() const override;
+
+ private:
+  template <typename T, typename... Args>
+  void MaybeAddFinder(Args&&... args) {
+    if (enable_all_reductions_ || std::uniform_int_distribution<>(0, 1)(rng_)) {
+      finders_.push_back(std::make_unique<T>(std::forward<Args>(args)...));
+    }
+  }
+
+  template <typename T>
+  T* GetRandomElement(std::vector<T>* arr) {
+    assert(!arr->empty() && "Can't get random element from an empty vector");
+    auto index =
+        std::uniform_int_distribution<size_t>(0, arr->size() - 1)(rng_);
+    return &(*arr)[index];
+  }
+
+  template <typename T>
+  T* GetRandomElement(std::vector<std::unique_ptr<T>>* arr) {
+    assert(!arr->empty() && "Can't get random element from an empty vector");
+    auto index =
+        std::uniform_int_distribution<size_t>(0, arr->size() - 1)(rng_);
+    return (*arr)[index].get();
+  }
+
+  bool ApplyReduction(
+      spvtools::reduce::ReductionOpportunity* reduction_opportunity);
+
+  // The SPIR-V binary that is being reduced.
+  std::unique_ptr<spvtools::opt::IRContext> ir_context_;
+
+  // The selected subset of reduction opportunity finders.
+  std::vector<std::unique_ptr<spvtools::reduce::ReductionOpportunityFinder>>
+      finders_;
+
+  // Random number generator initialized with `seed_`.
+  std::mt19937 rng_;
+
+  // All the errors produced by the reducer.
+  std::stringstream errors_;
+
+  // Whether the last call to the `Mutate` method produced the valid binary.
+  bool is_valid_;
+
+  // The number of reductions to apply on a single call to `Mutate`.
+  const uint32_t reductions_batch_size_;
+
+  // The total number of applied reductions.
+  uint32_t total_applied_reductions_;
+
+  // Whether we want to use all the reduction opportunity finders and not just a
+  // subset of them.
+  const bool enable_all_reductions_;
+
+  // Whether we want to validate all the binary after each reduction.
+  const bool validate_after_each_reduction_;
+
+  // The original binary that was used to initialize this mutator.
+  // Useful for debugging.
+  const std::vector<uint32_t> original_binary_;
+
+  // The seed that was used to initialize the random number generator.
+  // Useful for debugging.
+  const uint32_t seed_;
+};
+
+}  // namespace spvtools_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
+
+#endif  // FUZZERS_TINT_SPIRV_TOOLS_FUZZER_SPIRV_REDUCE_MUTATOR_H_
diff --git a/fuzzers/tint_spirv_tools_fuzzer/util.cc b/fuzzers/tint_spirv_tools_fuzzer/util.cc
new file mode 100644
index 0000000..bd574a2
--- /dev/null
+++ b/fuzzers/tint_spirv_tools_fuzzer/util.cc
@@ -0,0 +1,160 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <fstream>
+#include <iostream>
+
+#include "fuzzers/tint_spirv_tools_fuzzer/util.h"
+
+namespace tint {
+namespace fuzzers {
+namespace spvtools_fuzzer {
+namespace util {
+namespace {
+
+bool WriteBinary(const std::string& path, const uint8_t* data, size_t size) {
+  std::ofstream spv(path, std::ios::binary);
+  return spv && spv.write(reinterpret_cast<const char*>(data),
+                          static_cast<std::streamsize>(size));
+}
+
+void LogError(uint32_t index,
+              const std::string& type,
+              const std::string& message,
+              const std::string* path,
+              const uint8_t* data,
+              size_t size,
+              const std::string* wgsl) {
+  std::cout << index << " | " << type << ": " << message << std::endl;
+
+  if (path) {
+    auto prefix = *path + std::to_string(index);
+    std::ofstream(prefix + ".log") << message << std::endl;
+
+    WriteBinary(prefix + ".spv", data, size);
+
+    if (wgsl) {
+      std::ofstream(prefix + ".wgsl") << *wgsl << std::endl;
+    }
+  }
+}
+
+}  // namespace
+
+spvtools::MessageConsumer GetBufferMessageConsumer(std::stringstream* buffer) {
+  return [buffer](spv_message_level_t level, const char*,
+                  const spv_position_t& position, const char* message) {
+    std::string status;
+    switch (level) {
+      case SPV_MSG_FATAL:
+      case SPV_MSG_INTERNAL_ERROR:
+      case SPV_MSG_ERROR:
+        status = "ERROR";
+        break;
+      case SPV_MSG_WARNING:
+      case SPV_MSG_INFO:
+      case SPV_MSG_DEBUG:
+        status = "INFO";
+        break;
+    }
+    *buffer << status << " " << position.line << ":" << position.column << ":"
+            << position.index << ": " << message << std::endl;
+  };
+}
+
+void LogMutatorError(const Mutator& mutator, const std::string& error_dir) {
+  static uint32_t mutator_count = 0;
+  auto error_path = error_dir.empty() ? error_dir : error_dir + "/mutator/";
+  mutator.LogErrors(error_dir.empty() ? nullptr : &error_path, mutator_count++);
+}
+
+void LogWgslError(const std::string& message,
+                  const uint8_t* data,
+                  size_t size,
+                  const std::string& wgsl,
+                  OutputFormat output_format,
+                  const std::string& error_dir) {
+  static uint32_t wgsl_count = 0;
+  std::string error_type;
+  switch (output_format) {
+    case OutputFormat::kSpv:
+      error_type = "WGSL -> SPV";
+      break;
+    case OutputFormat::kMSL:
+      error_type = "WGSL -> MSL";
+      break;
+    case OutputFormat::kHLSL:
+      error_type = "WGSL -> HLSL";
+      break;
+    case OutputFormat::kWGSL:
+      error_type = "WGSL -> WGSL";
+      break;
+    default:
+      assert(false && "All output formats should've been handled");
+      break;
+  }
+  auto error_path = error_dir.empty() ? error_dir : error_dir + "/wgsl/";
+  LogError(wgsl_count++, error_type, message,
+           error_dir.empty() ? nullptr : &error_path, data, size, &wgsl);
+}
+
+void LogSpvError(const std::string& message,
+                 const uint8_t* data,
+                 size_t size,
+                 const std::string& error_dir) {
+  static uint32_t spv_count = 0;
+  auto error_path = error_dir.empty() ? error_dir : error_dir + "/spv/";
+  LogError(spv_count++, "SPV -> WGSL", message,
+           error_dir.empty() ? nullptr : &error_path, data, size, nullptr);
+}
+
+bool ReadBinary(const std::string& path, std::vector<uint32_t>* out) {
+  if (!out) {
+    return false;
+  }
+
+  std::ifstream file(path, std::ios::binary | std::ios::ate);
+  if (!file) {
+    return false;
+  }
+
+  auto size = file.tellg();
+  if (!file) {
+    return false;
+  }
+
+  file.seekg(0);
+  if (!file) {
+    return false;
+  }
+
+  std::vector<char> binary(static_cast<size_t>(size));
+  if (!file.read(binary.data(), size)) {
+    return false;
+  }
+
+  out->resize(binary.size() / sizeof(uint32_t));
+  std::memcpy(out->data(), binary.data(), binary.size());
+  return true;
+}
+
+bool WriteBinary(const std::string& path, const std::vector<uint32_t>& binary) {
+  return WriteBinary(path, reinterpret_cast<const uint8_t*>(binary.data()),
+                     binary.size() * sizeof(uint32_t));
+}
+
+}  // namespace util
+}  // namespace spvtools_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/fuzzers/tint_spirv_tools_fuzzer/util.h b/fuzzers/tint_spirv_tools_fuzzer/util.h
new file mode 100644
index 0000000..b5abe47
--- /dev/null
+++ b/fuzzers/tint_spirv_tools_fuzzer/util.h
@@ -0,0 +1,96 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef FUZZERS_TINT_SPIRV_TOOLS_FUZZER_UTIL_H_
+#define FUZZERS_TINT_SPIRV_TOOLS_FUZZER_UTIL_H_
+
+#include <sstream>
+#include <string>
+#include <vector>
+
+#include "fuzzers/tint_common_fuzzer.h"
+#include "fuzzers/tint_spirv_tools_fuzzer/mutator.h"
+
+#include "spirv-tools/libspirv.hpp"
+
+namespace tint {
+namespace fuzzers {
+namespace spvtools_fuzzer {
+namespace util {
+
+/// @param buffer will be used to output errors by the returned message
+///     consumer. Must remain in scope as long as the returned consumer is in
+///     scope.
+/// @return the message consumer that will print errors to the `buffer`.
+spvtools::MessageConsumer GetBufferMessageConsumer(std::stringstream* buffer);
+
+/// Output errors from the SPV -> WGSL conversion.
+///
+/// @param message - the error message.
+/// @param data - invalid SPIR-V binary.
+/// @param size - the size of `data`.
+/// @param error_dir - the directory, to which the binary will be printed to.
+///     If it's empty, the invalid binary and supplemental files will not be
+///     printed. Otherwise, it must have a `spv/` subdirectory.
+void LogSpvError(const std::string& message,
+                 const uint8_t* data,
+                 size_t size,
+                 const std::string& error_dir);
+
+/// Output errors from the WGSL -> `output_format` conversion.
+///
+/// @param message - the error message.
+/// @param data - the SPIR-V binary that generated the WGSL binary.
+/// @param size - the size of `data`.
+/// @param wgsl - the invalid WGSL binary.
+/// @param output_format - the format which we attempted to convert `wgsl` to.
+/// @param error_dir - the directory, to which the binary will be printed out.
+///     If it's empty, the invalid binary and supplemental files will not be
+///     printed. Otherwise, it must have a `wgsl/` subdirectory.
+void LogWgslError(const std::string& message,
+                  const uint8_t* data,
+                  size_t size,
+                  const std::string& wgsl,
+                  OutputFormat output_format,
+                  const std::string& error_dir);
+
+/// Output errors produced by the mutator.
+///
+/// @param mutator - the mutator with invalid state.
+/// @param error_dir - the directory, to which invalid files will be printed to.
+///     If it's empty, the invalid binary and supplemental files will not be
+///     printed. Otherwise, it must have a `mutator/` subdirectory.
+void LogMutatorError(const Mutator& mutator, const std::string& error_dir);
+
+/// Reads SPIR-V binary from `path` into `out`. Returns `true` if successful and
+/// `false` otherwise (in this case, `out` is unchanged).
+///
+/// @param path - the path to the SPIR-V binary.
+/// @param out - may be a `nullptr`. In this case, `false` is returned.
+/// @return `true` if successful and `false` otherwise.
+bool ReadBinary(const std::string& path, std::vector<uint32_t>* out);
+
+/// Writes `binary` into `path`.
+///
+/// @param path - the path to write `binary` to.
+/// @param binary - SPIR-V binary.
+/// @return whether the operation was successful.
+bool WriteBinary(const std::string& path, const std::vector<uint32_t>& binary);
+
+}  // namespace util
+}  // namespace spvtools_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
+
+#endif  // FUZZERS_TINT_SPIRV_TOOLS_FUZZER_UTIL_H_
diff --git a/third_party/CMakeLists.txt b/third_party/CMakeLists.txt
index 734ebca..230d66e 100644
--- a/third_party/CMakeLists.txt
+++ b/third_party/CMakeLists.txt
@@ -17,6 +17,13 @@
   add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/googletest EXCLUDE_FROM_ALL)
 endif()
 
+if (${TINT_BUILD_SPIRV_TOOLS_FUZZER} AND (NOT TARGET protobuf::libprotobuf OR NOT TARGET protobuf::protoc))
+  set(SPIRV_BUILD_FUZZER ON CACHE BOOL "Build spirv-fuzz")
+  set(protobuf_BUILD_TESTS OFF CACHE BOOL "Disable protobuf tests")
+  set(protobuf_MSVC_STATIC_RUNTIME OFF CACHE BOOL "Do not build protobuf static runtime")
+  add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/protobuf/cmake)
+endif()
+
 if(${TINT_BUILD_SPV_READER} OR ${TINT_BUILD_SPV_WRITER})
   if (NOT IS_DIRECTORY "${SPIRV-Headers_SOURCE_DIR}")
     set(SPIRV-Headers_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/spirv-headers CACHE STRING "")