Clean up IRObject for custom domain creation.

This includes:

1. Making the data field private and change any external access to use public methods.

2. Makeing IRObject/string conversion standalone instead of member functions.

   This makes it explicit that the string (i.e. persistent) representation of IRObject is internal and can only be called using the internal namespace.

PiperOrigin-RevId: 846782345
diff --git a/domain_tests/BUILD b/domain_tests/BUILD
index eca5a18..623ca0f 100644
--- a/domain_tests/BUILD
+++ b/domain_tests/BUILD
@@ -213,6 +213,7 @@
         "@abseil-cpp//absl/types:span",
         "@com_google_fuzztest//fuzztest:domain_core",
         "@com_google_fuzztest//fuzztest/internal:meta",
+        "@com_google_fuzztest//fuzztest/internal:serialization",
         "@com_google_fuzztest//fuzztest/internal:type_support",
         "@googletest//:gtest_main",
     ],
diff --git a/domain_tests/CMakeLists.txt b/domain_tests/CMakeLists.txt
index 69e21b6..9a40b67 100644
--- a/domain_tests/CMakeLists.txt
+++ b/domain_tests/CMakeLists.txt
@@ -189,6 +189,7 @@
     absl::span
     fuzztest::domain_core
     fuzztest::meta
+    fuzztest::serialization
     fuzztest::type_support
     GTest::gmock_main
 )
diff --git a/domain_tests/domain_testing.h b/domain_tests/domain_testing.h
index 9574b58..ca2aa3b 100644
--- a/domain_tests/domain_testing.h
+++ b/domain_tests/domain_testing.h
@@ -239,8 +239,9 @@
         << "v=" << v << " new_v=" << testing::PrintToString(new_v);
   }
   {
-    auto serialized = domain.SerializeCorpus(v.corpus_value).ToString();
-    auto parsed = internal::IRObject::FromString(serialized);
+    auto serialized =
+        internal::SerializeIRObject(domain.SerializeCorpus(v.corpus_value));
+    auto parsed = internal::ParseIRObject(serialized);
     ASSERT_TRUE(parsed);
     auto parsed_corpus = domain.ParseCorpus(*parsed);
     ASSERT_TRUE(parsed_corpus)
diff --git a/domain_tests/misc_domains_test.cc b/domain_tests/misc_domains_test.cc
index 2d5c94d..24e37b3 100644
--- a/domain_tests/misc_domains_test.cc
+++ b/domain_tests/misc_domains_test.cc
@@ -34,6 +34,7 @@
 #include "./fuzztest/domain_core.h"
 #include "./domain_tests/domain_testing.h"
 #include "./fuzztest/internal/meta.h"
+#include "./fuzztest/internal/serialization.h"
 #include "./fuzztest/internal/type_support.h"
 
 namespace fuzztest {
@@ -264,12 +265,16 @@
     ASSERT_TRUE(domain_0_corpus.has_value());
     auto domain_1_corpus = domain_1.FromValue(v.user_value);
     ASSERT_TRUE(domain_1_corpus.has_value());
-    EXPECT_NE(overlapped_domain.SerializeCorpus(v.corpus_value).ToString(),
-              domain_0.SerializeCorpus(*domain_0_corpus).ToString())
+    EXPECT_NE(
+        internal::SerializeIRObject(
+            overlapped_domain.SerializeCorpus(v.corpus_value)),
+        internal::SerializeIRObject(domain_0.SerializeCorpus(*domain_0_corpus)))
         << "Expect different serialized corpora before "
            "`WithSerializationDomain(...)`";
-    EXPECT_NE(overlapped_domain.SerializeCorpus(v.corpus_value).ToString(),
-              domain_1.SerializeCorpus(*domain_1_corpus).ToString())
+    EXPECT_NE(
+        internal::SerializeIRObject(
+            overlapped_domain.SerializeCorpus(v.corpus_value)),
+        internal::SerializeIRObject(domain_1.SerializeCorpus(*domain_1_corpus)))
         << "Expect different serialized corpora before "
            "`WithSerializationDomain(...)`";
   }
@@ -280,12 +285,16 @@
     ASSERT_TRUE(domain_0_corpus.has_value());
     auto domain_1_corpus = domain_1.FromValue(v.user_value);
     ASSERT_TRUE(domain_1_corpus.has_value());
-    EXPECT_EQ(overlapped_domain.SerializeCorpus(v.corpus_value).ToString(),
-              domain_0.SerializeCorpus(*domain_0_corpus).ToString())
+    EXPECT_EQ(
+        internal::SerializeIRObject(
+            overlapped_domain.SerializeCorpus(v.corpus_value)),
+        internal::SerializeIRObject(domain_0.SerializeCorpus(*domain_0_corpus)))
         << "Expect the same serialized corpora after "
            "`WithSerializationDomain(0)`";
-    EXPECT_NE(overlapped_domain.SerializeCorpus(v.corpus_value).ToString(),
-              domain_1.SerializeCorpus(*domain_1_corpus).ToString())
+    EXPECT_NE(
+        internal::SerializeIRObject(
+            overlapped_domain.SerializeCorpus(v.corpus_value)),
+        internal::SerializeIRObject(domain_1.SerializeCorpus(*domain_1_corpus)))
         << "Expect different serialized corpora after "
            "`WithSerializationDomain(0)`";
   }
@@ -296,12 +305,16 @@
     ASSERT_TRUE(domain_0_corpus.has_value());
     auto domain_1_corpus = domain_1.FromValue(v.user_value);
     ASSERT_TRUE(domain_1_corpus.has_value());
-    EXPECT_NE(overlapped_domain.SerializeCorpus(v.corpus_value).ToString(),
-              domain_0.SerializeCorpus(*domain_0_corpus).ToString())
+    EXPECT_NE(
+        internal::SerializeIRObject(
+            overlapped_domain.SerializeCorpus(v.corpus_value)),
+        internal::SerializeIRObject(domain_0.SerializeCorpus(*domain_0_corpus)))
         << "Expect different serialized corpora after "
            "`WithSerializationDomain(1)`";
-    EXPECT_EQ(overlapped_domain.SerializeCorpus(v.corpus_value).ToString(),
-              domain_1.SerializeCorpus(*domain_1_corpus).ToString())
+    EXPECT_EQ(
+        internal::SerializeIRObject(
+            overlapped_domain.SerializeCorpus(v.corpus_value)),
+        internal::SerializeIRObject(domain_1.SerializeCorpus(*domain_1_corpus)))
         << "Expect the same serialized corpora after "
            "`WithSerializationDomain(1)`";
   }
diff --git a/domain_tests/numeric_domains_test.cc b/domain_tests/numeric_domains_test.cc
index 8205ef8..fe6baa6 100644
--- a/domain_tests/numeric_domains_test.cc
+++ b/domain_tests/numeric_domains_test.cc
@@ -246,13 +246,13 @@
       : is_at_most_64_bit_integer      ? "i: $0"
                                        : R"(sub { i: 0 } sub { i: $0 })";
 
-  auto corpus_value = domain.ParseCorpus(*IRObject::FromString(absl::StrCat(
+  auto corpus_value = domain.ParseCorpus(*internal::ParseIRObject(absl::StrCat(
       "FUZZTESTv1 ",
       absl::Substitute(serialized_format, static_cast<int32_t>(max)))));
   ASSERT_TRUE(corpus_value.has_value());
   EXPECT_OK(domain.ValidateCorpusValue(*corpus_value));
 
-  corpus_value = domain.ParseCorpus(*IRObject::FromString(absl::StrCat(
+  corpus_value = domain.ParseCorpus(*internal::ParseIRObject(absl::StrCat(
       "FUZZTESTv1 ",
       absl::Substitute(serialized_format, static_cast<int32_t>(max) + 1))));
   // Greater than max should be parsed, but rejected by validation.
diff --git a/e2e_tests/functional_test.cc b/e2e_tests/functional_test.cc
index 7ae29ea..3ccbcc1 100644
--- a/e2e_tests/functional_test.cc
+++ b/e2e_tests/functional_test.cc
@@ -803,7 +803,7 @@
 
   auto replay_files = ReadFileOrDirectory(out_dir.path().c_str());
   ASSERT_EQ(replay_files.size(), 1) << std_err;
-  auto parsed = IRObject::FromString(replay_files[0].data);
+  auto parsed = ParseIRObject(replay_files[0].data);
   ASSERT_TRUE(parsed) << std_err;
   auto args = parsed->ToCorpus<std::tuple<std::string>>();
   EXPECT_THAT(args, Optional(FieldsAre(StartsWith("Fuzz")))) << std_err;
@@ -826,7 +826,7 @@
 
   auto replay_files = ReadFileOrDirectory(out_dir.path().c_str());
   ASSERT_EQ(replay_files.size(), 1) << std_err;
-  auto parsed = IRObject::FromString(replay_files[0].data);
+  auto parsed = ParseIRObject(replay_files[0].data);
   ASSERT_TRUE(parsed) << std_err;
   auto args = parsed->ToCorpus<std::tuple<std::string>>();
   EXPECT_THAT(args, Optional(FieldsAre(StartsWith("Fuzz")))) << std_err;
@@ -984,7 +984,8 @@
   template <typename T>
   ReplayFile(std::in_place_t, const T& corpus) {
     filename_ = dir_.path() / "replay_file";
-    WriteFile(filename_, internal::IRObject::FromCorpus(corpus).ToString());
+    WriteFile(filename_,
+              SerializeIRObject(internal::IRObject::FromCorpus(corpus)));
   }
 
   auto GetReplayEnv() const {
@@ -1037,7 +1038,7 @@
 
   auto replay_files = ReadFileOrDirectory(out_dir.path().c_str());
   ASSERT_EQ(replay_files.size(), 1) << std_err;
-  auto parsed = IRObject::FromString(replay_files[0].data);
+  auto parsed = ParseIRObject(replay_files[0].data);
   ASSERT_TRUE(parsed) << std_err;
   auto args = parsed->ToCorpus<std::tuple<uint8_t, double>>();
   EXPECT_THAT(args, Optional(FieldsAre(10, _))) << std_err;
@@ -1077,7 +1078,7 @@
 
     auto replay_files = ReadFileOrDirectory(out_dir.path().c_str());
     ASSERT_EQ(replay_files.size(), 1) << std_err;
-    auto parsed = IRObject::FromString(replay_files[0].data);
+    auto parsed = ParseIRObject(replay_files[0].data);
     ASSERT_TRUE(parsed) << std_err;
     auto args = parsed->ToCorpus<std::tuple<std::string>>();
     ASSERT_THAT(args, Optional(FieldsAre(HasSubstr("X"))));
diff --git a/fuzztest/internal/BUILD b/fuzztest/internal/BUILD
index cf3a529..ce749d4 100644
--- a/fuzztest/internal/BUILD
+++ b/fuzztest/internal/BUILD
@@ -86,6 +86,7 @@
         "@com_google_fuzztest//common:logging",
         "@com_google_fuzztest//common:remote_file",
         "@com_google_fuzztest//common:temp_dir",
+        "@com_google_fuzztest//fuzztest/internal:serialization",
         "@com_google_fuzztest//fuzztest/internal/domains:core_domains_impl",
     ],
 )
diff --git a/fuzztest/internal/centipede_adaptor.cc b/fuzztest/internal/centipede_adaptor.cc
index 8078eaa..9bb0256 100644
--- a/fuzztest/internal/centipede_adaptor.cc
+++ b/fuzztest/internal/centipede_adaptor.cc
@@ -88,6 +88,7 @@
 #include "./fuzztest/internal/io.h"
 #include "./fuzztest/internal/logging.h"
 #include "./fuzztest/internal/runtime.h"
+#include "./fuzztest/internal/serialization.h"
 #include "./fuzztest/internal/subprocess.h"
 #include "./fuzztest/internal/table_of_recent_compares.h"
 
@@ -504,7 +505,7 @@
     absl::c_shuffle(seeds, prng_);
     for (const auto& seed : seeds) {
       const auto seed_serialized =
-          fuzzer_impl_.params_domain_.SerializeCorpus(seed).ToString();
+          SerializeIRObject(fuzzer_impl_.params_domain_.SerializeCorpus(seed));
       seed_callback(fuzztest::internal::AsByteSpan(seed_serialized));
     }
   }
@@ -528,9 +529,8 @@
       constexpr double kDomainInitRatio = 0.0001;
       if (choice < kDomainInitRatio) {
         mutant_data =
-            fuzzer_impl_.params_domain_
-                .SerializeCorpus(fuzzer_impl_.params_domain_.Init(prng_))
-                .ToString();
+            SerializeIRObject(fuzzer_impl_.params_domain_.SerializeCorpus(
+                fuzzer_impl_.params_domain_.Init(prng_)));
       } else {
         const auto origin_index =
             absl::Uniform<size_t>(prng_, 0, inputs.size());
@@ -552,8 +552,8 @@
             mutant, prng_,
             {cmp_tables[origin_index].has_value() ? &*cmp_tables[origin_index]
                                                   : nullptr});
-        mutant_data =
-            fuzzer_impl_.params_domain_.SerializeCorpus(mutant.args).ToString();
+        mutant_data = SerializeIRObject(
+            fuzzer_impl_.params_domain_.SerializeCorpus(mutant.args));
       }
       new_mutant_callback(
           {(unsigned char*)mutant_data.data(), mutant_data.size()});
diff --git a/fuzztest/internal/compatibility_mode.cc b/fuzztest/internal/compatibility_mode.cc
index 240e919..97744b7 100644
--- a/fuzztest/internal/compatibility_mode.cc
+++ b/fuzztest/internal/compatibility_mode.cc
@@ -141,7 +141,7 @@
       impl.params_domain_.Mutate(copy, prng,
                                  /*only_shrink=*/max_size < data.size());
     }
-    result = impl.params_domain_.SerializeCorpus(copy).ToString();
+    result = SerializeIRObject(impl.params_domain_.SerializeCorpus(copy));
     if (result.size() <= max_size) break;
   }
   return result;
diff --git a/fuzztest/internal/domains/domain.h b/fuzztest/internal/domains/domain.h
index 27cd3c6..85425e2 100644
--- a/fuzztest/internal/domains/domain.h
+++ b/fuzztest/internal/domains/domain.h
@@ -378,7 +378,7 @@
 template <typename DomainT>
 absl::StatusOr<typename DomainT::value_type> ParseOneReproducerValue(
     absl::string_view data, DomainT domain) {
-  const auto ir_object = IRObject::FromString(data);
+  const auto ir_object = ParseIRObject(data);
   if (!ir_object) {
     return absl::InvalidArgumentError("Unexpected reproducer format");
   }
diff --git a/fuzztest/internal/domains/domain_base.h b/fuzztest/internal/domains/domain_base.h
index 28faefd..8b60bfe 100644
--- a/fuzztest/internal/domains/domain_base.h
+++ b/fuzztest/internal/domains/domain_base.h
@@ -39,6 +39,8 @@
 
 namespace fuzztest::domain_implementor {
 
+using IRObject = ::fuzztest::internal::IRObject;
+
 // `DomainBase` is the base class for all domain implementations.
 //
 // The type parameters are as follows:
@@ -143,14 +145,14 @@
     return v;
   }
 
-  std::optional<CorpusType> ParseCorpus(const internal::IRObject& obj) const {
+  std::optional<CorpusType> ParseCorpus(const IRObject& obj) const {
     static_assert(!has_custom_corpus_type);
     return obj.ToCorpus<CorpusType>();
   }
 
-  internal::IRObject SerializeCorpus(const CorpusType& v) const {
+  IRObject SerializeCorpus(const CorpusType& v) const {
     static_assert(!has_custom_corpus_type);
-    return internal::IRObject::FromCorpus(v);
+    return IRObject::FromCorpus(v);
   }
 
   void UpdateMemoryDictionary(const CorpusType& val, ConstCmpTablesPtr) {}
diff --git a/fuzztest/internal/runtime.cc b/fuzztest/internal/runtime.cc
index 753e708..24e094f 100644
--- a/fuzztest/internal/runtime.cc
+++ b/fuzztest/internal/runtime.cc
@@ -277,9 +277,8 @@
   FUZZTEST_CHECK(!out_location.dir_path.empty())
       << "Reproducer output directory must not be empty if "
          "not reporting to controller.";
-  const std::string content =
-      current_args_->domain.SerializeCorpus(current_args_->corpus_value)
-          .ToString();
+  const std::string content = SerializeIRObject(
+      current_args_->domain.SerializeCorpus(current_args_->corpus_value));
   std::string path = WriteDataToDir(content, out_location.dir_path);
   if (path.empty()) {
     absl::FPrintF(GetStderr(), "[!] Failed to write reproducer file!\n");
@@ -721,7 +720,7 @@
 
 absl::StatusOr<corpus_type> FuzzTestFuzzerImpl::TryParse(
     absl::string_view data) {
-  auto ir_value = IRObject::FromString(data);
+  auto ir_value = ParseIRObject(data);
   if (!ir_value) {
     return absl::InvalidArgumentError("Unexpected file format");
   }
@@ -773,7 +772,7 @@
     PRNG prng(seed_sequence_);
 
     const auto original_serialized =
-        params_domain_.SerializeCorpus(*to_minimize).ToString();
+        SerializeIRObject(params_domain_.SerializeCorpus(*to_minimize));
 
     // In minimize mode we keep mutating the given reproducer value with
     // `only_shrink=true` until we crash. We drop mutations that don't
@@ -792,7 +791,7 @@
       num_mutations = std::max(1, num_mutations - 1);
       // We compare the serialized version. Not very efficient but works for
       // now.
-      if (params_domain_.SerializeCorpus(copy).ToString() ==
+      if (SerializeIRObject(params_domain_.SerializeCorpus(copy)) ==
           original_serialized) {
         continue;
       }
@@ -987,8 +986,9 @@
 
 void FuzzTestFuzzerImpl::TryWriteCorpusFile(const Input& input) {
   if (corpus_out_dir_.empty()) return;
-  if (WriteDataToDir(params_domain_.SerializeCorpus(input.args).ToString(),
-                     corpus_out_dir_)
+  if (WriteDataToDir(
+          SerializeIRObject(params_domain_.SerializeCorpus(input.args)),
+          corpus_out_dir_)
           .empty()) {
     absl::FPrintF(GetStderr(), "[!] Failed to write corpus file.\n");
   }
@@ -1194,9 +1194,9 @@
     // Only run it if it actually is different. Random mutations might
     // not actually change the value, or we have reached a minimum that can't be
     // minimized anymore.
-    if (params_domain_.SerializeCorpus(minimal_non_fatal_counterexample_->args)
-            .ToString() !=
-        params_domain_.SerializeCorpus(copy.args).ToString()) {
+    if (SerializeIRObject(params_domain_.SerializeCorpus(
+            minimal_non_fatal_counterexample_->args)) !=
+        SerializeIRObject(params_domain_.SerializeCorpus(copy.args))) {
       runtime_.SetExternalFailureDetected(false);
       RunOneInput(copy);
       if (runtime_.external_failure_detected()) {
diff --git a/fuzztest/internal/serialization.cc b/fuzztest/internal/serialization.cc
index fe4fae7..1960dca 100644
--- a/fuzztest/internal/serialization.cc
+++ b/fuzztest/internal/serialization.cc
@@ -34,7 +34,6 @@
 namespace {
 
 struct OutputVisitor {
-  size_t index;
   int indent;
   std::string& out;
 
@@ -62,13 +61,12 @@
 
   void operator()(const std::vector<IRObject>& value) const {
     for (const auto& sub : value) {
-      const bool sub_is_scalar =
-          !std::holds_alternative<std::vector<IRObject>>(sub.value);
+      const bool has_subs = sub.HasSubs();
       absl::StrAppendFormat(&out, "%*ssub {%s", indent, "",
-                            sub_is_scalar ? " " : "\n");
-      std::visit(OutputVisitor{sub.value.index(), indent + 2, out}, sub.value);
-      absl::StrAppendFormat(&out, "%*s}\n", sub_is_scalar ? 0 : indent,
-                            sub_is_scalar ? " " : "");
+                            has_subs ? "\n" : " ");
+      sub.visit(OutputVisitor{indent + 2, out});
+      absl::StrAppendFormat(&out, "%*s}\n", has_subs ? indent : 0,
+                            has_subs ? "" : " ");
     }
   }
 };
@@ -143,7 +141,7 @@
   }
 
   if (key == "sub") {
-    auto& v = obj.value.emplace<std::vector<IRObject>>();
+    auto& v = obj.MutableSubs();
     do {
       if (ReadToken(str) != "{") return false;
       if (!ParseImpl(v.emplace_back(), str, recursion_depth + 1)) return false;
@@ -157,13 +155,12 @@
   } else {
     if (ReadToken(str) != ":") return false;
     auto value = ReadToken(str);
-    auto& v = obj.value;
     if (key == "i") {
-      return ReadScalar(v.emplace<uint64_t>(), value);
+      return ReadScalar(obj.MutableScalar<uint64_t>(), value);
     } else if (key == "d") {
-      return ReadScalar(v.emplace<double>(), value);
+      return ReadScalar(obj.MutableScalar<double>(), value);
     } else if (key == "s") {
-      return ReadScalar(v.emplace<std::string>(), value);
+      return ReadScalar(obj.MutableScalar<std::string>(), value);
     } else {
       // Unrecognized key
       return false;
@@ -227,7 +224,7 @@
     }
     offset += 1 + sizeof(size);
     for (const auto& sub : value) {
-      std::visit(BinaryOutputVisitor{buf, offset}, sub.value);
+      sub.visit(BinaryOutputVisitor{buf, offset});
     }
   }
 };
@@ -257,14 +254,14 @@
     }
     case BinaryFormatHeader::kUInt64: {
       if (buf.size < sizeof(uint64_t)) return false;
-      auto& t = obj.value.emplace<uint64_t>();
+      auto& t = obj.MutableScalar<uint64_t>();
       std::memcpy(&t, buf.str, sizeof(uint64_t));
       buf.Advance(sizeof(uint64_t));
       return true;
     }
     case BinaryFormatHeader::kDouble: {
       if (buf.size < sizeof(double)) return false;
-      auto& t = obj.value.emplace<double>();
+      auto& t = obj.MutableScalar<double>();
       std::memcpy(&t, buf.str, sizeof(t));
       buf.Advance(sizeof(double));
       return true;
@@ -275,7 +272,7 @@
       std::memcpy(&str_size, buf.str, sizeof(str_size));
       buf.Advance(sizeof(uint64_t));
       if (buf.size < str_size) return false;
-      obj.value.emplace<std::string>() = {buf.str,
+      obj.MutableScalar<std::string>() = {buf.str,
                                           static_cast<size_t>(str_size)};
       buf.Advance(str_size);
       return true;
@@ -287,7 +284,7 @@
       buf.Advance(sizeof(vec_size));
       // This could happen for malformed inputs.
       if (vec_size > buf.size) return false;
-      auto& v = obj.value.emplace<std::vector<IRObject>>();
+      auto& v = obj.MutableSubs();
       v.reserve(vec_size);
       for (uint64_t i = 0; i < vec_size; ++i) {
         if (!BinaryParse(v.emplace_back(), buf, recursion_depth + 1))
@@ -309,26 +306,26 @@
 
 }  // namespace
 
-std::string IRObject::ToString(bool binary_format) const {
+std::string SerializeIRObject(const IRObject& obj, bool binary_format) {
   if (binary_format) {
     size_t offset = kBinaryHeader.size();
     // Determine the output size before writing to the output to avoid
     // reallocation.
-    std::visit(BinaryOutputVisitor{/*buf=*/nullptr, offset}, value);
+    obj.visit(BinaryOutputVisitor{/*buf=*/nullptr, offset});
     std::string out;
     out.resize(offset);
     std::memcpy(out.data(), kBinaryHeader.data(), kBinaryHeader.size());
     offset = kBinaryHeader.size();
-    std::visit(BinaryOutputVisitor{out.data(), offset}, value);
+    obj.visit(BinaryOutputVisitor{out.data(), offset});
     return out;
   }
   std::string out = absl::StrCat(kHeader, "\n");
-  std::visit(OutputVisitor{value.index(), 0, out}, value);
+  obj.visit(OutputVisitor{0, out});
   return out;
 }
 
 // TODO(lszekeres): Return StatusOr<IRObject>.
-std::optional<IRObject> IRObject::FromString(absl::string_view str) {
+std::optional<IRObject> ParseIRObject(absl::string_view str) {
   IRObject object;
   if (IsInBinaryFormat(str)) {
     BinaryParseBuf buf = {str.data(), str.size()};
diff --git a/fuzztest/internal/serialization.h b/fuzztest/internal/serialization.h
index 1ee1d0f..8c3c4ce 100644
--- a/fuzztest/internal/serialization.h
+++ b/fuzztest/internal/serialization.h
@@ -47,7 +47,7 @@
 template <>
 inline constexpr bool is_bytevector_v<std::vector<std::byte>> = true;
 
-struct IRObject;
+namespace no_adl {
 
 // Simple intermediate representation object and ParseInput/SerializeInput
 // functions for it.
@@ -68,19 +68,28 @@
 // is repeated, but the C++ type enforces the invariant that only one field is
 // set.
 
-struct IRObject {
+class IRObject {
+ public:
   using Value = std::variant<std::monostate, uint64_t, double, std::string,
                              std::vector<IRObject>>;
-  Value value;
-
   IRObject() = default;
   template <
       typename T,
       std::enable_if_t<std::is_enum_v<T> || std::is_integral_v<T>, int> = 0>
-  explicit IRObject(T v) : value(static_cast<uint64_t>(v)) {}
+  explicit IRObject(T v) : value_(static_cast<uint64_t>(v)) {}
   template <typename T, std::enable_if_t<std::is_floating_point_v<T>, int> = 0>
-  explicit IRObject(T v) : value(static_cast<double>(v)) {}
-  explicit IRObject(Value v) : value(std::move(v)) {}
+  explicit IRObject(T v) : value_(static_cast<double>(v)) {}
+  explicit IRObject(Value v) : value_(std::move(v)) {}
+
+  // Returns true if any value (scalar or subs) has been set, false otherwise.
+  bool HasValue() const {
+    return !std::holds_alternative<std::monostate>(value_);
+  }
+
+  // Returns true if it has subs, false otherwise.
+  bool HasSubs() const {
+    return std::holds_alternative<std::vector<IRObject>>(value_);
+  }
 
   // Accessors for scalars to simplify their use, and hide conversions when
   // needed.
@@ -92,15 +101,15 @@
       auto inner = GetScalar<std::underlying_type_t<T>>();
       return inner ? std::optional(static_cast<T>(*inner)) : std::nullopt;
     } else if constexpr (std::is_integral_v<T>) {
-      const uint64_t* i = std::get_if<uint64_t>(&value);
+      const uint64_t* i = std::get_if<uint64_t>(&value_);
       return i != nullptr ? std::optional(static_cast<T>(*i)) : std::nullopt;
     } else if constexpr (std::is_same_v<float, T> ||
                          std::is_same_v<double, T>) {
-      const double* i = std::get_if<double>(&value);
+      const double* i = std::get_if<double>(&value_);
       return i != nullptr ? std::optional(static_cast<T>(*i)) : std::nullopt;
     } else if constexpr (std::is_same_v<std::string, T>) {
       std::optional<absl::string_view> out;
-      if (const auto* s = std::get_if<std::string>(&value)) {
+      if (const auto* s = std::get_if<std::string>(&value_)) {
         out = *s;
       }
       return out;
@@ -115,25 +124,38 @@
     if constexpr (std::is_enum_v<T>) {
       SetScalar(static_cast<std::underlying_type_t<T>>(v));
     } else if constexpr (std::is_integral_v<T>) {
-      value = static_cast<uint64_t>(v);
+      value_ = static_cast<uint64_t>(v);
     } else if constexpr (std::is_same_v<float, T> ||
                          std::is_same_v<double, T>) {
-      value = static_cast<double>(v);
+      value_ = static_cast<double>(v);
     } else if constexpr (std::is_same_v<std::string, T>) {
-      value = std::move(v);
+      value_ = std::move(v);
     } else {
       static_assert(always_false<T>, "Invalid type");
     }
   }
 
+  // Sets this node have the Scalar type T, and returns a mutable reference to
+  // the value.
+  template <typename T>
+  auto& MutableScalar() {
+    if constexpr (is_monostate_v<T>) {
+      static_assert(always_false<T>, "Invalid type");
+    }
+    if (!std::holds_alternative<T>(value_)) {
+      value_.emplace<T>();
+    }
+    return std::get<T>(value_);
+  }
+
   // If this node contains subs, return it as a Span. Otherwise, nullopt.
   std::optional<absl::Span<const IRObject>> Subs() const {
-    if (const auto* i = std::get_if<std::vector<IRObject>>(&value)) {
+    if (const auto* i = std::get_if<std::vector<IRObject>>(&value_)) {
       return *i;
     }
     // The empty vector is serialized the same way as the monostate: nothing.
     // Handle that case too.
-    if (std::holds_alternative<std::monostate>(value)) {
+    if (std::holds_alternative<std::monostate>(value_)) {
       return absl::Span<const IRObject>{};
     }
     return std::nullopt;
@@ -143,10 +165,10 @@
   // them.
   // Overwrites any existing data.
   std::vector<IRObject>& MutableSubs() {
-    if (!std::holds_alternative<std::vector<IRObject>>(value)) {
-      value.emplace<std::vector<IRObject>>();
+    if (!std::holds_alternative<std::vector<IRObject>>(value_)) {
+      value_.emplace<std::vector<IRObject>>();
     }
-    return std::get<std::vector<IRObject>>(value);
+    return std::get<std::vector<IRObject>>(value_);
   }
 
   // Conversion functions to map IRObject to/from corpus values.
@@ -224,7 +246,7 @@
     if constexpr (std::is_const_v<T>) {
       return ToCorpus<std::remove_const_t<T>>();
     } else if constexpr (is_monostate_v<T>) {
-      if (std::holds_alternative<std::monostate>(value)) return T{};
+      if (std::holds_alternative<std::monostate>(value_)) return T{};
       return std::nullopt;
     } else if constexpr (std::is_same_v<T, IRObject>) {
       return *this;
@@ -252,13 +274,13 @@
       }
       return std::nullopt;
     } else if constexpr (is_protocol_buffer_v<T>) {
-      const std::string* v = std::get_if<std::string>(&value);
+      const std::string* v = std::get_if<std::string>(&value_);
       T out;
       if (v && out.ParseFromString(*v)) return out;
       return std::nullopt;
     } else if constexpr (is_dynamic_container_v<T>) {
       if constexpr (is_bytevector_v<T>) {
-        const std::string* v = std::get_if<std::string>(&value);
+        const std::string* v = std::get_if<std::string>(&value_);
         if (v) {
           T out;
           out.resize(v->size());
@@ -296,10 +318,15 @@
     }
   }
 
-  // Serialize the object as a string. This is used to persist the object on
-  // files for reproducing bugs later.
-  std::string ToString(bool binary_format = true) const;
-  static std::optional<IRObject> FromString(absl::string_view str);
+  template <typename F>
+  void visit(F&& visitor) const {
+    std::visit(std::forward<F>(visitor), value_);
+  }
+
+  template <typename F>
+  void visit(F&& visitor) {
+    std::visit(std::forward<F>(visitor), value_);
+  }
 
  private:
   template <typename T>
@@ -308,8 +335,20 @@
   template <typename K, typename V>
   static std::pair<std::remove_const_t<K>, std::remove_const_t<V>>
       RemoveConstFromPair(std::pair<K, V>);
+
+  Value value_;
 };
 
+}  // namespace no_adl
+
+using no_adl::IRObject;
+
+// Serializes `obj` as a string. This is used to persist the object on
+// files for reproducing bugs later.
+std::string SerializeIRObject(const IRObject& obj, bool binary_format = true);
+// Parses `str` and returns an IRObject, or std::nullopt if the parsing failed.
+std::optional<IRObject> ParseIRObject(absl::string_view str);
+
 }  // namespace fuzztest::internal
 
 #endif  // FUZZTEST_FUZZTEST_INTERNAL_SERIALIZATION_H_
diff --git a/fuzztest/internal/serialization_test.cc b/fuzztest/internal/serialization_test.cc
index c84da1d..3932ad9 100644
--- a/fuzztest/internal/serialization_test.cc
+++ b/fuzztest/internal/serialization_test.cc
@@ -40,26 +40,31 @@
 using ::testing::_;
 using ::testing::AllOf;
 using ::testing::ElementsAre;
+using ::testing::Eq;
 using ::testing::FieldsAre;
+using ::testing::IsFalse;
 using ::testing::NanSensitiveDoubleEq;
 using ::testing::Not;
 using ::testing::Optional;
 using ::testing::Pair;
+using ::testing::Property;
 using ::testing::StartsWith;
 using ::testing::VariantWith;
 
+auto HasNoValue() { return Property(&IRObject::HasValue, IsFalse()); }
+
 template <typename T>
 auto ValueIs(const T& v) {
   if constexpr (std::is_same_v<T, double>) {
-    return FieldsAre(VariantWith<double>(NanSensitiveDoubleEq(v)));
+    return Property(&IRObject::GetScalar<T>, Optional(NanSensitiveDoubleEq(v)));
   } else {
-    return FieldsAre(VariantWith<T>(v));
+    return Property(&IRObject::GetScalar<T>, Optional(Eq(v)));
   }
 }
 
 template <typename... T>
 auto SubsAre(const T&... v) {
-  return FieldsAre(VariantWith<std::vector<IRObject>>(ElementsAre(v...)));
+  return Property(&IRObject::Subs, Optional(ElementsAre(v...)));
 }
 
 struct VerifyVisitor {
@@ -81,7 +86,7 @@
     EXPECT_EQ(0, proto.value_case());
     ASSERT_EQ(subs.size(), proto.sub_size());
     for (int i = 0; i < subs.size(); ++i) {
-      std::visit(VerifyVisitor{proto.sub(i)}, subs[i].value);
+      subs[i].visit(VerifyVisitor{proto.sub(i)});
     }
   }
 
@@ -92,18 +97,18 @@
 };
 
 TEST(SerializerTest, HeaderIsCorrect) {
-  EXPECT_THAT(IRObject(0).ToString(/*binary_format=*/false),
+  EXPECT_THAT(SerializeIRObject(IRObject{0}, /*binary_format=*/false),
               AllOf(StartsWith("FUZZTESTv1"), Not(StartsWith("FUZZTESTv1b"))));
-  EXPECT_THAT(IRObject(0).ToString(/*binary_format=*/true),
+  EXPECT_THAT(SerializeIRObject(IRObject{0}, /*binary_format=*/true),
               StartsWith("FUZZTESTv1b"));
 }
 
 // We manually write the serialized form to test the error handling of the
 // parser. The serializer would not generate these, so we can't use it.
 TEST(SerializerTest, WrongHeaderWontParse) {
-  EXPECT_THAT(IRObject::FromString("FUZZTESTv2"), Not(Optional(_)));
-  EXPECT_THAT(IRObject::FromString("FUZZtESTv1"), Not(Optional(_)));
-  EXPECT_THAT(IRObject::FromString("-FUZZTESTv1"), Not(Optional(_)));
+  EXPECT_THAT(ParseIRObject("FUZZTESTv2"), Not(Optional(_)));
+  EXPECT_THAT(ParseIRObject("FUZZtESTv1"), Not(Optional(_)));
+  EXPECT_THAT(ParseIRObject("-FUZZTESTv1"), Not(Optional(_)));
 }
 
 TEST(SerializerTest, SubsAccesors) {
@@ -134,22 +139,24 @@
 }
 
 TEST(SerializerTest, RecursiveStructureBelowDepthLimitGetsParsed) {
-  EXPECT_TRUE(IRObject::FromString(CreateRecursiveObject(/*depth=*/100)
-                                       .ToString(/*binary_format=*/false))
-                  .has_value());
   EXPECT_TRUE(
-      IRObject::FromString(
-          CreateRecursiveObject(/*depth=*/100).ToString(/*binary_format=*/true))
+      ParseIRObject(SerializeIRObject(CreateRecursiveObject(/*depth=*/100),
+                                      /*binary_format=*/false))
+          .has_value());
+  EXPECT_TRUE(
+      ParseIRObject(SerializeIRObject(CreateRecursiveObject(/*depth=*/100),
+                                      /*binary_format=*/true))
           .has_value());
 }
 
 TEST(SerializerTest, RecursiveStructureAboveDepthLimitDoesNotGetParsed) {
-  EXPECT_FALSE(IRObject::FromString(CreateRecursiveObject(/*depth=*/150)
-                                        .ToString(/*binary_format=*/false))
-                   .has_value());
   EXPECT_FALSE(
-      IRObject::FromString(
-          CreateRecursiveObject(/*depth=*/150).ToString(/*binary_format=*/true))
+      ParseIRObject(SerializeIRObject(CreateRecursiveObject(/*depth=*/150),
+                                      /*binary_format=*/false))
+          .has_value());
+  EXPECT_FALSE(
+      ParseIRObject(SerializeIRObject(CreateRecursiveObject(/*depth=*/150),
+                                      /*binary_format=*/true))
           .has_value());
 }
 
@@ -162,26 +169,25 @@
 
 void VerifyProtobufFormat(const IRObject& object) {
   IRObjectTestProto proto;
-  std::string s = object.ToString(/*binary_format=*/false);
+  std::string s = SerializeIRObject(object, /*binary_format=*/false);
   // Chop the header.
   s.erase(0, strlen("FUZZTESTv1\n"));
 
   ASSERT_TRUE(google::protobuf::TextFormat::ParseFromString(s, &proto));
-  std::visit(VerifyVisitor{proto}, object.value);
+  object.visit(VerifyVisitor{proto});
 }
 
 template <typename... T>
 void RoundTripVerify(bool binary_format, const T&... values) {
   IRObject object;
-  object.value = std::vector{IRObject{values}...};
-  std::string s = object.ToString(binary_format);
+  object.MutableSubs() = std::vector{IRObject{values}...};
+  std::string s = SerializeIRObject(object, binary_format);
 
   SCOPED_TRACE(s);
 
   if (!binary_format) VerifyProtobufFormat(object);
 
-  EXPECT_THAT(IRObject::FromString(s),
-              Optional(SubsAre(ValueIs<T>(values)...)));
+  EXPECT_THAT(ParseIRObject(s), Optional(SubsAre(ValueIs<T>(values)...)));
 }
 
 template <typename T>
@@ -205,13 +211,13 @@
                                                      IRObject{"child3.2.2"}}}}},
       IRObject{"child4"}}};
 
-  std::string s = root.ToString(GetParam().binary_format);
+  std::string s = SerializeIRObject(root, GetParam().binary_format);
 
   SCOPED_TRACE(s);
 
   if (!GetParam().binary_format) VerifyProtobufFormat(root);
 
-  std::optional<IRObject> obj = IRObject::FromString(s);
+  std::optional<IRObject> obj = ParseIRObject(s);
   EXPECT_THAT(
       obj, Optional(SubsAre(
                ValueIs<std::string>("child1"), ValueIs<std::string>("child2"),
@@ -222,9 +228,9 @@
 }
 
 TEST_P(SerializerRoundTripTest, EmptyObjectRoundTrips) {
-  std::string s = IRObject{}.ToString(GetParam().binary_format);
+  std::string s = SerializeIRObject(IRObject{}, GetParam().binary_format);
   SCOPED_TRACE(s);
-  EXPECT_THAT(IRObject::FromString(s), Optional(ValueIs<std::monostate>({})));
+  EXPECT_THAT(ParseIRObject(s), Optional(HasNoValue()));
 }
 
 template <typename T>
@@ -235,7 +241,7 @@
   obj.SetScalar(value);
   EXPECT_THAT(obj.GetScalar<T>(), Optional(value));
 
-  auto roundtrip = IRObject::FromString(obj.ToString(binary_format));
+  auto roundtrip = ParseIRObject(SerializeIRObject(obj, binary_format));
   EXPECT_THAT(obj.GetScalar<T>(), Optional(value));
 }
 
@@ -272,7 +278,7 @@
                                            IRObject{uint64_t{322}}}}}},
                   IRObject{uint64_t{4}}}};
 
-  std::string s = root.ToString(/*binary_format=*/false);
+  std::string s = SerializeIRObject(root, /*binary_format=*/false);
 
   EXPECT_EQ(s, R"(FUZZTESTv1
 sub { i: 1 }
@@ -285,43 +291,42 @@
   }
 }
 sub { i: 4 }
-)");
+)") << s;
 }
 
 TEST(TextFormatSerializerTest, HandlesUnterminatedString) {
-  EXPECT_THAT(IRObject::FromString("FUZZTESTv1\""), Not(Optional(_)));
+  EXPECT_THAT(ParseIRObject("FUZZTESTv1\""), Not(Optional(_)));
 }
 
 TEST(TextFormatSerializerTest, BadScalarWontParse) {
-  EXPECT_THAT(IRObject::FromString("FUZZTESTv1 i: 1"),
-              Optional(ValueIs<uint64_t>(1)));
+  EXPECT_THAT(ParseIRObject("FUZZTESTv1 i: 1"), Optional(ValueIs<uint64_t>(1)));
   // Out of bounds values
-  EXPECT_THAT(IRObject::FromString("FUZZTESTv1 i: 123456789012345678901"),
+  EXPECT_THAT(ParseIRObject("FUZZTESTv1 i: 123456789012345678901"),
               Not(Optional(_)));
-  EXPECT_THAT(IRObject::FromString("FUZZTESTv1 i: -1"), Not(Optional(_)));
+  EXPECT_THAT(ParseIRObject("FUZZTESTv1 i: -1"), Not(Optional(_)));
   // Missing :
-  EXPECT_THAT(IRObject::FromString("FUZZTESTv1 i 1"), Not(Optional(_)));
+  EXPECT_THAT(ParseIRObject("FUZZTESTv1 i 1"), Not(Optional(_)));
   // Bad tag
-  EXPECT_THAT(IRObject::FromString("FUZZTESTv1 x: 1"), Not(Optional(_)));
+  EXPECT_THAT(ParseIRObject("FUZZTESTv1 x: 1"), Not(Optional(_)));
   // Wrong separator
-  EXPECT_THAT(IRObject::FromString("FUZZTESTv1 i; 1"), Not(Optional(_)));
+  EXPECT_THAT(ParseIRObject("FUZZTESTv1 i; 1"), Not(Optional(_)));
   // Extra close
-  EXPECT_THAT(IRObject::FromString("FUZZTESTv1 i: 1}"), Not(Optional(_)));
+  EXPECT_THAT(ParseIRObject("FUZZTESTv1 i: 1}"), Not(Optional(_)));
 }
 
 TEST(TextFormatSerializerTest, BadSubWontParse) {
-  EXPECT_THAT(IRObject::FromString("FUZZTESTv1 sub { i: 0 }"),
+  EXPECT_THAT(ParseIRObject("FUZZTESTv1 sub { i: 0 }"),
               Optional(SubsAre(ValueIs<uint64_t>(0))));
-  EXPECT_THAT(IRObject::FromString("FUZZTESTv1 sub: { }"), Not(Optional(_)));
-  EXPECT_THAT(IRObject::FromString("FUZZTESTv1 sub  }"), Not(Optional(_)));
-  EXPECT_THAT(IRObject::FromString("FUZZTESTv1 sub { "), Not(Optional(_)));
-  EXPECT_THAT(IRObject::FromString("FUZZTESTv1 sub { } }"), Not(Optional(_)));
+  EXPECT_THAT(ParseIRObject("FUZZTESTv1 sub: { }"), Not(Optional(_)));
+  EXPECT_THAT(ParseIRObject("FUZZTESTv1 sub  }"), Not(Optional(_)));
+  EXPECT_THAT(ParseIRObject("FUZZTESTv1 sub { "), Not(Optional(_)));
+  EXPECT_THAT(ParseIRObject("FUZZTESTv1 sub { } }"), Not(Optional(_)));
 }
 
 TEST(TextFormatSerializerTest, ExtraWhitespaceIsFine) {
-  EXPECT_THAT(IRObject::FromString("FUZZTESTv1 i: 0 \n "),
+  EXPECT_THAT(ParseIRObject("FUZZTESTv1 i: 0 \n "),
               Optional(ValueIs<uint64_t>(0)));
-  EXPECT_THAT(IRObject::FromString("FUZZTESTv1 sub {   \n i:   0 \n}  \n "),
+  EXPECT_THAT(ParseIRObject("FUZZTESTv1 sub {   \n i:   0 \n}  \n "),
               Optional(SubsAre(ValueIs<uint64_t>(0))));
 }
 
@@ -330,11 +335,10 @@
       "FUZZTESTv1b\x04\x01\x00\x00\x00\x00\x00\x00\x00\x00";
   static constexpr char kBadInput[] =
       "FUZZTESTv1b\x04\xff\xff\xff\xff\xff\xff\xff\xff\x00";
-  EXPECT_THAT(IRObject::FromString({kGoodInput, sizeof(kGoodInput) - 1}),
-              Optional(SubsAre(ValueIs<std::monostate>({}))));
+  EXPECT_THAT(ParseIRObject({kGoodInput, sizeof(kGoodInput) - 1}),
+              Optional(SubsAre(HasNoValue())));
   // Expect grace failure instead of OOM error.
-  EXPECT_EQ(IRObject::FromString({kBadInput, sizeof(kBadInput) - 1}),
-            std::nullopt);
+  EXPECT_EQ(ParseIRObject({kBadInput, sizeof(kBadInput) - 1}), std::nullopt);
 }
 
 TEST(IRToCorpus, SpecializationIsBackwardCompatible) {
@@ -423,9 +427,7 @@
   EXPECT_THAT(obj.GetScalar<int>(), Optional(1979));
   obj.MutableSubs().emplace_back("ABC");
   obj = round_trip(obj).value();
-  EXPECT_THAT(
-      obj.Subs(),
-      Optional(ElementsAre(FieldsAre(VariantWith<std::string>("ABC")))));
+  EXPECT_THAT(obj.Subs(), Optional(ElementsAre(ValueIs<std::string>("ABC"))));
 }
 
 TEST(CorpusToIR, FailureConditions) {
@@ -438,7 +440,7 @@
     using V = std::variant<int, std::string>;
     // Valid index, but bad value.
     IRObject var = IRObject::FromCorpus(V(2));
-    auto& v = std::get<std::vector<IRObject>>(var.value);
+    auto& v = var.MutableSubs();
     EXPECT_THAT(v[0].GetScalar<int>(), Optional(0));
     v[0] = IRObject(1);
     EXPECT_FALSE(var.ToCorpus<V>());