spirv-fuzz: Refactor fuzzer, replayer and shrinker (#3818)

In preparation for some upcoming work on the shrinker, this PR changes
the interfaces of Fuzzer, Replayer and Shrinker so that all data
relevant to each class is provided on construction, meaning that the
"Run" method can become a zero-argument method that returns a status,
transformed binary and sequence of applied transformations via a
struct.

This makes greater use of fields, so that -- especially in Fuzzer --
there is a lot less parameter passing.
diff --git a/source/fuzz/fuzzer.cpp b/source/fuzz/fuzzer.cpp
index 79228db..845c8c0 100644
--- a/source/fuzz/fuzzer.cpp
+++ b/source/fuzz/fuzzer.cpp
@@ -88,7 +88,6 @@
 #include "source/fuzz/pass_management/repeated_pass_manager_simple.h"
 #include "source/fuzz/pass_management/repeated_pass_recommender_standard.h"
 #include "source/fuzz/protobufs/spirvfuzz_protobufs.h"
-#include "source/fuzz/pseudo_random_generator.h"
 #include "source/fuzz/transformation_context.h"
 #include "source/opt/build_module.h"
 #include "source/spirv_fuzzer_options.h"
@@ -104,59 +103,59 @@
 
 }  // namespace
 
-Fuzzer::Fuzzer(spv_target_env target_env, uint32_t seed, bool enable_all_passes,
+Fuzzer::Fuzzer(spv_target_env target_env, MessageConsumer consumer,
+               const std::vector<uint32_t>& binary_in,
+               const protobufs::FactSequence& initial_facts,
+               const std::vector<fuzzerutil::ModuleSupplier>& donor_suppliers,
+               std::unique_ptr<RandomGenerator> random_generator,
+               bool enable_all_passes,
                RepeatedPassStrategy repeated_pass_strategy,
                bool validate_after_each_fuzzer_pass,
                spv_validator_options validator_options)
     : target_env_(target_env),
-      seed_(seed),
+      consumer_(std::move(consumer)),
+      binary_in_(binary_in),
+      initial_facts_(initial_facts),
+      donor_suppliers_(donor_suppliers),
+      random_generator_(std::move(random_generator)),
       enable_all_passes_(enable_all_passes),
       repeated_pass_strategy_(repeated_pass_strategy),
       validate_after_each_fuzzer_pass_(validate_after_each_fuzzer_pass),
       validator_options_(validator_options),
-      num_repeated_passes_applied_(0) {}
+      num_repeated_passes_applied_(0),
+      ir_context_(nullptr),
+      fuzzer_context_(nullptr),
+      transformation_context_(nullptr),
+      transformation_sequence_out_() {}
 
 Fuzzer::~Fuzzer() = default;
 
-void Fuzzer::SetMessageConsumer(MessageConsumer consumer) {
-  consumer_ = std::move(consumer);
-}
-
 template <typename FuzzerPassT, typename... Args>
-void Fuzzer::MaybeAddRepeatedPass(
-    RepeatedPassInstances* pass_instances, opt::IRContext* ir_context,
-    TransformationContext* transformation_context,
-    FuzzerContext* fuzzer_context,
-    protobufs::TransformationSequence* transformation_sequence_out,
-    Args&&... extra_args) const {
-  if (enable_all_passes_ || fuzzer_context->ChooseEven()) {
+void Fuzzer::MaybeAddRepeatedPass(RepeatedPassInstances* pass_instances,
+                                  Args&&... extra_args) {
+  if (enable_all_passes_ || fuzzer_context_->ChooseEven()) {
     pass_instances->SetPass(MakeUnique<FuzzerPassT>(
-        ir_context, transformation_context, fuzzer_context,
-        transformation_sequence_out, std::forward<Args>(extra_args)...));
+        ir_context_.get(), transformation_context_.get(), fuzzer_context_.get(),
+        &transformation_sequence_out_, std::forward<Args>(extra_args)...));
   }
 }
 
 template <typename FuzzerPassT, typename... Args>
-void Fuzzer::MaybeAddFinalPass(
-    std::vector<std::unique_ptr<FuzzerPass>>* passes,
-    opt::IRContext* ir_context, TransformationContext* transformation_context,
-    FuzzerContext* fuzzer_context,
-    protobufs::TransformationSequence* transformation_sequence_out,
-    Args&&... extra_args) const {
-  if (enable_all_passes_ || fuzzer_context->ChooseEven()) {
+void Fuzzer::MaybeAddFinalPass(std::vector<std::unique_ptr<FuzzerPass>>* passes,
+                               Args&&... extra_args) {
+  if (enable_all_passes_ || fuzzer_context_->ChooseEven()) {
     passes->push_back(MakeUnique<FuzzerPassT>(
-        ir_context, transformation_context, fuzzer_context,
-        transformation_sequence_out, std::forward<Args>(extra_args)...));
+        ir_context_.get(), transformation_context_.get(), fuzzer_context_.get(),
+        &transformation_sequence_out_, std::forward<Args>(extra_args)...));
   }
 }
 
 bool Fuzzer::ApplyPassAndCheckValidity(
-    FuzzerPass* pass, const opt::IRContext& ir_context,
-    const spvtools::SpirvTools& tools) const {
+    FuzzerPass* pass, const spvtools::SpirvTools& tools) const {
   pass->Apply();
   if (validate_after_each_fuzzer_pass_) {
     std::vector<uint32_t> binary_to_validate;
-    ir_context.module()->ToBinary(&binary_to_validate, false);
+    ir_context_->module()->ToBinary(&binary_to_validate, false);
     if (!tools.Validate(&binary_to_validate[0], binary_to_validate.size(),
                         validator_options_)) {
       consumer_(SPV_MSG_INFO, nullptr, {},
@@ -168,38 +167,37 @@
   return true;
 }
 
-Fuzzer::FuzzerResultStatus Fuzzer::Run(
-    const std::vector<uint32_t>& binary_in,
-    const protobufs::FactSequence& initial_facts,
-    const std::vector<fuzzerutil::ModuleSupplier>& donor_suppliers,
-    std::vector<uint32_t>* binary_out,
-    protobufs::TransformationSequence* transformation_sequence_out) {
+Fuzzer::FuzzerResult Fuzzer::Run() {
   // Check compatibility between the library version being linked with and the
   // header files being used.
   GOOGLE_PROTOBUF_VERIFY_VERSION;
 
+  assert(ir_context_ == nullptr && fuzzer_context_ == nullptr &&
+         transformation_context_ == nullptr &&
+         transformation_sequence_out_.transformation_size() == 0 &&
+         "'Run' must not be invoked more than once.");
+
   spvtools::SpirvTools tools(target_env_);
   tools.SetMessageConsumer(consumer_);
   if (!tools.IsValid()) {
     consumer_(SPV_MSG_ERROR, nullptr, {},
               "Failed to create SPIRV-Tools interface; stopping.");
-    return Fuzzer::FuzzerResultStatus::kFailedToCreateSpirvToolsInterface;
+    return {Fuzzer::FuzzerResultStatus::kFailedToCreateSpirvToolsInterface,
+            std::vector<uint32_t>(), protobufs::TransformationSequence()};
   }
 
   // Initial binary should be valid.
-  if (!tools.Validate(&binary_in[0], binary_in.size(), validator_options_)) {
+  if (!tools.Validate(&binary_in_[0], binary_in_.size(), validator_options_)) {
     consumer_(SPV_MSG_ERROR, nullptr, {},
               "Initial binary is invalid; stopping.");
-    return Fuzzer::FuzzerResultStatus::kInitialBinaryInvalid;
+    return {Fuzzer::FuzzerResultStatus::kInitialBinaryInvalid,
+            std::vector<uint32_t>(), protobufs::TransformationSequence()};
   }
 
   // Build the module from the input binary.
-  std::unique_ptr<opt::IRContext> ir_context =
-      BuildModule(target_env_, consumer_, binary_in.data(), binary_in.size());
-  assert(ir_context);
-
-  // Make a PRNG from the seed passed to the fuzzer on creation.
-  PseudoRandomGenerator random_generator(seed_);
+  ir_context_ =
+      BuildModule(target_env_, consumer_, binary_in_.data(), binary_in_.size());
+  assert(ir_context_);
 
   // The fuzzer will introduce new ids into the module.  The module's id bound
   // gives the smallest id that can be used for this purpose.  We add an offset
@@ -208,13 +206,14 @@
   //
   // TODO(https://github.com/KhronosGroup/SPIRV-Tools/issues/2541) consider the
   //  case where the maximum id bound is reached.
-  auto minimum_fresh_id = ir_context->module()->id_bound() + kIdBoundGap;
-  FuzzerContext fuzzer_context(&random_generator, minimum_fresh_id);
+  auto minimum_fresh_id = ir_context_->module()->id_bound() + kIdBoundGap;
+  fuzzer_context_ =
+      MakeUnique<FuzzerContext>(random_generator_.get(), minimum_fresh_id);
 
   FactManager fact_manager;
-  fact_manager.AddFacts(consumer_, initial_facts, ir_context.get());
-  TransformationContext transformation_context(&fact_manager,
-                                               validator_options_);
+  fact_manager.AddFacts(consumer_, initial_facts_, ir_context_.get());
+  transformation_context_ =
+      MakeUnique<TransformationContext>(&fact_manager, validator_options_);
 
   RepeatedPassInstances pass_instances{};
   do {
@@ -223,243 +222,133 @@
     // if it is enabled.
     // TODO(https://github.com/KhronosGroup/SPIRV-Tools/issues/3764): Consider
     //  enabling some passes always, or with higher probability.
-    MaybeAddRepeatedPass<FuzzerPassAddAccessChains>(
-        &pass_instances, ir_context.get(), &transformation_context,
-        &fuzzer_context, transformation_sequence_out);
-    MaybeAddRepeatedPass<FuzzerPassAddBitInstructionSynonyms>(
-        &pass_instances, ir_context.get(), &transformation_context,
-        &fuzzer_context, transformation_sequence_out);
-    MaybeAddRepeatedPass<FuzzerPassAddCompositeInserts>(
-        &pass_instances, ir_context.get(), &transformation_context,
-        &fuzzer_context, transformation_sequence_out);
-    MaybeAddRepeatedPass<FuzzerPassAddCompositeTypes>(
-        &pass_instances, ir_context.get(), &transformation_context,
-        &fuzzer_context, transformation_sequence_out);
-    MaybeAddRepeatedPass<FuzzerPassAddCopyMemory>(
-        &pass_instances, ir_context.get(), &transformation_context,
-        &fuzzer_context, transformation_sequence_out);
-    MaybeAddRepeatedPass<FuzzerPassAddDeadBlocks>(
-        &pass_instances, ir_context.get(), &transformation_context,
-        &fuzzer_context, transformation_sequence_out);
-    MaybeAddRepeatedPass<FuzzerPassAddDeadBreaks>(
-        &pass_instances, ir_context.get(), &transformation_context,
-        &fuzzer_context, transformation_sequence_out);
-    MaybeAddRepeatedPass<FuzzerPassAddDeadContinues>(
-        &pass_instances, ir_context.get(), &transformation_context,
-        &fuzzer_context, transformation_sequence_out);
-    MaybeAddRepeatedPass<FuzzerPassAddEquationInstructions>(
-        &pass_instances, ir_context.get(), &transformation_context,
-        &fuzzer_context, transformation_sequence_out);
-    MaybeAddRepeatedPass<FuzzerPassAddFunctionCalls>(
-        &pass_instances, ir_context.get(), &transformation_context,
-        &fuzzer_context, transformation_sequence_out);
-    MaybeAddRepeatedPass<FuzzerPassAddGlobalVariables>(
-        &pass_instances, ir_context.get(), &transformation_context,
-        &fuzzer_context, transformation_sequence_out);
+    MaybeAddRepeatedPass<FuzzerPassAddAccessChains>(&pass_instances);
+    MaybeAddRepeatedPass<FuzzerPassAddBitInstructionSynonyms>(&pass_instances);
+    MaybeAddRepeatedPass<FuzzerPassAddCompositeInserts>(&pass_instances);
+    MaybeAddRepeatedPass<FuzzerPassAddCompositeTypes>(&pass_instances);
+    MaybeAddRepeatedPass<FuzzerPassAddCopyMemory>(&pass_instances);
+    MaybeAddRepeatedPass<FuzzerPassAddDeadBlocks>(&pass_instances);
+    MaybeAddRepeatedPass<FuzzerPassAddDeadBreaks>(&pass_instances);
+    MaybeAddRepeatedPass<FuzzerPassAddDeadContinues>(&pass_instances);
+    MaybeAddRepeatedPass<FuzzerPassAddEquationInstructions>(&pass_instances);
+    MaybeAddRepeatedPass<FuzzerPassAddFunctionCalls>(&pass_instances);
+    MaybeAddRepeatedPass<FuzzerPassAddGlobalVariables>(&pass_instances);
     MaybeAddRepeatedPass<FuzzerPassAddImageSampleUnusedComponents>(
-        &pass_instances, ir_context.get(), &transformation_context,
-        &fuzzer_context, transformation_sequence_out);
-    MaybeAddRepeatedPass<FuzzerPassAddLoads>(
-        &pass_instances, ir_context.get(), &transformation_context,
-        &fuzzer_context, transformation_sequence_out);
-    MaybeAddRepeatedPass<FuzzerPassAddLocalVariables>(
-        &pass_instances, ir_context.get(), &transformation_context,
-        &fuzzer_context, transformation_sequence_out);
-    MaybeAddRepeatedPass<FuzzerPassAddLoopPreheaders>(
-        &pass_instances, ir_context.get(), &transformation_context,
-        &fuzzer_context, transformation_sequence_out);
-    MaybeAddRepeatedPass<FuzzerPassAddOpPhiSynonyms>(
-        &pass_instances, ir_context.get(), &transformation_context,
-        &fuzzer_context, transformation_sequence_out);
-    MaybeAddRepeatedPass<FuzzerPassAddParameters>(
-        &pass_instances, ir_context.get(), &transformation_context,
-        &fuzzer_context, transformation_sequence_out);
-    MaybeAddRepeatedPass<FuzzerPassAddRelaxedDecorations>(
-        &pass_instances, ir_context.get(), &transformation_context,
-        &fuzzer_context, transformation_sequence_out);
-    MaybeAddRepeatedPass<FuzzerPassAddStores>(
-        &pass_instances, ir_context.get(), &transformation_context,
-        &fuzzer_context, transformation_sequence_out);
-    MaybeAddRepeatedPass<FuzzerPassAddSynonyms>(
-        &pass_instances, ir_context.get(), &transformation_context,
-        &fuzzer_context, transformation_sequence_out);
+        &pass_instances);
+    MaybeAddRepeatedPass<FuzzerPassAddLoads>(&pass_instances);
+    MaybeAddRepeatedPass<FuzzerPassAddLocalVariables>(&pass_instances);
+    MaybeAddRepeatedPass<FuzzerPassAddLoopPreheaders>(&pass_instances);
+    MaybeAddRepeatedPass<FuzzerPassAddOpPhiSynonyms>(&pass_instances);
+    MaybeAddRepeatedPass<FuzzerPassAddParameters>(&pass_instances);
+    MaybeAddRepeatedPass<FuzzerPassAddRelaxedDecorations>(&pass_instances);
+    MaybeAddRepeatedPass<FuzzerPassAddStores>(&pass_instances);
+    MaybeAddRepeatedPass<FuzzerPassAddSynonyms>(&pass_instances);
     MaybeAddRepeatedPass<FuzzerPassAddVectorShuffleInstructions>(
-        &pass_instances, ir_context.get(), &transformation_context,
-        &fuzzer_context, transformation_sequence_out);
-    MaybeAddRepeatedPass<FuzzerPassApplyIdSynonyms>(
-        &pass_instances, ir_context.get(), &transformation_context,
-        &fuzzer_context, transformation_sequence_out);
-    MaybeAddRepeatedPass<FuzzerPassConstructComposites>(
-        &pass_instances, ir_context.get(), &transformation_context,
-        &fuzzer_context, transformation_sequence_out);
-    MaybeAddRepeatedPass<FuzzerPassCopyObjects>(
-        &pass_instances, ir_context.get(), &transformation_context,
-        &fuzzer_context, transformation_sequence_out);
-    MaybeAddRepeatedPass<FuzzerPassDonateModules>(
-        &pass_instances, ir_context.get(), &transformation_context,
-        &fuzzer_context, transformation_sequence_out, donor_suppliers);
+        &pass_instances);
+    MaybeAddRepeatedPass<FuzzerPassApplyIdSynonyms>(&pass_instances);
+    MaybeAddRepeatedPass<FuzzerPassConstructComposites>(&pass_instances);
+    MaybeAddRepeatedPass<FuzzerPassCopyObjects>(&pass_instances);
+    MaybeAddRepeatedPass<FuzzerPassDonateModules>(&pass_instances,
+                                                  donor_suppliers_);
     MaybeAddRepeatedPass<FuzzerPassDuplicateRegionsWithSelections>(
-        &pass_instances, ir_context.get(), &transformation_context,
-        &fuzzer_context, transformation_sequence_out);
-    MaybeAddRepeatedPass<FuzzerPassFlattenConditionalBranches>(
-        &pass_instances, ir_context.get(), &transformation_context,
-        &fuzzer_context, transformation_sequence_out);
-    MaybeAddRepeatedPass<FuzzerPassInlineFunctions>(
-        &pass_instances, ir_context.get(), &transformation_context,
-        &fuzzer_context, transformation_sequence_out);
-    MaybeAddRepeatedPass<FuzzerPassInvertComparisonOperators>(
-        &pass_instances, ir_context.get(), &transformation_context,
-        &fuzzer_context, transformation_sequence_out);
+        &pass_instances);
+    MaybeAddRepeatedPass<FuzzerPassFlattenConditionalBranches>(&pass_instances);
+    MaybeAddRepeatedPass<FuzzerPassInlineFunctions>(&pass_instances);
+    MaybeAddRepeatedPass<FuzzerPassInvertComparisonOperators>(&pass_instances);
     MaybeAddRepeatedPass<FuzzerPassMakeVectorOperationsDynamic>(
-        &pass_instances, ir_context.get(), &transformation_context,
-        &fuzzer_context, transformation_sequence_out);
-    MaybeAddRepeatedPass<FuzzerPassMergeBlocks>(
-        &pass_instances, ir_context.get(), &transformation_context,
-        &fuzzer_context, transformation_sequence_out);
-    MaybeAddRepeatedPass<FuzzerPassMutatePointers>(
-        &pass_instances, ir_context.get(), &transformation_context,
-        &fuzzer_context, transformation_sequence_out);
-    MaybeAddRepeatedPass<FuzzerPassObfuscateConstants>(
-        &pass_instances, ir_context.get(), &transformation_context,
-        &fuzzer_context, transformation_sequence_out);
-    MaybeAddRepeatedPass<FuzzerPassOutlineFunctions>(
-        &pass_instances, ir_context.get(), &transformation_context,
-        &fuzzer_context, transformation_sequence_out);
-    MaybeAddRepeatedPass<FuzzerPassPermuteBlocks>(
-        &pass_instances, ir_context.get(), &transformation_context,
-        &fuzzer_context, transformation_sequence_out);
-    MaybeAddRepeatedPass<FuzzerPassPermuteFunctionParameters>(
-        &pass_instances, ir_context.get(), &transformation_context,
-        &fuzzer_context, transformation_sequence_out);
-    MaybeAddRepeatedPass<FuzzerPassPermuteInstructions>(
-        &pass_instances, ir_context.get(), &transformation_context,
-        &fuzzer_context, transformation_sequence_out);
-    MaybeAddRepeatedPass<FuzzerPassPropagateInstructionsUp>(
-        &pass_instances, ir_context.get(), &transformation_context,
-        &fuzzer_context, transformation_sequence_out);
-    MaybeAddRepeatedPass<FuzzerPassPushIdsThroughVariables>(
-        &pass_instances, ir_context.get(), &transformation_context,
-        &fuzzer_context, transformation_sequence_out);
+        &pass_instances);
+    MaybeAddRepeatedPass<FuzzerPassMergeBlocks>(&pass_instances);
+    MaybeAddRepeatedPass<FuzzerPassMutatePointers>(&pass_instances);
+    MaybeAddRepeatedPass<FuzzerPassObfuscateConstants>(&pass_instances);
+    MaybeAddRepeatedPass<FuzzerPassOutlineFunctions>(&pass_instances);
+    MaybeAddRepeatedPass<FuzzerPassPermuteBlocks>(&pass_instances);
+    MaybeAddRepeatedPass<FuzzerPassPermuteFunctionParameters>(&pass_instances);
+    MaybeAddRepeatedPass<FuzzerPassPermuteInstructions>(&pass_instances);
+    MaybeAddRepeatedPass<FuzzerPassPropagateInstructionsUp>(&pass_instances);
+    MaybeAddRepeatedPass<FuzzerPassPushIdsThroughVariables>(&pass_instances);
     MaybeAddRepeatedPass<FuzzerPassReplaceAddsSubsMulsWithCarryingExtended>(
-        &pass_instances, ir_context.get(), &transformation_context,
-        &fuzzer_context, transformation_sequence_out);
+        &pass_instances);
     MaybeAddRepeatedPass<FuzzerPassReplaceCopyMemoriesWithLoadsStores>(
-        &pass_instances, ir_context.get(), &transformation_context,
-        &fuzzer_context, transformation_sequence_out);
+        &pass_instances);
     MaybeAddRepeatedPass<FuzzerPassReplaceCopyObjectsWithStoresLoads>(
-        &pass_instances, ir_context.get(), &transformation_context,
-        &fuzzer_context, transformation_sequence_out);
+        &pass_instances);
     MaybeAddRepeatedPass<FuzzerPassReplaceLoadsStoresWithCopyMemories>(
-        &pass_instances, ir_context.get(), &transformation_context,
-        &fuzzer_context, transformation_sequence_out);
-    MaybeAddRepeatedPass<FuzzerPassReplaceParameterWithGlobal>(
-        &pass_instances, ir_context.get(), &transformation_context,
-        &fuzzer_context, transformation_sequence_out);
+        &pass_instances);
+    MaybeAddRepeatedPass<FuzzerPassReplaceParameterWithGlobal>(&pass_instances);
     MaybeAddRepeatedPass<FuzzerPassReplaceLinearAlgebraInstructions>(
-        &pass_instances, ir_context.get(), &transformation_context,
-        &fuzzer_context, transformation_sequence_out);
-    MaybeAddRepeatedPass<FuzzerPassReplaceIrrelevantIds>(
-        &pass_instances, ir_context.get(), &transformation_context,
-        &fuzzer_context, transformation_sequence_out);
+        &pass_instances);
+    MaybeAddRepeatedPass<FuzzerPassReplaceIrrelevantIds>(&pass_instances);
     MaybeAddRepeatedPass<FuzzerPassReplaceOpPhiIdsFromDeadPredecessors>(
-        &pass_instances, ir_context.get(), &transformation_context,
-        &fuzzer_context, transformation_sequence_out);
+        &pass_instances);
     MaybeAddRepeatedPass<FuzzerPassReplaceOpSelectsWithConditionalBranches>(
-        &pass_instances, ir_context.get(), &transformation_context,
-        &fuzzer_context, transformation_sequence_out);
-    MaybeAddRepeatedPass<FuzzerPassReplaceParamsWithStruct>(
-        &pass_instances, ir_context.get(), &transformation_context,
-        &fuzzer_context, transformation_sequence_out);
-    MaybeAddRepeatedPass<FuzzerPassSplitBlocks>(
-        &pass_instances, ir_context.get(), &transformation_context,
-        &fuzzer_context, transformation_sequence_out);
+        &pass_instances);
+    MaybeAddRepeatedPass<FuzzerPassReplaceParamsWithStruct>(&pass_instances);
+    MaybeAddRepeatedPass<FuzzerPassSplitBlocks>(&pass_instances);
     MaybeAddRepeatedPass<FuzzerPassSwapBranchConditionalOperands>(
-        &pass_instances, ir_context.get(), &transformation_context,
-        &fuzzer_context, transformation_sequence_out);
+        &pass_instances);
     // There is a theoretical possibility that no pass instances were created
     // until now; loop again if so.
   } while (pass_instances.GetPasses().empty());
 
   RepeatedPassRecommenderStandard pass_recommender(&pass_instances,
-                                                   &fuzzer_context);
+                                                   fuzzer_context_.get());
 
   std::unique_ptr<RepeatedPassManager> repeated_pass_manager = nullptr;
   switch (repeated_pass_strategy_) {
     case RepeatedPassStrategy::kSimple:
       repeated_pass_manager = MakeUnique<RepeatedPassManagerSimple>(
-          &fuzzer_context, &pass_instances);
+          fuzzer_context_.get(), &pass_instances);
       break;
     case RepeatedPassStrategy::kLoopedWithRecommendations:
       repeated_pass_manager =
           MakeUnique<RepeatedPassManagerLoopedWithRecommendations>(
-              &fuzzer_context, &pass_instances, &pass_recommender);
+              fuzzer_context_.get(), &pass_instances, &pass_recommender);
       break;
     case RepeatedPassStrategy::kRandomWithRecommendations:
       repeated_pass_manager =
           MakeUnique<RepeatedPassManagerRandomWithRecommendations>(
-              &fuzzer_context, &pass_instances, &pass_recommender);
+              fuzzer_context_.get(), &pass_instances, &pass_recommender);
       break;
   }
 
   do {
     if (!ApplyPassAndCheckValidity(repeated_pass_manager->ChoosePass(),
-                                   *ir_context, tools)) {
-      return Fuzzer::FuzzerResultStatus::kFuzzerPassLedToInvalidModule;
+                                   tools)) {
+      return {Fuzzer::FuzzerResultStatus::kFuzzerPassLedToInvalidModule,
+              std::vector<uint32_t>(), protobufs::TransformationSequence()};
     }
-  } while (
-      ShouldContinueFuzzing(*transformation_sequence_out, &fuzzer_context));
+  } while (ShouldContinueFuzzing());
 
   // Now apply some passes that it does not make sense to apply repeatedly,
   // as they do not unlock other passes.
   std::vector<std::unique_ptr<FuzzerPass>> final_passes;
-  MaybeAddFinalPass<FuzzerPassAdjustBranchWeights>(
-      &final_passes, ir_context.get(), &transformation_context, &fuzzer_context,
-      transformation_sequence_out);
-  MaybeAddFinalPass<FuzzerPassAdjustFunctionControls>(
-      &final_passes, ir_context.get(), &transformation_context, &fuzzer_context,
-      transformation_sequence_out);
-  MaybeAddFinalPass<FuzzerPassAdjustLoopControls>(
-      &final_passes, ir_context.get(), &transformation_context, &fuzzer_context,
-      transformation_sequence_out);
-  MaybeAddFinalPass<FuzzerPassAdjustMemoryOperandsMasks>(
-      &final_passes, ir_context.get(), &transformation_context, &fuzzer_context,
-      transformation_sequence_out);
-  MaybeAddFinalPass<FuzzerPassAdjustSelectionControls>(
-      &final_passes, ir_context.get(), &transformation_context, &fuzzer_context,
-      transformation_sequence_out);
-  MaybeAddFinalPass<FuzzerPassAddNoContractionDecorations>(
-      &final_passes, ir_context.get(), &transformation_context, &fuzzer_context,
-      transformation_sequence_out);
+  MaybeAddFinalPass<FuzzerPassAdjustBranchWeights>(&final_passes);
+  MaybeAddFinalPass<FuzzerPassAdjustFunctionControls>(&final_passes);
+  MaybeAddFinalPass<FuzzerPassAdjustLoopControls>(&final_passes);
+  MaybeAddFinalPass<FuzzerPassAdjustMemoryOperandsMasks>(&final_passes);
+  MaybeAddFinalPass<FuzzerPassAdjustSelectionControls>(&final_passes);
+  MaybeAddFinalPass<FuzzerPassAddNoContractionDecorations>(&final_passes);
   MaybeAddFinalPass<FuzzerPassInterchangeSignednessOfIntegerOperands>(
-      &final_passes, ir_context.get(), &transformation_context, &fuzzer_context,
-      transformation_sequence_out);
-  MaybeAddFinalPass<FuzzerPassInterchangeZeroLikeConstants>(
-      &final_passes, ir_context.get(), &transformation_context, &fuzzer_context,
-      transformation_sequence_out);
-  MaybeAddFinalPass<FuzzerPassPermutePhiOperands>(
-      &final_passes, ir_context.get(), &transformation_context, &fuzzer_context,
-      transformation_sequence_out);
-  MaybeAddFinalPass<FuzzerPassSwapCommutableOperands>(
-      &final_passes, ir_context.get(), &transformation_context, &fuzzer_context,
-      transformation_sequence_out);
-  MaybeAddFinalPass<FuzzerPassToggleAccessChainInstruction>(
-      &final_passes, ir_context.get(), &transformation_context, &fuzzer_context,
-      transformation_sequence_out);
+      &final_passes);
+  MaybeAddFinalPass<FuzzerPassInterchangeZeroLikeConstants>(&final_passes);
+  MaybeAddFinalPass<FuzzerPassPermutePhiOperands>(&final_passes);
+  MaybeAddFinalPass<FuzzerPassSwapCommutableOperands>(&final_passes);
+  MaybeAddFinalPass<FuzzerPassToggleAccessChainInstruction>(&final_passes);
   for (auto& pass : final_passes) {
-    if (!ApplyPassAndCheckValidity(pass.get(), *ir_context, tools)) {
-      return Fuzzer::FuzzerResultStatus::kFuzzerPassLedToInvalidModule;
+    if (!ApplyPassAndCheckValidity(pass.get(), tools)) {
+      return {Fuzzer::FuzzerResultStatus::kFuzzerPassLedToInvalidModule,
+              std::vector<uint32_t>(), protobufs::TransformationSequence()};
     }
   }
-
   // Encode the module as a binary.
-  ir_context->module()->ToBinary(binary_out, false);
+  std::vector<uint32_t> binary_out;
+  ir_context_->module()->ToBinary(&binary_out, false);
 
-  return Fuzzer::FuzzerResultStatus::kComplete;
+  return {Fuzzer::FuzzerResultStatus::kComplete, std::move(binary_out),
+          std::move(transformation_sequence_out_)};
 }
 
-bool Fuzzer::ShouldContinueFuzzing(
-    const protobufs::TransformationSequence& transformation_sequence_out,
-    FuzzerContext* fuzzer_context) {
+bool Fuzzer::ShouldContinueFuzzing() {
   // There's a risk that fuzzing could get stuck, if none of the enabled fuzzer
   // passes are able to apply any transformations.  To guard against this we
   // count the number of times some repeated pass has been applied and ensure
@@ -473,7 +362,7 @@
     return false;
   }
   auto transformations_applied_so_far =
-      static_cast<uint32_t>(transformation_sequence_out.transformation_size());
+      static_cast<uint32_t>(transformation_sequence_out_.transformation_size());
   if (transformations_applied_so_far >= kTransformationLimit) {
     // Stop because we have reached the transformation limit.
     return false;
@@ -481,7 +370,7 @@
   auto chance_of_continuing = static_cast<uint32_t>(
       100.0 * (1.0 - (static_cast<double>(transformations_applied_so_far) /
                       static_cast<double>(kTransformationLimit))));
-  if (!fuzzer_context->ChoosePercentage(chance_of_continuing)) {
+  if (!fuzzer_context_->ChoosePercentage(chance_of_continuing)) {
     // We have probabilistically decided to stop.
     return false;
   }
diff --git a/source/fuzz/fuzzer.h b/source/fuzz/fuzzer.h
index 01b2c68..379c041 100644
--- a/source/fuzz/fuzzer.h
+++ b/source/fuzz/fuzzer.h
@@ -24,6 +24,8 @@
 #include "source/fuzz/pass_management/repeated_pass_instances.h"
 #include "source/fuzz/pass_management/repeated_pass_recommender.h"
 #include "source/fuzz/protobufs/spirvfuzz_protobufs.h"
+#include "source/fuzz/random_generator.h"
+#include "source/opt/ir_context.h"
 #include "spirv-tools/libspirv.hpp"
 
 namespace spvtools {
@@ -41,6 +43,12 @@
     kInitialBinaryInvalid,
   };
 
+  struct FuzzerResult {
+    FuzzerResultStatus status;
+    std::vector<uint32_t> transformed_binary;
+    protobufs::TransformationSequence applied_transformations;
+  };
+
   // Each field of this enum corresponds to an available repeated pass
   // strategy, and is used to decide which kind of RepeatedPassManager object
   // to create.
@@ -50,15 +58,12 @@
     kLoopedWithRecommendations
   };
 
-  // Constructs a fuzzer from the given target environment |target_env|.  |seed|
-  // is a seed for pseudo-random number generation.  If |enable_all_passes| is
-  // true then all fuzzer passes will be enabled, otherwise a random subset of
-  // fuzzer passes will be enabled.  |validate_after_each_fuzzer_pass| controls
-  // whether the validator will be invoked after every fuzzer pass is applied,
-  // and |validator_options| provides the options that should be used during
-  // validation if so.
-  Fuzzer(spv_target_env target_env, uint32_t seed, bool enable_all_passes,
-         RepeatedPassStrategy repeated_pass_strategy,
+  Fuzzer(spv_target_env target_env, MessageConsumer consumer,
+         const std::vector<uint32_t>& binary_in,
+         const protobufs::FactSequence& initial_facts,
+         const std::vector<fuzzerutil::ModuleSupplier>& donor_suppliers,
+         std::unique_ptr<RandomGenerator> random_generator,
+         bool enable_all_passes, RepeatedPassStrategy repeated_pass_strategy,
          bool validate_after_each_fuzzer_pass,
          spv_validator_options validator_options);
 
@@ -70,73 +75,66 @@
 
   ~Fuzzer();
 
-  // Sets the message consumer to the given |consumer|. The |consumer| will be
-  // invoked once for each message communicated from the library.
-  void SetMessageConsumer(MessageConsumer consumer);
-
-  // Transforms |binary_in| to |binary_out| by running a number of randomized
-  // fuzzer passes.  Initial facts about the input binary and the context in
-  // which it will execute are provided via |initial_facts|.  A source of donor
-  // modules to be used by transformations is provided via |donor_suppliers|.
-  // The transformation sequence that was applied is returned via
-  // |transformation_sequence_out|.
-  FuzzerResultStatus Run(
-      const std::vector<uint32_t>& binary_in,
-      const protobufs::FactSequence& initial_facts,
-      const std::vector<fuzzerutil::ModuleSupplier>& donor_suppliers,
-      std::vector<uint32_t>* binary_out,
-      protobufs::TransformationSequence* transformation_sequence_out);
+  // Transforms |binary_in_| by running a number of randomized fuzzer passes.
+  // Initial facts about the input binary and the context in which it will
+  // execute are provided via |initial_facts_|.  A source of donor modules to be
+  // used by transformations is provided via |donor_suppliers_|.  On success,
+  // returns a successful result status together with the transformed binary and
+  // the sequence of transformations that were applied.  Otherwise, returns an
+  // appropriate result status together with an empty binary and empty
+  // transformation sequence.
+  FuzzerResult Run();
 
  private:
   // A convenience method to add a repeated fuzzer pass to |pass_instances| with
   // probability 0.5, or with probability 1 if |enable_all_passes_| is true.
   //
-  // All fuzzer passes take |ir_context|, |transformation_context|,
-  // |fuzzer_context| and |transformation_sequence_out| as parameters.  Extra
+  // All fuzzer passes take members |ir_context_|, |transformation_context_|,
+  // |fuzzer_context_| and |transformation_sequence_out_| as parameters.  Extra
   // arguments can be provided via |extra_args|.
   template <typename FuzzerPassT, typename... Args>
-  void MaybeAddRepeatedPass(
-      RepeatedPassInstances* pass_instances, opt::IRContext* ir_context,
-      TransformationContext* transformation_context,
-      FuzzerContext* fuzzer_context,
-      protobufs::TransformationSequence* transformation_sequence_out,
-      Args&&... extra_args) const;
+  void MaybeAddRepeatedPass(RepeatedPassInstances* pass_instances,
+                            Args&&... extra_args);
 
   // A convenience method to add a final fuzzer pass to |passes| with
   // probability 0.5, or with probability 1 if |enable_all_passes_| is true.
   //
-  // All fuzzer passes take |ir_context|, |transformation_context|,
-  // |fuzzer_context| and |transformation_sequence_out| as parameters.  Extra
+  // All fuzzer passes take members |ir_context_|, |transformation_context_|,
+  // |fuzzer_context_| and |transformation_sequence_out_| as parameters.  Extra
   // arguments can be provided via |extra_args|.
   template <typename FuzzerPassT, typename... Args>
-  void MaybeAddFinalPass(
-      std::vector<std::unique_ptr<FuzzerPass>>* passes,
-      opt::IRContext* ir_context, TransformationContext* transformation_context,
-      FuzzerContext* fuzzer_context,
-      protobufs::TransformationSequence* transformation_sequence_out,
-      Args&&... extra_args) const;
+  void MaybeAddFinalPass(std::vector<std::unique_ptr<FuzzerPass>>* passes,
+                         Args&&... extra_args);
 
   // Decides whether to apply more repeated passes. The probability decreases as
   // the number of transformations that have been applied increases.
-  bool ShouldContinueFuzzing(
-      const protobufs::TransformationSequence& transformation_sequence_out,
-      FuzzerContext* fuzzer_context);
+  bool ShouldContinueFuzzing();
 
   // Applies |pass|, which must be a pass constructed with |ir_context|, and
   // then returns true if and only if |ir_context| is valid.  |tools| is used to
   // check validity.
   bool ApplyPassAndCheckValidity(FuzzerPass* pass,
-                                 const opt::IRContext& ir_context,
                                  const spvtools::SpirvTools& tools) const;
 
   // Target environment.
   const spv_target_env target_env_;
 
-  // Message consumer.
+  // Message consumer that will be invoked once for each message communicated
+  // from the library.
   MessageConsumer consumer_;
 
-  // Seed for random number generator.
-  const uint32_t seed_;
+  // The initial binary to which fuzzing should be applied.
+  const std::vector<uint32_t>& binary_in_;
+
+  // Initial facts known to hold in advance of applying any transformations.
+  const protobufs::FactSequence& initial_facts_;
+
+  // A source of modules whose contents can be donated into the module being
+  // fuzzed.
+  const std::vector<fuzzerutil::ModuleSupplier>& donor_suppliers_;
+
+  // Random number generator to control decision making during fuzzing.
+  std::unique_ptr<RandomGenerator> random_generator_;
 
   // Determines whether all passes should be enabled, vs. having passes be
   // probabilistically enabled.
@@ -155,6 +153,20 @@
   // of, in order to enforce a hard limit on the number of times such passes
   // can be applied.
   uint32_t num_repeated_passes_applied_;
+
+  // Intermediate representation for the module being fuzzed, which gets
+  // mutated as fuzzing proceeds.
+  std::unique_ptr<opt::IRContext> ir_context_;
+
+  // Provides probabilities that control the fuzzing process.
+  std::unique_ptr<FuzzerContext> fuzzer_context_;
+
+  // Contextual information that is required in order to apply transformations.
+  std::unique_ptr<TransformationContext> transformation_context_;
+
+  // The sequence of transformations that have been applied during fuzzing.  It
+  // is initially empty and grows as fuzzer passes are applied.
+  protobufs::TransformationSequence transformation_sequence_out_;
 };
 
 }  // namespace fuzz
diff --git a/source/fuzz/replayer.cpp b/source/fuzz/replayer.cpp
index d439b9d..df2b848 100644
--- a/source/fuzz/replayer.cpp
+++ b/source/fuzz/replayer.cpp
@@ -28,81 +28,88 @@
 namespace spvtools {
 namespace fuzz {
 
-Replayer::Replayer(spv_target_env target_env, bool validate_during_replay,
-                   spv_validator_options validator_options)
+Replayer::Replayer(
+    spv_target_env target_env, MessageConsumer consumer,
+    const std::vector<uint32_t>& binary_in,
+    const protobufs::FactSequence& initial_facts,
+    const protobufs::TransformationSequence& transformation_sequence_in,
+    uint32_t num_transformations_to_apply, uint32_t first_overflow_id,
+    bool validate_during_replay, spv_validator_options validator_options)
     : target_env_(target_env),
+      consumer_(std::move(consumer)),
+      binary_in_(binary_in),
+      initial_facts_(initial_facts),
+      transformation_sequence_in_(transformation_sequence_in),
+      num_transformations_to_apply_(num_transformations_to_apply),
+      first_overflow_id_(first_overflow_id),
       validate_during_replay_(validate_during_replay),
       validator_options_(validator_options) {}
 
 Replayer::~Replayer() = default;
 
-void Replayer::SetMessageConsumer(MessageConsumer consumer) {
-  consumer_ = std::move(consumer);
-}
-
-Replayer::ReplayerResultStatus Replayer::Run(
-    const std::vector<uint32_t>& binary_in,
-    const protobufs::FactSequence& initial_facts,
-    const protobufs::TransformationSequence& transformation_sequence_in,
-    uint32_t num_transformations_to_apply, uint32_t first_overflow_id,
-    std::vector<uint32_t>* binary_out,
-    protobufs::TransformationSequence* transformation_sequence_out) const {
+Replayer::ReplayerResult Replayer::Run() {
   // Check compatibility between the library version being linked with and the
   // header files being used.
   GOOGLE_PROTOBUF_VERIFY_VERSION;
 
-  if (num_transformations_to_apply >
-      static_cast<uint32_t>(transformation_sequence_in.transformation_size())) {
+  if (num_transformations_to_apply_ >
+      static_cast<uint32_t>(
+          transformation_sequence_in_.transformation_size())) {
     consumer_(SPV_MSG_ERROR, nullptr, {},
               "The number of transformations to be replayed must not "
               "exceed the size of the transformation sequence.");
-    return Replayer::ReplayerResultStatus::kTooManyTransformationsRequested;
+    return {Replayer::ReplayerResultStatus::kTooManyTransformationsRequested,
+            std::vector<uint32_t>(), protobufs::TransformationSequence()};
   }
 
   spvtools::SpirvTools tools(target_env_);
   if (!tools.IsValid()) {
     consumer_(SPV_MSG_ERROR, nullptr, {},
               "Failed to create SPIRV-Tools interface; stopping.");
-    return Replayer::ReplayerResultStatus::kFailedToCreateSpirvToolsInterface;
+    return {Replayer::ReplayerResultStatus::kFailedToCreateSpirvToolsInterface,
+            std::vector<uint32_t>(), protobufs::TransformationSequence()};
   }
 
   // Initial binary should be valid.
-  if (!tools.Validate(&binary_in[0], binary_in.size(), validator_options_)) {
+  if (!tools.Validate(&binary_in_[0], binary_in_.size(), validator_options_)) {
     consumer_(SPV_MSG_INFO, nullptr, {},
               "Initial binary is invalid; stopping.");
-    return Replayer::ReplayerResultStatus::kInitialBinaryInvalid;
+    return {Replayer::ReplayerResultStatus::kInitialBinaryInvalid,
+            std::vector<uint32_t>(), protobufs::TransformationSequence()};
   }
 
   // Build the module from the input binary.
   std::unique_ptr<opt::IRContext> ir_context =
-      BuildModule(target_env_, consumer_, binary_in.data(), binary_in.size());
+      BuildModule(target_env_, consumer_, binary_in_.data(), binary_in_.size());
   assert(ir_context);
 
   // For replay validation, we track the last valid SPIR-V binary that was
   // observed. Initially this is the input binary.
   std::vector<uint32_t> last_valid_binary;
   if (validate_during_replay_) {
-    last_valid_binary = binary_in;
+    last_valid_binary = binary_in_;
   }
 
   FactManager fact_manager;
-  fact_manager.AddFacts(consumer_, initial_facts, ir_context.get());
+  fact_manager.AddFacts(consumer_, initial_facts_, ir_context.get());
   std::unique_ptr<TransformationContext> transformation_context =
-      first_overflow_id == 0
+      first_overflow_id_ == 0
           ? MakeUnique<TransformationContext>(&fact_manager, validator_options_)
           : MakeUnique<TransformationContext>(
                 &fact_manager, validator_options_,
-                MakeUnique<CounterOverflowIdSource>(first_overflow_id));
+                MakeUnique<CounterOverflowIdSource>(first_overflow_id_));
 
   // We track the largest id bound observed, to ensure that it only increases
   // as transformations are applied.
   uint32_t max_observed_id_bound = ir_context->module()->id_bound();
   (void)(max_observed_id_bound);  // Keep release-mode compilers happy.
 
+  protobufs::TransformationSequence transformation_sequence_out;
+
   // Consider the transformation proto messages in turn.
   uint32_t counter = 0;
-  for (auto& message : transformation_sequence_in.transformation()) {
-    if (counter >= num_transformations_to_apply) {
+  for (auto& message : transformation_sequence_in_.transformation()) {
+    if (counter >= num_transformations_to_apply_) {
       break;
     }
     counter++;
@@ -115,7 +122,7 @@
       // The transformation is applicable, so apply it, and copy it to the
       // sequence of transformations that were applied.
       transformation->Apply(ir_context.get(), transformation_context.get());
-      *transformation_sequence_out->add_transformation() = message;
+      *transformation_sequence_out.add_transformation() = message;
 
       assert(ir_context->module()->id_bound() >= max_observed_id_bound &&
              "The module's id bound should only increase due to applying "
@@ -132,7 +139,8 @@
           consumer_(SPV_MSG_INFO, nullptr, {},
                     "Binary became invalid during replay (set a "
                     "breakpoint to inspect); stopping.");
-          return Replayer::ReplayerResultStatus::kReplayValidationFailure;
+          return {Replayer::ReplayerResultStatus::kReplayValidationFailure,
+                  std::vector<uint32_t>(), protobufs::TransformationSequence()};
         }
 
         // The binary was valid, so it becomes the latest valid binary.
@@ -142,8 +150,10 @@
   }
 
   // Write out the module as a binary.
-  ir_context->module()->ToBinary(binary_out, false);
-  return Replayer::ReplayerResultStatus::kComplete;
+  std::vector<uint32_t> binary_out;
+  ir_context->module()->ToBinary(&binary_out, false);
+  return {Replayer::ReplayerResultStatus::kComplete, std::move(binary_out),
+          std::move(transformation_sequence_out)};
 }
 
 }  // namespace fuzz
diff --git a/source/fuzz/replayer.h b/source/fuzz/replayer.h
index a10e536..5bc62d9 100644
--- a/source/fuzz/replayer.h
+++ b/source/fuzz/replayer.h
@@ -29,7 +29,7 @@
 class Replayer {
  public:
   // Possible statuses that can result from running the replayer.
-  enum ReplayerResultStatus {
+  enum class ReplayerResultStatus {
     kComplete,
     kFailedToCreateSpirvToolsInterface,
     kInitialBinaryInvalid,
@@ -37,8 +37,18 @@
     kTooManyTransformationsRequested,
   };
 
-  // Constructs a replayer from the given target environment.
-  Replayer(spv_target_env target_env, bool validate_during_replay,
+  struct ReplayerResult {
+    ReplayerResultStatus status;
+    std::vector<uint32_t> transformed_binary;
+    protobufs::TransformationSequence applied_transformations;
+  };
+
+  Replayer(spv_target_env target_env, MessageConsumer consumer,
+           const std::vector<uint32_t>& binary_in,
+           const protobufs::FactSequence& initial_facts,
+           const protobufs::TransformationSequence& transformation_sequence_in,
+           uint32_t num_transformations_to_apply, uint32_t first_overflow_id,
+           bool validate_during_replay,
            spv_validator_options validator_options);
 
   // Disables copy/move constructor/assignment operations.
@@ -49,31 +59,21 @@
 
   ~Replayer();
 
-  // Sets the message consumer to the given |consumer|. The |consumer| will be
-  // invoked once for each message communicated from the library.
-  void SetMessageConsumer(MessageConsumer consumer);
-
-  // Transforms |binary_in| to |binary_out| by attempting to apply the first
-  // |num_transformations_to_apply| transformations from
-  // |transformation_sequence_in|.
+  // Attempts to apply the first |num_transformations_to_apply_| transformations
+  // from |transformation_sequence_in_| to |binary_in_|.  Initial facts about
+  // the input binary and the context in which it will execute are provided via
+  // |initial_facts_|.
   //
-  // Initial facts about the input binary and the context in which it will
-  // execute are provided via |initial_facts|.
-  //
-  // |first_overflow_id| should be set to 0 if overflow ids are not available
-  // during replay.  Otherwise |first_overflow_id| must be larger than any id
-  // referred to in |binary_in| or |transformation_sequence_in|, and overflow
+  // |first_overflow_id_| should be set to 0 if overflow ids are not available
+  // during replay.  Otherwise |first_overflow_id_| must be larger than any id
+  // referred to in |binary_in_| or |transformation_sequence_in_|, and overflow
   // ids will be available during replay starting from this value.
   //
-  // The transformations that were successfully applied are returned via
-  // |transformation_sequence_out|.
-  ReplayerResultStatus Run(
-      const std::vector<uint32_t>& binary_in,
-      const protobufs::FactSequence& initial_facts,
-      const protobufs::TransformationSequence& transformation_sequence_in,
-      uint32_t num_transformations_to_apply, uint32_t first_overflow_id,
-      std::vector<uint32_t>* binary_out,
-      protobufs::TransformationSequence* transformation_sequence_out) const;
+  // On success, returns a successful result status together with the
+  // transformations that were successfully applied and the binary resulting
+  // from applying them.  Otherwise, returns an appropriate result status
+  // together with an empty binary and empty transformation sequence.
+  ReplayerResult Run();
 
  private:
   // Target environment.
@@ -82,6 +82,22 @@
   // Message consumer.
   MessageConsumer consumer_;
 
+  // The binary to which transformations are to be applied.
+  const std::vector<uint32_t>& binary_in_;
+
+  // Initial facts known to hold in advance of applying any transformations.
+  const protobufs::FactSequence& initial_facts_;
+
+  // The transformations to be replayed.
+  const protobufs::TransformationSequence& transformation_sequence_in_;
+
+  // The number of transformations that should be replayed.
+  const uint32_t num_transformations_to_apply_;
+
+  // Zero if overflow ids are not available, otherwise hold the value of the
+  // smallest id that may be used for overflow purposes.
+  const uint32_t first_overflow_id_;
+
   // Controls whether the validator should be run after every replay step.
   const bool validate_during_replay_;
 
diff --git a/source/fuzz/shrinker.cpp b/source/fuzz/shrinker.cpp
index 7b88405..a88a1ea 100644
--- a/source/fuzz/shrinker.cpp
+++ b/source/fuzz/shrinker.cpp
@@ -61,35 +61,27 @@
 
 }  // namespace
 
-uint32_t Shrinker::GetIdBound(const std::vector<uint32_t>& binary) const {
-  // Build the module from the input binary.
-  std::unique_ptr<opt::IRContext> ir_context =
-      BuildModule(target_env_, consumer_, binary.data(), binary.size());
-  assert(ir_context && "Error building module.");
-  return ir_context->module()->id_bound();
-}
-
-Shrinker::Shrinker(spv_target_env target_env, uint32_t step_limit,
-                   bool validate_during_replay,
-                   spv_validator_options validator_options)
+Shrinker::Shrinker(
+    spv_target_env target_env, MessageConsumer consumer,
+    const std::vector<uint32_t>& binary_in,
+    const protobufs::FactSequence& initial_facts,
+    const protobufs::TransformationSequence& transformation_sequence_in,
+    const InterestingnessFunction& interestingness_function,
+    uint32_t step_limit, bool validate_during_replay,
+    spv_validator_options validator_options)
     : target_env_(target_env),
+      consumer_(consumer),
+      binary_in_(binary_in),
+      initial_facts_(initial_facts),
+      transformation_sequence_in_(transformation_sequence_in),
+      interestingness_function_(interestingness_function),
       step_limit_(step_limit),
       validate_during_replay_(validate_during_replay),
       validator_options_(validator_options) {}
 
 Shrinker::~Shrinker() = default;
 
-void Shrinker::SetMessageConsumer(MessageConsumer consumer) {
-  consumer_ = std::move(consumer);
-}
-
-Shrinker::ShrinkerResultStatus Shrinker::Run(
-    const std::vector<uint32_t>& binary_in,
-    const protobufs::FactSequence& initial_facts,
-    const protobufs::TransformationSequence& transformation_sequence_in,
-    const Shrinker::InterestingnessFunction& interestingness_function,
-    std::vector<uint32_t>* binary_out,
-    protobufs::TransformationSequence* transformation_sequence_out) const {
+Shrinker::ShrinkerResult Shrinker::Run() {
   // Check compatibility between the library version being linked with and the
   // header files being used.
   GOOGLE_PROTOBUF_VERIFY_VERSION;
@@ -98,46 +90,54 @@
   if (!tools.IsValid()) {
     consumer_(SPV_MSG_ERROR, nullptr, {},
               "Failed to create SPIRV-Tools interface; stopping.");
-    return Shrinker::ShrinkerResultStatus::kFailedToCreateSpirvToolsInterface;
+    return {Shrinker::ShrinkerResultStatus::kFailedToCreateSpirvToolsInterface,
+            std::vector<uint32_t>(), protobufs::TransformationSequence()};
   }
 
   // Initial binary should be valid.
-  if (!tools.Validate(&binary_in[0], binary_in.size(), validator_options_)) {
+  if (!tools.Validate(&binary_in_[0], binary_in_.size(), validator_options_)) {
     consumer_(SPV_MSG_INFO, nullptr, {},
               "Initial binary is invalid; stopping.");
-    return Shrinker::ShrinkerResultStatus::kInitialBinaryInvalid;
+    return {Shrinker::ShrinkerResultStatus::kInitialBinaryInvalid,
+            std::vector<uint32_t>(), protobufs::TransformationSequence()};
   }
 
-  std::vector<uint32_t> current_best_binary;
-  protobufs::TransformationSequence current_best_transformations;
-
-  // Run a replay of the initial transformation sequence to (a) check that it
-  // succeeds, (b) get the binary that results from running these
-  // transformations, and (c) get the subsequence of the initial transformations
-  // that actually apply (in principle this could be a strict subsequence).
-  Replayer replayer(target_env_, validate_during_replay_, validator_options_);
-  replayer.SetMessageConsumer(consumer_);
-  if (replayer.Run(binary_in, initial_facts, transformation_sequence_in,
-                   static_cast<uint32_t>(
-                       transformation_sequence_in.transformation_size()),
-                   /* No overflow ids */ 0, &current_best_binary,
-                   &current_best_transformations) !=
+  // Run a replay of the initial transformation sequence to check that it
+  // succeeds.
+  auto initial_replay_result =
+      Replayer(target_env_, consumer_, binary_in_, initial_facts_,
+               transformation_sequence_in_,
+               static_cast<uint32_t>(
+                   transformation_sequence_in_.transformation_size()),
+               /* No overflow ids */ 0, validate_during_replay_,
+               validator_options_)
+          .Run();
+  if (initial_replay_result.status !=
       Replayer::ReplayerResultStatus::kComplete) {
-    return ShrinkerResultStatus::kReplayFailed;
+    return {ShrinkerResultStatus::kReplayFailed, std::vector<uint32_t>(),
+            protobufs::TransformationSequence()};
   }
+  // Get the binary that results from running these transformations, and the
+  // subsequence of the initial transformations that actually apply (in
+  // principle this could be a strict subsequence).
+  std::vector<uint32_t> current_best_binary =
+      std::move(initial_replay_result.transformed_binary);
+  protobufs::TransformationSequence current_best_transformations =
+      std::move(initial_replay_result.applied_transformations);
 
   // Check that the binary produced by applying the initial transformations is
   // indeed interesting.
-  if (!interestingness_function(current_best_binary, 0)) {
+  if (!interestingness_function_(current_best_binary, 0)) {
     consumer_(SPV_MSG_INFO, nullptr, {},
               "Initial binary is not interesting; stopping.");
-    return ShrinkerResultStatus::kInitialBinaryNotInteresting;
+    return {ShrinkerResultStatus::kInitialBinaryNotInteresting,
+            std::vector<uint32_t>(), protobufs::TransformationSequence()};
   }
 
   // The largest id used by the module before any shrinking has been applied
   // serves as the first id that can be used for overflow purposes.
   const uint32_t first_overflow_id = GetIdBound(current_best_binary);
-  assert(first_overflow_id >= GetIdBound(binary_in) &&
+  assert(first_overflow_id >= GetIdBound(binary_in_) &&
          "Applying transformations should only increase a module's id bound.");
 
   uint32_t attempt = 0;  // Keeps track of the number of shrink attempts that
@@ -195,29 +195,34 @@
       // replay might be even smaller than the transformations with the chunk
       // removed, because removing those transformations might make further
       // transformations inapplicable.
-      std::vector<uint32_t> next_binary;
-      protobufs::TransformationSequence next_transformation_sequence;
-      if (replayer.Run(
-              binary_in, initial_facts, transformations_with_chunk_removed,
+      auto replay_result =
+          Replayer(
+              target_env_, consumer_, binary_in_, initial_facts_,
+              transformations_with_chunk_removed,
               static_cast<uint32_t>(
                   transformations_with_chunk_removed.transformation_size()),
-              first_overflow_id, &next_binary, &next_transformation_sequence) !=
-          Replayer::ReplayerResultStatus::kComplete) {
+              first_overflow_id, validate_during_replay_, validator_options_)
+              .Run();
+      if (replay_result.status != Replayer::ReplayerResultStatus::kComplete) {
         // Replay should not fail; if it does, we need to abort shrinking.
-        return ShrinkerResultStatus::kReplayFailed;
+        return {ShrinkerResultStatus::kReplayFailed, std::vector<uint32_t>(),
+                protobufs::TransformationSequence()};
       }
 
-      assert(NumRemainingTransformations(next_transformation_sequence) >=
-                 chunk_index * chunk_size &&
-             "Removing this chunk of transformations should not have an effect "
-             "on earlier chunks.");
+      assert(
+          NumRemainingTransformations(replay_result.applied_transformations) >=
+              chunk_index * chunk_size &&
+          "Removing this chunk of transformations should not have an effect "
+          "on earlier chunks.");
 
-      if (interestingness_function(next_binary, attempt)) {
+      if (interestingness_function_(replay_result.transformed_binary,
+                                    attempt)) {
         // If the binary arising from the smaller transformation sequence is
         // interesting, this becomes our current best binary and transformation
         // sequence.
-        current_best_binary = next_binary;
-        current_best_transformations = next_transformation_sequence;
+        current_best_binary = std::move(replay_result.transformed_binary);
+        current_best_transformations =
+            std::move(replay_result.applied_transformations);
         progress_this_round = true;
       }
       // Either way, this was a shrink attempt, so increment our count of shrink
@@ -237,22 +242,32 @@
     }
   }
 
-  // The output from the shrinker is the best binary we saw, and the
-  // transformations that led to it.
-  *binary_out = current_best_binary;
-  *transformation_sequence_out = current_best_transformations;
-
   // Indicate whether shrinking completed or was truncated due to reaching the
   // step limit.
+  //
+  // Either way, the output from the shrinker is the best binary we saw, and the
+  // transformations that led to it.
   assert(attempt <= step_limit_);
   if (attempt == step_limit_) {
     std::stringstream strstream;
     strstream << "Shrinking did not complete; step limit " << step_limit_
               << " was reached.";
     consumer_(SPV_MSG_WARNING, nullptr, {}, strstream.str().c_str());
-    return Shrinker::ShrinkerResultStatus::kStepLimitReached;
+    return {Shrinker::ShrinkerResultStatus::kStepLimitReached,
+            std::move(current_best_binary),
+            std::move(current_best_transformations)};
   }
-  return Shrinker::ShrinkerResultStatus::kComplete;
+  return {Shrinker::ShrinkerResultStatus::kComplete,
+          std::move(current_best_binary),
+          std::move(current_best_transformations)};
+}
+
+uint32_t Shrinker::GetIdBound(const std::vector<uint32_t>& binary) const {
+  // Build the module from the input binary.
+  std::unique_ptr<opt::IRContext> ir_context =
+      BuildModule(target_env_, consumer_, binary.data(), binary.size());
+  assert(ir_context && "Error building module.");
+  return ir_context->module()->id_bound();
 }
 
 }  // namespace fuzz
diff --git a/source/fuzz/shrinker.h b/source/fuzz/shrinker.h
index 0fe8929..982a843 100644
--- a/source/fuzz/shrinker.h
+++ b/source/fuzz/shrinker.h
@@ -30,7 +30,7 @@
 class Shrinker {
  public:
   // Possible statuses that can result from running the shrinker.
-  enum ShrinkerResultStatus {
+  enum class ShrinkerResultStatus {
     kComplete,
     kFailedToCreateSpirvToolsInterface,
     kInitialBinaryInvalid,
@@ -39,6 +39,12 @@
     kStepLimitReached,
   };
 
+  struct ShrinkerResult {
+    ShrinkerResultStatus status;
+    std::vector<uint32_t> transformed_binary;
+    protobufs::TransformationSequence applied_transformations;
+  };
+
   // The type for a function that will take a binary, |binary|, and return true
   // if and only if the binary is deemed interesting. (The function also takes
   // an integer argument, |counter|, that will be incremented each time the
@@ -49,8 +55,12 @@
   using InterestingnessFunction = std::function<bool(
       const std::vector<uint32_t>& binary, uint32_t counter)>;
 
-  Shrinker(spv_target_env target_env, uint32_t step_limit,
-           bool validate_during_replay,
+  Shrinker(spv_target_env target_env, MessageConsumer consumer,
+           const std::vector<uint32_t>& binary_in,
+           const protobufs::FactSequence& initial_facts,
+           const protobufs::TransformationSequence& transformation_sequence_in,
+           const InterestingnessFunction& interestingness_function,
+           uint32_t step_limit, bool validate_during_replay,
            spv_validator_options validator_options);
 
   // Disables copy/move constructor/assignment operations.
@@ -61,25 +71,20 @@
 
   ~Shrinker();
 
-  // Sets the message consumer to the given |consumer|. The |consumer| will be
-  // invoked once for each message communicated from the library.
-  void SetMessageConsumer(MessageConsumer consumer);
-
-  // Requires that when |transformation_sequence_in| is applied to |binary_in|
-  // with initial facts |initial_facts|, the resulting binary is interesting
-  // according to |interestingness_function|.
+  // Requires that when |transformation_sequence_in_| is applied to |binary_in_|
+  // with initial facts |initial_facts_|, the resulting binary is interesting
+  // according to |interestingness_function_|.
   //
-  // Produces, via |transformation_sequence_out|, a subsequence of
-  // |transformation_sequence_in| that, when applied with initial facts
-  // |initial_facts|, produces a binary (captured via |binary_out|) that is
-  // also interesting according to |interestingness_function|.
-  ShrinkerResultStatus Run(
-      const std::vector<uint32_t>& binary_in,
-      const protobufs::FactSequence& initial_facts,
-      const protobufs::TransformationSequence& transformation_sequence_in,
-      const InterestingnessFunction& interestingness_function,
-      std::vector<uint32_t>* binary_out,
-      protobufs::TransformationSequence* transformation_sequence_out) const;
+  // If shrinking succeeded -- possibly terminating early due to reaching the
+  // shrinker's step limit -- an associated result status is returned together
+  // with a subsequence of |transformation_sequence_in_| that, when applied
+  // to |binary_in_| with initial facts |initial_facts_|, produces a binary
+  // that is also interesting according to |interestingness_function_|; this
+  // binary is also returned.
+  //
+  // If shrinking failed for some reason, an appropriate result status is
+  // returned together with an empty binary and empty transformation sequence.
+  ShrinkerResult Run();
 
  private:
   // Returns the id bound for the given SPIR-V binary, which is assumed to be
@@ -89,9 +94,22 @@
   // Target environment.
   const spv_target_env target_env_;
 
-  // Message consumer.
+  // Message consumer that will be invoked once for each message communicated
+  // from the library.
   MessageConsumer consumer_;
 
+  // The binary to which transformations are to be applied.
+  const std::vector<uint32_t>& binary_in_;
+
+  // Initial facts known to hold in advance of applying any transformations.
+  const protobufs::FactSequence& initial_facts_;
+
+  // The series of transformations to be shrunk.
+  const protobufs::TransformationSequence& transformation_sequence_in_;
+
+  // Function that decides whether a given binary is interesting.
+  const InterestingnessFunction& interestingness_function_;
+
   // Step limit to decide when to terminate shrinking early.
   const uint32_t step_limit_;
 
diff --git a/test/fuzz/fuzzer_replayer_test.cpp b/test/fuzz/fuzzer_replayer_test.cpp
index eb2b817..bfcf4ea 100644
--- a/test/fuzz/fuzzer_replayer_test.cpp
+++ b/test/fuzz/fuzzer_replayer_test.cpp
@@ -14,6 +14,7 @@
 
 #include "source/fuzz/fuzzer.h"
 #include "source/fuzz/fuzzer_util.h"
+#include "source/fuzz/pseudo_random_generator.h"
 #include "source/fuzz/replayer.h"
 #include "source/fuzz/uniform_buffer_element_descriptor.h"
 #include "test/fuzz/fuzz_test_util.h"
@@ -1645,51 +1646,46 @@
       Fuzzer::RepeatedPassStrategy::kRandomWithRecommendations};
   uint32_t strategy_index = 0;
   for (uint32_t seed = initial_seed; seed < initial_seed + num_runs; seed++) {
-    std::vector<uint32_t> fuzzer_binary_out;
-    protobufs::TransformationSequence fuzzer_transformation_sequence_out;
-
     spvtools::ValidatorOptions validator_options;
     // Every 4th time we run the fuzzer, enable all fuzzer passes.
     bool enable_all_passes = (seed % 4) == 0;
-    Fuzzer fuzzer(env, seed, enable_all_passes, strategies[strategy_index],
-                  true, validator_options);
+    auto fuzzer_result =
+        Fuzzer(env, kSilentConsumer, binary_in, initial_facts, donor_suppliers,
+               MakeUnique<PseudoRandomGenerator>(seed), enable_all_passes,
+               strategies[strategy_index], true, validator_options)
+            .Run();
 
     // Cycle the repeated pass strategy so that we try a different one next time
     // we run the fuzzer.
     strategy_index =
         (strategy_index + 1) % static_cast<uint32_t>(strategies.size());
 
-    fuzzer.SetMessageConsumer(kConsoleMessageConsumer);
-    auto fuzzer_result_status =
-        fuzzer.Run(binary_in, initial_facts, donor_suppliers,
-                   &fuzzer_binary_out, &fuzzer_transformation_sequence_out);
-    ASSERT_EQ(Fuzzer::FuzzerResultStatus::kComplete, fuzzer_result_status);
-    ASSERT_TRUE(t.Validate(fuzzer_binary_out));
+    ASSERT_EQ(Fuzzer::FuzzerResultStatus::kComplete, fuzzer_result.status);
+    ASSERT_TRUE(t.Validate(fuzzer_result.transformed_binary));
 
-    std::vector<uint32_t> replayer_binary_out;
-    protobufs::TransformationSequence replayer_transformation_sequence_out;
-
-    Replayer replayer(env, false, validator_options);
-    replayer.SetMessageConsumer(kConsoleMessageConsumer);
-    auto replayer_result_status = replayer.Run(
-        binary_in, initial_facts, fuzzer_transformation_sequence_out,
-        static_cast<uint32_t>(
-            fuzzer_transformation_sequence_out.transformation_size()),
-        0, &replayer_binary_out, &replayer_transformation_sequence_out);
+    auto replayer_result =
+        Replayer(
+            env, kConsoleMessageConsumer, binary_in, initial_facts,
+            fuzzer_result.applied_transformations,
+            static_cast<uint32_t>(
+                fuzzer_result.applied_transformations.transformation_size()),
+            0, false, validator_options)
+            .Run();
     ASSERT_EQ(Replayer::ReplayerResultStatus::kComplete,
-              replayer_result_status);
+              replayer_result.status);
 
     // After replaying the transformations applied by the fuzzer, exactly those
     // transformations should have been applied, and the binary resulting from
     // replay should be identical to that which resulted from fuzzing.
     std::string fuzzer_transformations_string;
     std::string replayer_transformations_string;
-    fuzzer_transformation_sequence_out.SerializeToString(
+    fuzzer_result.applied_transformations.SerializeToString(
         &fuzzer_transformations_string);
-    replayer_transformation_sequence_out.SerializeToString(
+    replayer_result.applied_transformations.SerializeToString(
         &replayer_transformations_string);
     ASSERT_EQ(fuzzer_transformations_string, replayer_transformations_string);
-    ASSERT_EQ(fuzzer_binary_out, replayer_binary_out);
+    ASSERT_EQ(fuzzer_result.transformed_binary,
+              replayer_result.transformed_binary);
   }
 }
 
diff --git a/test/fuzz/fuzzer_shrinker_test.cpp b/test/fuzz/fuzzer_shrinker_test.cpp
index c9ae42a..361f5e8 100644
--- a/test/fuzz/fuzzer_shrinker_test.cpp
+++ b/test/fuzz/fuzzer_shrinker_test.cpp
@@ -991,25 +991,25 @@
     uint32_t expected_transformations_out_size, uint32_t step_limit,
     spv_validator_options validator_options) {
   // Run the shrinker.
-  Shrinker shrinker(target_env, step_limit, false, validator_options);
-  shrinker.SetMessageConsumer(kSilentConsumer);
+  auto shrinker_result =
+      Shrinker(target_env, kSilentConsumer, binary_in, initial_facts,
+               transformation_sequence_in, interestingness_function, step_limit,
+               false, validator_options)
+          .Run();
 
-  std::vector<uint32_t> binary_out;
-  protobufs::TransformationSequence transformations_out;
-  Shrinker::ShrinkerResultStatus shrinker_result_status =
-      shrinker.Run(binary_in, initial_facts, transformation_sequence_in,
-                   interestingness_function, &binary_out, &transformations_out);
   ASSERT_TRUE(Shrinker::ShrinkerResultStatus::kComplete ==
-                  shrinker_result_status ||
+                  shrinker_result.status ||
               Shrinker::ShrinkerResultStatus::kStepLimitReached ==
-                  shrinker_result_status);
+                  shrinker_result.status);
 
   // If a non-empty expected binary was provided, check that it matches the
   // result of shrinking and that the expected number of transformations remain.
   if (!expected_binary_out.empty()) {
-    ASSERT_EQ(expected_binary_out, binary_out);
-    ASSERT_EQ(expected_transformations_out_size,
-              static_cast<uint32_t>(transformations_out.transformation_size()));
+    ASSERT_EQ(expected_binary_out, shrinker_result.transformed_binary);
+    ASSERT_EQ(
+        expected_transformations_out_size,
+        static_cast<uint32_t>(
+            shrinker_result.applied_transformations.transformation_size()));
   }
 }
 
@@ -1037,8 +1037,6 @@
   }
 
   // Run the fuzzer and check that it successfully yields a valid binary.
-  std::vector<uint32_t> fuzzer_binary_out;
-  protobufs::TransformationSequence fuzzer_transformation_sequence_out;
   spvtools::ValidatorOptions validator_options;
 
   // Depending on the seed, decide whether to enable all passes and which
@@ -1055,14 +1053,13 @@
         Fuzzer::RepeatedPassStrategy::kRandomWithRecommendations;
   }
 
-  Fuzzer fuzzer(env, seed, enable_all_passes, repeated_pass_strategy, true,
-                validator_options);
-  fuzzer.SetMessageConsumer(kSilentConsumer);
-  auto fuzzer_result_status =
-      fuzzer.Run(binary_in, initial_facts, donor_suppliers, &fuzzer_binary_out,
-                 &fuzzer_transformation_sequence_out);
-  ASSERT_EQ(Fuzzer::FuzzerResultStatus::kComplete, fuzzer_result_status);
-  ASSERT_TRUE(t.Validate(fuzzer_binary_out));
+  auto fuzzer_result =
+      Fuzzer(env, kSilentConsumer, binary_in, initial_facts, donor_suppliers,
+             MakeUnique<PseudoRandomGenerator>(seed), enable_all_passes,
+             repeated_pass_strategy, true, validator_options)
+          .Run();
+  ASSERT_EQ(Fuzzer::FuzzerResultStatus::kComplete, fuzzer_result.status);
+  ASSERT_TRUE(t.Validate(fuzzer_result.transformed_binary));
 
   const uint32_t kReasonableStepLimit = 50;
   const uint32_t kSmallStepLimit = 20;
@@ -1070,30 +1067,30 @@
   // With the AlwaysInteresting test, we should quickly shrink to the original
   // binary with no transformations remaining.
   RunAndCheckShrinker(env, binary_in, initial_facts,
-                      fuzzer_transformation_sequence_out,
+                      fuzzer_result.applied_transformations,
                       AlwaysInteresting().AsFunction(), binary_in, 0,
                       kReasonableStepLimit, validator_options);
 
   // With the OnlyInterestingFirstTime test, no shrinking should be achieved.
   RunAndCheckShrinker(
-      env, binary_in, initial_facts, fuzzer_transformation_sequence_out,
-      OnlyInterestingFirstTime().AsFunction(), fuzzer_binary_out,
+      env, binary_in, initial_facts, fuzzer_result.applied_transformations,
+      OnlyInterestingFirstTime().AsFunction(), fuzzer_result.transformed_binary,
       static_cast<uint32_t>(
-          fuzzer_transformation_sequence_out.transformation_size()),
+          fuzzer_result.applied_transformations.transformation_size()),
       kReasonableStepLimit, validator_options);
 
   // The PingPong test is unpredictable; passing an empty expected binary
   // means that we don't check anything beyond that shrinking completes
   // successfully.
   RunAndCheckShrinker(
-      env, binary_in, initial_facts, fuzzer_transformation_sequence_out,
+      env, binary_in, initial_facts, fuzzer_result.applied_transformations,
       PingPong().AsFunction(), {}, 0, kSmallStepLimit, validator_options);
 
   // The InterestingThenRandom test is unpredictable; passing an empty
   // expected binary means that we do not check anything about shrinking
   // results.
   RunAndCheckShrinker(
-      env, binary_in, initial_facts, fuzzer_transformation_sequence_out,
+      env, binary_in, initial_facts, fuzzer_result.applied_transformations,
       InterestingThenRandom(PseudoRandomGenerator(seed)).AsFunction(), {}, 0,
       kSmallStepLimit, validator_options);
 }
diff --git a/test/fuzz/replayer_test.cpp b/test/fuzz/replayer_test.cpp
index 39787d2..2444e9f 100644
--- a/test/fuzz/replayer_test.cpp
+++ b/test/fuzz/replayer_test.cpp
@@ -89,20 +89,17 @@
 
   {
     // Full replay
-    protobufs::TransformationSequence transformations_out;
     protobufs::FactSequence empty_facts;
-    std::vector<uint32_t> binary_out;
-    Replayer replayer(env, true, validator_options);
-    replayer.SetMessageConsumer(kSilentConsumer);
-    auto replayer_result_status =
-        replayer.Run(binary_in, empty_facts, transformations, 11, 0,
-                     &binary_out, &transformations_out);
+    auto replayer_result =
+        Replayer(env, kSilentConsumer, binary_in, empty_facts, transformations,
+                 11, 0, true, validator_options)
+            .Run();
     // Replay should succeed.
     ASSERT_EQ(Replayer::ReplayerResultStatus::kComplete,
-              replayer_result_status);
+              replayer_result.status);
     // All transformations should be applied.
     ASSERT_TRUE(google::protobuf::util::MessageDifferencer::Equals(
-        transformations, transformations_out));
+        transformations, replayer_result.applied_transformations));
 
     const std::string kFullySplitShader = R"(
                OpCapability Shader
@@ -172,28 +169,26 @@
                OpReturn
                OpFunctionEnd
     )";
-    ASSERT_TRUE(IsEqual(env, kFullySplitShader, binary_out));
+    ASSERT_TRUE(
+        IsEqual(env, kFullySplitShader, replayer_result.transformed_binary));
   }
 
   {
     // Half replay
-    protobufs::TransformationSequence transformations_out;
     protobufs::FactSequence empty_facts;
-    std::vector<uint32_t> binary_out;
-    Replayer replayer(env, true, validator_options);
-    replayer.SetMessageConsumer(kSilentConsumer);
-    auto replayer_result_status =
-        replayer.Run(binary_in, empty_facts, transformations, 5, 0, &binary_out,
-                     &transformations_out);
+    auto replayer_result =
+        Replayer(env, kSilentConsumer, binary_in, empty_facts, transformations,
+                 5, 0, true, validator_options)
+            .Run();
     // Replay should succeed.
     ASSERT_EQ(Replayer::ReplayerResultStatus::kComplete,
-              replayer_result_status);
+              replayer_result.status);
     // The first 5 transformations should be applied
-    ASSERT_EQ(5, transformations_out.transformation_size());
+    ASSERT_EQ(5, replayer_result.applied_transformations.transformation_size());
     for (uint32_t i = 0; i < 5; i++) {
       ASSERT_TRUE(google::protobuf::util::MessageDifferencer::Equals(
           transformations.transformation(i),
-          transformations_out.transformation(i)));
+          replayer_result.applied_transformations.transformation(i)));
     }
 
     const std::string kHalfSplitShader = R"(
@@ -252,47 +247,42 @@
                OpReturn
                OpFunctionEnd
     )";
-    ASSERT_TRUE(IsEqual(env, kHalfSplitShader, binary_out));
+    ASSERT_TRUE(
+        IsEqual(env, kHalfSplitShader, replayer_result.transformed_binary));
   }
 
   {
     // Empty replay
-    protobufs::TransformationSequence transformations_out;
     protobufs::FactSequence empty_facts;
-    std::vector<uint32_t> binary_out;
-    Replayer replayer(env, true, validator_options);
-    replayer.SetMessageConsumer(kSilentConsumer);
-    auto replayer_result_status =
-        replayer.Run(binary_in, empty_facts, transformations, 0, 0, &binary_out,
-                     &transformations_out);
+    auto replayer_result =
+        Replayer(env, kSilentConsumer, binary_in, empty_facts, transformations,
+                 0, 0, true, validator_options)
+            .Run();
     // Replay should succeed.
     ASSERT_EQ(Replayer::ReplayerResultStatus::kComplete,
-              replayer_result_status);
+              replayer_result.status);
     // No transformations should be applied
-    ASSERT_EQ(0, transformations_out.transformation_size());
-    ASSERT_TRUE(IsEqual(env, kTestShader, binary_out));
+    ASSERT_EQ(0, replayer_result.applied_transformations.transformation_size());
+    ASSERT_TRUE(IsEqual(env, kTestShader, replayer_result.transformed_binary));
   }
 
   {
     // Invalid replay: too many transformations
-    protobufs::TransformationSequence transformations_out;
     protobufs::FactSequence empty_facts;
-    std::vector<uint32_t> binary_out;
     // The number of transformations requested to be applied exceeds the number
     // of transformations
-    Replayer replayer(env, true, validator_options);
-    replayer.SetMessageConsumer(kSilentConsumer);
-    auto replayer_result_status =
-        replayer.Run(binary_in, empty_facts, transformations, 12, 0,
-                     &binary_out, &transformations_out);
+    auto replayer_result =
+        Replayer(env, kSilentConsumer, binary_in, empty_facts, transformations,
+                 12, 0, true, validator_options)
+            .Run();
 
     // Replay should not succeed.
     ASSERT_EQ(Replayer::ReplayerResultStatus::kTooManyTransformationsRequested,
-              replayer_result_status);
+              replayer_result.status);
     // No transformations should be applied
-    ASSERT_EQ(0, transformations_out.transformation_size());
+    ASSERT_EQ(0, replayer_result.applied_transformations.transformation_size());
     // The output binary should be empty
-    ASSERT_TRUE(binary_out.empty());
+    ASSERT_TRUE(replayer_result.transformed_binary.empty());
   }
 }
 
diff --git a/tools/fuzz/fuzz.cpp b/tools/fuzz/fuzz.cpp
index ea7e0b7..80ac9f5 100644
--- a/tools/fuzz/fuzz.cpp
+++ b/tools/fuzz/fuzz.cpp
@@ -16,6 +16,7 @@
 #include <cerrno>
 #include <cstring>
 #include <fstream>
+#include <memory>
 #include <random>
 #include <sstream>
 #include <string>
@@ -24,12 +25,14 @@
 #include "source/fuzz/fuzzer.h"
 #include "source/fuzz/fuzzer_util.h"
 #include "source/fuzz/protobufs/spirvfuzz_protobufs.h"
+#include "source/fuzz/pseudo_random_generator.h"
 #include "source/fuzz/replayer.h"
 #include "source/fuzz/shrinker.h"
 #include "source/opt/build_module.h"
 #include "source/opt/ir_context.h"
 #include "source/opt/log.h"
 #include "source/spirv_fuzzer_options.h"
+#include "source/util/make_unique.h"
 #include "source/util/string_utils.h"
 #include "tools/io.h"
 #include "tools/util/cli_consumer.h"
@@ -453,9 +456,6 @@
                             &transformation_sequence)) {
     return false;
   }
-  spvtools::fuzz::Replayer replayer(
-      target_env, fuzzer_options->replay_validation_enabled, validator_options);
-  replayer.SetMessageConsumer(spvtools::utils::CLIMessageConsumer);
 
   uint32_t num_transformations_to_apply;
   if (fuzzer_options->replay_range > 0) {
@@ -474,11 +474,17 @@
                         fuzzer_options->replay_range));
   }
 
-  auto replay_result_status = replayer.Run(
-      binary_in, initial_facts, transformation_sequence,
-      num_transformations_to_apply, 0, binary_out, transformations_applied);
-  return !(replay_result_status !=
-           spvtools::fuzz::Replayer::ReplayerResultStatus::kComplete);
+  auto replay_result =
+      spvtools::fuzz::Replayer(
+          target_env, spvtools::utils::CLIMessageConsumer, binary_in,
+          initial_facts, transformation_sequence, num_transformations_to_apply,
+          0, fuzzer_options->replay_validation_enabled, validator_options)
+          .Run();
+
+  *binary_out = std::move(replay_result.transformed_binary);
+  *transformations_applied = std::move(replay_result.applied_transformations);
+  return replay_result.status ==
+         spvtools::fuzz::Replayer::ReplayerResultStatus::kComplete;
 }
 
 bool Shrink(const spv_target_env& target_env,
@@ -497,11 +503,6 @@
                             &transformation_sequence)) {
     return false;
   }
-  spvtools::fuzz::Shrinker shrinker(
-      target_env, fuzzer_options->shrinker_step_limit,
-      fuzzer_options->replay_validation_enabled, validator_options);
-  shrinker.SetMessageConsumer(spvtools::utils::CLIMessageConsumer);
-
   assert(!interestingness_command.empty() &&
          "An error should have been raised because the interestingness_command "
          "is empty.");
@@ -527,13 +528,20 @@
     return ExecuteCommand(command);
   };
 
-  auto shrink_result_status = shrinker.Run(
-      binary_in, initial_facts, transformation_sequence,
-      interestingness_function, binary_out, transformations_applied);
+  auto shrink_result =
+      spvtools::fuzz::Shrinker(
+          target_env, spvtools::utils::CLIMessageConsumer, binary_in,
+          initial_facts, transformation_sequence, interestingness_function,
+          fuzzer_options->shrinker_step_limit,
+          fuzzer_options->replay_validation_enabled, validator_options)
+          .Run();
+
+  *binary_out = std::move(shrink_result.transformed_binary);
+  *transformations_applied = std::move(shrink_result.applied_transformations);
   return spvtools::fuzz::Shrinker::ShrinkerResultStatus::kComplete ==
-             shrink_result_status ||
+             shrink_result.status ||
          spvtools::fuzz::Shrinker::ShrinkerResultStatus::kStepLimitReached ==
-             shrink_result_status;
+             shrink_result.status;
 }
 
 bool Fuzz(const spv_target_env& target_env,
@@ -571,18 +579,20 @@
         });
   }
 
-  spvtools::fuzz::Fuzzer fuzzer(
-      target_env,
-      fuzzer_options->has_random_seed
-          ? fuzzer_options->random_seed
-          : static_cast<uint32_t>(std::random_device()()),
-      fuzzer_options->all_passes_enabled, repeated_pass_strategy,
-      fuzzer_options->fuzzer_pass_validation_enabled, validator_options);
-  fuzzer.SetMessageConsumer(message_consumer);
-  auto fuzz_result_status =
-      fuzzer.Run(binary_in, initial_facts, donor_suppliers, binary_out,
-                 transformations_applied);
-  if (fuzz_result_status !=
+  auto fuzz_result =
+      spvtools::fuzz::Fuzzer(
+          target_env, message_consumer, binary_in, initial_facts,
+          donor_suppliers,
+          spvtools::MakeUnique<spvtools::fuzz::PseudoRandomGenerator>(
+              fuzzer_options->has_random_seed
+                  ? fuzzer_options->random_seed
+                  : static_cast<uint32_t>(std::random_device()())),
+          fuzzer_options->all_passes_enabled, repeated_pass_strategy,
+          fuzzer_options->fuzzer_pass_validation_enabled, validator_options)
+          .Run();
+  *binary_out = std::move(fuzz_result.transformed_binary);
+  *transformations_applied = std::move(fuzz_result.applied_transformations);
+  if (fuzz_result.status !=
       spvtools::fuzz::Fuzzer::FuzzerResultStatus::kComplete) {
     spvtools::Error(FuzzDiagnostic, nullptr, {}, "Error running fuzzer");
     return false;