Repository: WebAssembly/binaryen
Pull request #7373: [Strings] Make string a subtype of ext, not any
Author: login: kripken
Created: 2025-03-13T23:51:44Z | Merged: 2025-03-20T16:49:39Z

StringLowering converts strings to externs, which makes sense as we lower
stringrefs to imported JS strings. For the reverse transform it is convenient
to just have strings be subtypes of ext, see #7370 - that makes it simple to
switch stringref to externref and vice versa.


Diff:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5a8ce1af093..6e63e9ee5ae 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,6 +18,9 @@ Current Trunk
  - Add an option to preserve imports and exports in the fuzzer (for fuzzer
    harnesses where they only want Binaryen to modify their given testcases, not
    generate new things in them).
+ - `string` is now a subtype of `ext` (rather than `any`). This allows better
+   transformations for strings, like an inverse of StringLowering, but will
+   error on codebases that depend on being able to pass strings into anyrefs.
 
 v122
 ----
diff --git a/README.md b/README.md
index e634491229b..19334a96662 100644
--- a/README.md
+++ b/README.md
@@ -105,6 +105,13 @@ There are a few differences between Binaryen IR and the WebAssembly language:
      Binaryen IR is more structured than WebAssembly as noted earlier). Note
      that Binaryen does support unreachable code in .wat text files, since as we
      saw Binaryen only supports s-expressions there, which are structured.
+   * Binaryen supports a `stringref` type. This is similar to the currently-
+     frozen [stringref proposal], with the difference that the string type is a
+     subtype of `externref` rather than `anyref`. Doing so allows toolchains to
+     emit code in a form that uses [js string builtins] which Binaryen can then
+     "lift" into stringref in its internal IR, optimize (for example, a
+     concatenation of "a" and "b" can be optimized at compile time to "ab"), and
+     then "lower" that into js string builtins once more.
  * Blocks
    * Binaryen IR has only one node that contains a variable-length list of
      operands: the block. WebAssembly on the other hand allows lists in loops,
@@ -1039,3 +1046,5 @@ Windows and OS X as most of the core devs are on Linux.
 [minification]: https://kripken.github.io/talks/binaryen.html#/2
 [unreachable]: https://github.com/WebAssembly/binaryen/issues/903
 [binaryen_ir]: https://github.com/WebAssembly/binaryen/issues/663
+[stringref proposal]: https://github.com/WebAssembly/stringref/blob/main/proposals/stringref/Overview.md
+[js string builtins]: https://github.com/WebAssembly/js-string-builtins/blob/main/proposals/js-string-builtins/Overview.md
diff --git a/scripts/bundle_clusterfuzz.py b/scripts/bundle_clusterfuzz.py
index 65714340a5e..0dee8c95157 100755
--- a/scripts/bundle_clusterfuzz.py
+++ b/scripts/bundle_clusterfuzz.py
@@ -107,6 +107,7 @@
     '--disable-shared-everything',
     '--disable-fp16',
     '--disable-custom-descriptors',
+    '--disable-strings',
 ]
 
 with tarfile.open(output_file, "w:gz") as tar:
diff --git a/scripts/clusterfuzz/run.py b/scripts/clusterfuzz/run.py
index e3b59234e7f..4e7bf6659f0 100755
--- a/scripts/clusterfuzz/run.py
+++ b/scripts/clusterfuzz/run.py
@@ -88,6 +88,7 @@
     '--disable-shared-everything',
     '--disable-fp16',
     '--disable-custom-descriptors',
+    '--disable-strings',
 ]
 
 
diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py
index e12dc809bc5..e77b755e2e1 100755
--- a/scripts/fuzz_opt.py
+++ b/scripts/fuzz_opt.py
@@ -151,10 +151,12 @@ def randomize_feature_opts():
 
         # The shared-everything feature is new and we want to fuzz it, but it
         # also currently disables fuzzing V8, so disable it most of the time.
-        # Same with custom descriptors.
+        # Same with custom descriptors and strings - all these cannot be run in
+        # V8 for now.
         if random.random() < 0.9:
             FEATURE_OPTS.append('--disable-shared-everything')
             FEATURE_OPTS.append('--disable-custom-descriptors')
+            FEATURE_OPTS.append('--disable-strings')
 
     print('randomized feature opts:', '\n  ' + '\n  '.join(FEATURE_OPTS))
 
@@ -815,8 +817,9 @@ def run(self, wasm, extra_d8_flags=[]):
             def can_run(self, wasm):
                 # V8 does not support shared memories when running with
                 # shared-everything enabled, so do not fuzz shared-everything
-                # for now. It also does not yet support custom descriptors.
-                return all_disallowed(['shared-everything', 'custom-descriptors'])
+                # for now. It also does not yet support custom descriptors, nor
+                # strings.
+                return all_disallowed(['shared-everything', 'custom-descriptors', 'strings'])
 
             def can_compare_to_self(self):
                 # With nans, VM differences can confuse us, so only very simple VMs
@@ -1573,7 +1576,7 @@ def can_run_on_wasm(self, wasm):
             return False
 
         # see D8.can_run
-        return all_disallowed(['shared-everything', 'custom-descriptors'])
+        return all_disallowed(['shared-everything', 'custom-descriptors', 'strings'])
 
 
 # Check that the text format round-trips without error.
@@ -1758,7 +1761,7 @@ def can_run_on_wasm(self, wasm):
             return False
         if NANS:
             return False
-        return all_disallowed(['shared-everything', 'custom-descriptors'])
+        return all_disallowed(['shared-everything', 'custom-descriptors', 'strings'])
 
 
 # Test --fuzz-preserve-imports-exports, which never modifies imports or exports.
diff --git a/src/literal.h b/src/literal.h
index 91bbacbfa3c..4f9e3d535ad 100644
--- a/src/literal.h
+++ b/src/literal.h
@@ -57,6 +57,9 @@ class Literal {
     // Externalized i31 references have a gcData containing the internal i31
     // reference as its sole value even though internal i31 references do not
     // have a gcData.
+    //
+    // Note that strings can be internalized, in which case they keep the same
+    // gcData, but their type becomes anyref.
     std::shared_ptr<GCData> gcData;
     // A reference to Exn data.
     std::shared_ptr<ExnData> exnData;
diff --git a/src/tools/execution-results.h b/src/tools/execution-results.h
index f0b31cdf7db..fe96fc55771 100644
--- a/src/tools/execution-results.h
+++ b/src/tools/execution-results.h
@@ -303,11 +303,19 @@ struct ExecutionResults {
   }
 
   void printValue(Literal value) {
-    // Unwrap an externalized value to get the actual value.
-    if (Type::isSubType(value.type, Type(HeapType::ext, Nullable))) {
+    // Unwrap an externalized GC value to get the actual value, but not strings,
+    // which are normally a subtype of ext.
+    if (Type::isSubType(value.type, Type(HeapType::ext, Nullable)) &&
+        !value.type.isString()) {
       value = value.internalize();
     }
 
+    // An anyref literal is a string.
+    if (value.type.isRef() &&
+        value.type.getHeapType().isMaybeShared(HeapType::any)) {
+      value = value.externalize();
+    }
+
     // Don't print most reference values, as e.g. funcref(N) contains an index,
     // which is not guaranteed to remain identical after optimizations. Do not
     // print the type in detail (as even that may change due to closed-world
diff --git a/src/tools/fuzzing/fuzzing.cpp b/src/tools/fuzzing/fuzzing.cpp
index 2f8910db8cf..c72b239bda2 100644
--- a/src/tools/fuzzing/fuzzing.cpp
+++ b/src/tools/fuzzing/fuzzing.cpp
@@ -3399,6 +3399,10 @@ Expression* TranslateToFuzzReader::makeBasicRef(Type type) {
   auto share = heapType.getShared();
   switch (heapType.getBasic(Unshared)) {
     case HeapType::ext: {
+      if (wasm.features.hasStrings() && share == Unshared && oneIn(2)) {
+        // Shared strings not yet supported.
+        return makeConst(Type(HeapType::string, NonNullable));
+      }
       auto null = builder.makeRefNull(HeapTypes::ext.getBasic(share));
       // TODO: support actual non-nullable externrefs via imported globals or
       // similar.
@@ -3429,10 +3433,6 @@ Expression* TranslateToFuzzReader::makeBasicRef(Type type) {
         HeapType::i31,
         HeapType::struct_,
         HeapType::array);
-      if (share == Unshared) {
-        // Shared strings not yet supported.
-        subtypeOpts.add(FeatureSet::Strings, HeapType::string);
-      }
       auto subtype = pick(subtypeOpts).getBasic(share);
       return makeConst(Type(subtype, nullability));
     }
@@ -5376,11 +5376,16 @@ HeapType TranslateToFuzzReader::getSubType(HeapType type) {
           .getBasic(share);
       case HeapType::cont:
         return pick(HeapTypes::cont, HeapTypes::nocont).getBasic(share);
-      case HeapType::ext:
-        return pick(FeatureOptions<HeapType>()
-                      .add(FeatureSet::ReferenceTypes, HeapType::ext)
-                      .add(FeatureSet::GC, HeapType::noext))
-          .getBasic(share);
+      case HeapType::ext: {
+        auto options = FeatureOptions<HeapType>()
+                         .add(FeatureSet::ReferenceTypes, HeapType::ext)
+                         .add(FeatureSet::GC, HeapType::noext);
+        if (share == Unshared) {
+          // Shared strings not yet supported.
+          options.add(FeatureSet::Strings, HeapType::string);
+        }
+        return pick(options).getBasic(share);
+      }
       case HeapType::any: {
         assert(wasm.features.hasReferenceTypes());
         assert(wasm.features.hasGC());
@@ -5391,10 +5396,6 @@ HeapType TranslateToFuzzReader::getSubType(HeapType type) {
                                                       HeapType::struct_,
                                                       HeapType::array,
                                                       HeapType::none);
-        if (share == Unshared) {
-          // Shared strings not yet supported.
-          options.add(FeatureSet::Strings, HeapType::string);
-        }
         return pick(options).getBasic(share);
       }
       case HeapType::eq:
diff --git a/src/tools/fuzzing/heap-types.cpp b/src/tools/fuzzing/heap-types.cpp
index dc6e574c7e9..f5a6717ceb3 100644
--- a/src/tools/fuzzing/heap-types.cpp
+++ b/src/tools/fuzzing/heap-types.cpp
@@ -510,7 +510,7 @@ struct HeapTypeGeneratorImpl {
         candidates.push_back(HeapTypes::any.getBasic(share));
         break;
       case HeapType::string:
-        candidates.push_back(HeapTypes::any.getBasic(share));
+        candidates.push_back(HeapTypes::ext.getBasic(share));
         break;
       case HeapType::none:
         return pickSubAny(share);
diff --git a/src/wasm/literal.cpp b/src/wasm/literal.cpp
index d1fe5cfcc03..de95fe565bf 100644
--- a/src/wasm/literal.cpp
+++ b/src/wasm/literal.cpp
@@ -76,10 +76,13 @@ Literal::Literal(std::shared_ptr<GCData> gcData, HeapType type)
     type(type, gcData ? NonNullable : Nullable, gcData ? Inexact : Exact) {
   // TODO: Use exact types for more than just nulls.
   // The type must be a proper type for GC data: either a struct, array, or
-  // string; or an externalized version of the same; or a null.
+  // string; or an externalized version of the same; or a null; or an
+  // internalized string (which appears as an anyref).
   assert((isData() && gcData) ||
          (type.isMaybeShared(HeapType::ext) && gcData) ||
-         (type.isBottom() && !gcData));
+         (type.isBottom() && !gcData) ||
+         (type.isMaybeShared(HeapType::any) && gcData &&
+          gcData->type.isMaybeShared(HeapType::string)));
 }
 
 Literal::Literal(std::shared_ptr<ExnData> exnData)
@@ -153,6 +156,11 @@ Literal::Literal(const Literal& other) : type(other.type) {
     case HeapType::nocont:
       WASM_UNREACHABLE("null literals should already have been handled");
     case HeapType::any:
+      // This must be an anyref literal, which is an internalized string.
+      assert(other.gcData &&
+             other.gcData->type.isMaybeShared(HeapType::string));
+      new (&gcData) std::shared_ptr<GCData>(other.gcData);
+      return;
     case HeapType::eq:
     case HeapType::func:
     case HeapType::cont:
@@ -169,7 +177,8 @@ Literal::~Literal() {
   if (type.isBasic()) {
     return;
   }
-  if (isNull() || isData() || type.getHeapType().isMaybeShared(HeapType::ext)) {
+  if (isNull() || isData() || type.getHeapType().isMaybeShared(HeapType::ext) ||
+      type.getHeapType().isMaybeShared(HeapType::any)) {
     gcData.~shared_ptr();
   } else if (isExn()) {
     exnData.~shared_ptr();
@@ -652,13 +661,14 @@ std::ostream& operator<<(std::ostream& o, Literal literal) {
         case HeapType::exn:
           o << "exnref";
           break;
-        case HeapType::any:
         case HeapType::eq:
         case HeapType::func:
         case HeapType::cont:
         case HeapType::struct_:
         case HeapType::array:
           WASM_UNREACHABLE("invalid type");
+        case HeapType::any:
+          // Anyref literals contain strings.
         case HeapType::string: {
           auto data = literal.getGCData();
           if (!data) {
@@ -2868,6 +2878,11 @@ Literal Literal::externalize() const {
     return Literal(std::make_shared<GCData>(heapType, Literals{*this}),
                    extType);
   }
+  if (heapType.isMaybeShared(HeapType::any)) {
+    // Anyref literals turn into strings (if we add any other anyref literals,
+    // we will need to be more careful here).
+    return Literal(gcData, HeapTypes::string.getBasic(share));
+  }
   return Literal(gcData, extType);
 }
 
@@ -2883,6 +2898,10 @@ Literal Literal::internalize() const {
     assert(gcData->values[0].type.getHeapType().isMaybeShared(HeapType::i31));
     return gcData->values[0];
   }
+  if (gcData->type.isMaybeShared(HeapType::string)) {
+    // Strings turn into anyref literals.
+    return Literal(gcData, HeapTypes::any.getBasic(share));
+  }
   return Literal(gcData, gcData->type);
 }
 
diff --git a/src/wasm/wasm-type.cpp b/src/wasm/wasm-type.cpp
index b7cacd3e69a..b3ffd216351 100644
--- a/src/wasm/wasm-type.cpp
+++ b/src/wasm/wasm-type.cpp
@@ -407,6 +407,11 @@ std::optional<HeapType> getBasicHeapTypeLUB(HeapType::BasicHeapType a,
   HeapType lubUnshared;
   switch (HeapType(a).getBasic(Unshared)) {
     case HeapType::ext:
+      if (bUnshared != HeapType::string) {
+        return std::nullopt;
+      }
+      lubUnshared = HeapType::ext;
+      break;
     case HeapType::func:
     case HeapType::cont:
     case HeapType::exn:
@@ -437,9 +442,14 @@ std::optional<HeapType> getBasicHeapTypeLUB(HeapType::BasicHeapType a,
       }
       break;
     case HeapType::array:
-    case HeapType::string:
       lubUnshared = HeapType::any;
       break;
+    case HeapType::string:
+      // String has already been handled: we sorted before in a way that ensures
+      // the type the string is compared to is of a higher index, which means it
+      // is a bottom type (string is the last type that is not a bottom type),
+      // but we have handled the case of either a or b being a bottom type
+      // earlier already.
     case HeapType::none:
     case HeapType::noext:
     case HeapType::nofunc:
@@ -953,8 +963,9 @@ std::optional<HeapType> HeapType::getSuperType() const {
       case none:
       case exn:
       case noexn:
-      case string:
         return {};
+      case string:
+        return HeapType(ext).getBasic(share);
       case eq:
         return HeapType(any).getBasic(share);
       case i31:
@@ -1021,12 +1032,12 @@ size_t HeapType::getDepth() const {
         case HeapType::exn:
           break;
         case HeapType::eq:
+        case HeapType::string:
           depth++;
           break;
         case HeapType::i31:
         case HeapType::struct_:
         case HeapType::array:
-        case HeapType::string:
           depth += 2;
           break;
         case HeapType::none:
@@ -1070,9 +1081,9 @@ HeapType::BasicHeapType HeapType::getUnsharedBottom() const {
       case i31:
       case struct_:
       case array:
-      case string:
       case none:
         return none;
+      case string:
       case noext:
         return noext;
       case nofunc:
@@ -1530,8 +1541,9 @@ bool SubTyper::isSubType(HeapType a, HeapType b) {
                aUnshared == HeapType::struct_ || aUnshared == HeapType::array ||
                a.isStruct() || a.isArray();
       case HeapType::i31:
-      case HeapType::string:
         return aUnshared == HeapType::none;
+      case HeapType::string:
+        return aUnshared == HeapType::noext;
       case HeapType::struct_:
         return aUnshared == HeapType::none || a.isStruct();
       case HeapType::array:
diff --git a/test/gtest/type-builder.cpp b/test/gtest/type-builder.cpp
index 39d388064ee..f75639fa3d2 100644
--- a/test/gtest/type-builder.cpp
+++ b/test/gtest/type-builder.cpp
@@ -706,7 +706,7 @@ TEST_F(TypeTest, TestHeapTypeRelations) {
   assertLUB(ext, i31, {});
   assertLUB(ext, struct_, {});
   assertLUB(ext, array, {});
-  assertLUB(ext, string, {});
+  assertLUB(ext, string, ext);
   assertLUB(ext, none, {});
   assertLUB(ext, noext, ext);
   assertLUB(ext, nofunc, {});
@@ -779,7 +779,7 @@ TEST_F(TypeTest, TestHeapTypeRelations) {
   assertLUB(any, i31, any);
   assertLUB(any, struct_, any);
   assertLUB(any, array, any);
-  assertLUB(any, string, any);
+  assertLUB(any, string, {});
   assertLUB(any, none, any);
   assertLUB(any, noext, {});
   assertLUB(any, nofunc, {});
@@ -802,7 +802,7 @@ TEST_F(TypeTest, TestHeapTypeRelations) {
   assertLUB(eq, i31, eq);
   assertLUB(eq, struct_, eq);
   assertLUB(eq, array, eq);
-  assertLUB(eq, string, any);
+  assertLUB(eq, string, {});
   assertLUB(eq, none, eq);
   assertLUB(eq, noext, {});
   assertLUB(eq, nofunc, {});
@@ -824,7 +824,7 @@ TEST_F(TypeTest, TestHeapTypeRelations) {
   assertLUB(i31, cont, {});
   assertLUB(i31, struct_, eq);
   assertLUB(i31, array, eq);
-  assertLUB(i31, string, any);
+  assertLUB(i31, string, {});
   assertLUB(i31, none, i31);
   assertLUB(i31, noext, {});
   assertLUB(i31, nofunc, {});
@@ -845,7 +845,7 @@ TEST_F(TypeTest, TestHeapTypeRelations) {
   assertLUB(struct_, struct_, struct_);
   assertLUB(struct_, cont, {});
   assertLUB(struct_, array, eq);
-  assertLUB(struct_, string, any);
+  assertLUB(struct_, string, {});
   assertLUB(struct_, none, struct_);
   assertLUB(struct_, noext, {});
   assertLUB(struct_, nofunc, {});
@@ -865,7 +865,7 @@ TEST_F(TypeTest, TestHeapTypeRelations) {
 
   assertLUB(array, array, array);
   assertLUB(array, cont, {});
-  assertLUB(array, string, any);
+  assertLUB(array, string, {});
   assertLUB(array, none, array);
   assertLUB(array, noext, {});
   assertLUB(array, nofunc, {});
@@ -885,14 +885,14 @@ TEST_F(TypeTest, TestHeapTypeRelations) {
 
   assertLUB(string, string, string);
   assertLUB(string, cont, {});
-  assertLUB(string, none, string);
-  assertLUB(string, noext, {});
+  assertLUB(string, none, {});
+  assertLUB(string, noext, string);
   assertLUB(string, nofunc, {});
   assertLUB(string, nocont, {});
   assertLUB(string, defFunc, {});
   assertLUB(string, defCont, {});
-  assertLUB(string, defStruct, any);
-  assertLUB(string, defArray, any);
+  assertLUB(string, defStruct, {});
+  assertLUB(string, defArray, {});
   assertLUB(string, sharedAny, {});
   assertLUB(string, sharedEq, {});
   assertLUB(string, sharedI31, {});
@@ -1650,7 +1650,7 @@ TEST_F(TypeTest, TestDepth) {
   EXPECT_EQ(HeapTypes::ext.getDepth(), 0U);
 
   EXPECT_EQ(HeapTypes::i31.getDepth(), 2U);
-  EXPECT_EQ(HeapTypes::string.getDepth(), 2U);
+  EXPECT_EQ(HeapTypes::string.getDepth(), 1U);
 
   EXPECT_EQ(HeapTypes::none.getDepth(), size_t(-1));
   EXPECT_EQ(HeapTypes::nofunc.getDepth(), size_t(-1));
@@ -1731,7 +1731,7 @@ TEST_F(TypeTest, TestSupertypes) {
   ASSERT_EQ(HeapTypes::i31.getSuperType(), HeapType::eq);
   ASSERT_EQ(HeapTypes::struct_.getSuperType(), HeapType::eq);
   ASSERT_EQ(HeapTypes::array.getSuperType(), HeapType::eq);
-  ASSERT_FALSE(HeapTypes::string.getSuperType());
+  ASSERT_EQ(HeapTypes::string.getSuperType(), HeapType::ext);
   ASSERT_FALSE(HeapTypes::none.getSuperType());
   ASSERT_FALSE(HeapTypes::noext.getSuperType());
   ASSERT_FALSE(HeapTypes::nofunc.getSuperType());
diff --git a/test/gtest/type-domains.cpp b/test/gtest/type-domains.cpp
index 038a09a2460..deb9d0a1dad 100644
--- a/test/gtest/type-domains.cpp
+++ b/test/gtest/type-domains.cpp
@@ -486,8 +486,10 @@ fuzztest::Domain<HeapTypePlan> AvailableStrictSubHeapType(TypeBuilderPlan plan,
 
     switch (type->getBasic(Unshared)) {
       case HeapType::ext:
-        return fuzztest::Just(
-          HeapTypePlan{HeapType(HeapTypes::noext.getBasic(share))});
+        return matchingOrAbstract(
+          [](auto kind) { return false; },
+          fuzztest::ElementOf({HeapType(HeapTypes::string.getBasic(share)),
+                               HeapType(HeapTypes::noext.getBasic(share))}));
       case HeapType::func:
         return matchingOrAbstract(
           [](auto kind) { return kind == FuncKind; },
@@ -501,7 +503,6 @@ fuzztest::Domain<HeapTypePlan> AvailableStrictSubHeapType(TypeBuilderPlan plan,
           [](auto kind) { return kind == StructKind || kind == ArrayKind; },
           fuzztest::ElementOf({HeapType(HeapTypes::eq.getBasic(share)),
                                HeapType(HeapTypes::i31.getBasic(share)),
-                               HeapType(HeapTypes::string.getBasic(share)),
                                HeapType(HeapTypes::struct_.getBasic(share)),
                                HeapType(HeapTypes::array.getBasic(share)),
                                HeapType(HeapTypes::none.getBasic(share))}));
@@ -523,10 +524,12 @@ fuzztest::Domain<HeapTypePlan> AvailableStrictSubHeapType(TypeBuilderPlan plan,
       case HeapType::exn:
         return fuzztest::Just(
           HeapTypePlan{HeapType(HeapTypes::noexn.getBasic(share))});
-      case HeapType::string:
       case HeapType::i31:
         return fuzztest::Just(
           HeapTypePlan{HeapType(HeapTypes::none.getBasic(share))});
+      case HeapType::string:
+        return fuzztest::Just(
+          HeapTypePlan{HeapType(HeapTypes::noext.getBasic(share))});
       case HeapType::none:
       case HeapType::noext:
       case HeapType::nofunc:
@@ -594,16 +597,17 @@ AvailableStrictSuperHeapType(TypeBuilderPlan plan, HeapTypePlan sub) {
       case HeapType::i31:
       case HeapType::struct_:
       case HeapType::array:
-      case HeapType::string:
         return fuzztest::Just(
           HeapTypePlan{HeapType(HeapTypes::any.getBasic(share))});
+      case HeapType::string:
+        return fuzztest::Just(
+          HeapTypePlan{HeapType(HeapTypes::ext.getBasic(share))});
       case HeapType::none:
         return matchingOrAbstract(
           [](auto kind) { return kind == StructKind || kind == ArrayKind; },
           fuzztest::ElementOf({HeapType(HeapTypes::any.getBasic(share)),
                                HeapType(HeapTypes::eq.getBasic(share)),
                                HeapType(HeapTypes::i31.getBasic(share)),
-                               HeapType(HeapTypes::string.getBasic(share)),
                                HeapType(HeapTypes::struct_.getBasic(share)),
                                HeapType(HeapTypes::array.getBasic(share))}));
       case HeapType::noext:
@@ -1043,7 +1047,7 @@ std::vector<HeapType> BuildHeapTypes(TypeBuilderPlan plan) {
   auto built = builder.build();
   if (auto* err = built.getError()) {
     std::cerr << err->index << ": " << err->reason << "\n";
-    ;
+    std::cerr << "Plan: " << plan << '\n';
   }
   assert(built);
   return *built;
diff --git a/test/lit/ctor-eval/string.wast b/test/lit/ctor-eval/string.wast
index 045c6eec01d..d1e254f53c1 100644
--- a/test/lit/ctor-eval/string.wast
+++ b/test/lit/ctor-eval/string.wast
@@ -9,14 +9,14 @@
 
  (export "test" (func $test))
 
- (func $test (result anyref)
+ (func $test (result externref)
   (global.get $global)
  )
 )
-;; CHECK:      (type $0 (func (result anyref)))
+;; CHECK:      (type $0 (func (result externref)))
 
 ;; CHECK:      (export "test" (func $test_1))
 
-;; CHECK:      (func $test_1 (type $0) (result anyref)
+;; CHECK:      (func $test_1 (type $0) (result externref)
 ;; CHECK-NEXT:  (string.const "one")
 ;; CHECK-NEXT: )
diff --git a/test/lit/exec/strings.wast b/test/lit/exec/strings.wast
index f4ba59844d8..e9dd9fd2d0b 100644
--- a/test/lit/exec/strings.wast
+++ b/test/lit/exec/strings.wast
@@ -477,20 +477,20 @@
     )
   )
 
-  ;; CHECK:      [fuzz-exec] calling extern
-  ;; CHECK-NEXT: [fuzz-exec] note result: extern => string("string")
-  (func $extern (export "extern") (result externref)
-    (extern.convert_any
-      (string.const "string")
+  ;; CHECK:      [fuzz-exec] calling string.to.any
+  ;; CHECK-NEXT: [fuzz-exec] note result: string.to.any => string("six")
+  (func $string.to.any (export "string.to.any") (result anyref)
+    (any.convert_extern
+      (string.const "six")
     )
   )
 
-  ;; CHECK:      [fuzz-exec] calling extern-intern
-  ;; CHECK-NEXT: [fuzz-exec] note result: extern-intern => string("string")
-  (func $extern-intern (export "extern-intern") (result anyref)
-    (any.convert_extern
-      (extern.convert_any
-        (string.const "string")
+  ;; CHECK:      [fuzz-exec] calling string.to.any.and.back
+  ;; CHECK-NEXT: [fuzz-exec] note result: string.to.any.and.back => string("seven")
+  (func $string.to.any.and.back (export "string.to.any.and.back") (result externref)
+    (extern.convert_any
+      (any.convert_extern
+        (string.const "seven")
       )
     )
   )
@@ -623,11 +623,11 @@
 ;; CHECK:      [fuzz-exec] calling string.measure
 ;; CHECK-NEXT: [fuzz-exec] note result: string.measure => 5
 
-;; CHECK:      [fuzz-exec] calling extern
-;; CHECK-NEXT: [fuzz-exec] note result: extern => string("string")
+;; CHECK:      [fuzz-exec] calling string.to.any
+;; CHECK-NEXT: [fuzz-exec] note result: string.to.any => string("six")
 
-;; CHECK:      [fuzz-exec] calling extern-intern
-;; CHECK-NEXT: [fuzz-exec] note result: extern-intern => string("string")
+;; CHECK:      [fuzz-exec] calling string.to.any.and.back
+;; CHECK-NEXT: [fuzz-exec] note result: string.to.any.and.back => string("seven")
 ;; CHECK-NEXT: [fuzz-exec] comparing compare.1
 ;; CHECK-NEXT: [fuzz-exec] comparing compare.10
 ;; CHECK-NEXT: [fuzz-exec] comparing compare.2
@@ -648,8 +648,6 @@
 ;; CHECK-NEXT: [fuzz-exec] comparing eq.3
 ;; CHECK-NEXT: [fuzz-exec] comparing eq.4
 ;; CHECK-NEXT: [fuzz-exec] comparing eq.5
-;; CHECK-NEXT: [fuzz-exec] comparing extern
-;; CHECK-NEXT: [fuzz-exec] comparing extern-intern
 ;; CHECK-NEXT: [fuzz-exec] comparing get_codeunit
 ;; CHECK-NEXT: [fuzz-exec] comparing invalid_code_point
 ;; CHECK-NEXT: [fuzz-exec] comparing isolated_high_code_point
@@ -668,6 +666,8 @@
 ;; CHECK-NEXT: [fuzz-exec] comparing slice-unicode
 ;; CHECK-NEXT: [fuzz-exec] comparing string.from_code_point
 ;; CHECK-NEXT: [fuzz-exec] comparing string.measure
+;; CHECK-NEXT: [fuzz-exec] comparing string.to.any
+;; CHECK-NEXT: [fuzz-exec] comparing string.to.any.and.back
 ;; CHECK-NEXT: [fuzz-exec] comparing surrogate_pair_code_point
 ;; CHECK-NEXT: [fuzz-exec] comparing unsigned_code_point
 ;; CHECK-NEXT: [fuzz-exec] comparing weird_code_point
diff --git a/test/lit/passes/precompute-strings.wast b/test/lit/passes/precompute-strings.wast
index 47eaddd47e0..4649eb194fc 100644
--- a/test/lit/passes/precompute-strings.wast
+++ b/test/lit/passes/precompute-strings.wast
@@ -12,7 +12,7 @@
 
  ;; CHECK:      (type $2 (func (result (ref string))))
 
- ;; CHECK:      (type $3 (func (result anyref)))
+ ;; CHECK:      (type $3 (func (result externref)))
 
  ;; CHECK:      (type $4 (func (result (ref any))))
 
@@ -262,7 +262,7 @@
  )
 
 
- ;; CHECK:      (func $string.new-mutable (type $3) (result anyref)
+ ;; CHECK:      (func $string.new-mutable (type $3) (result externref)
  ;; CHECK-NEXT:  (string.new_wtf16_array
  ;; CHECK-NEXT:   (array.new_fixed $array16 4
  ;; CHECK-NEXT:    (i32.const 65)
@@ -274,7 +274,7 @@
  ;; CHECK-NEXT:   (i32.const 4)
  ;; CHECK-NEXT:  )
  ;; CHECK-NEXT: )
- (func $string.new-mutable (result anyref)
+ (func $string.new-mutable (result externref)
   ;; We do not precompute this because the array is mutable, and we do not yet
   ;; do an analysis to see that it does not "escape" into places that modify it.
   (string.new_wtf16_array
@@ -289,10 +289,10 @@
   )
  )
 
- ;; CHECK:      (func $string.new-immutable (type $3) (result anyref)
+ ;; CHECK:      (func $string.new-immutable (type $3) (result externref)
  ;; CHECK-NEXT:  (string.const "ABCD")
  ;; CHECK-NEXT: )
- (func $string.new-immutable (result anyref)
+ (func $string.new-immutable (result externref)
   ;; This array is immutable and we can optimize here.
   (string.new_wtf16_array
    (array.new_fixed $array16-imm 4
diff --git a/test/lit/passes/string-lowering-instructions.wast b/test/lit/passes/string-lowering-instructions.wast
index 1fd470ff822..d9a9fe6e77c 100644
--- a/test/lit/passes/string-lowering-instructions.wast
+++ b/test/lit/passes/string-lowering-instructions.wast
@@ -236,7 +236,8 @@
     (if (result stringref)
       (i32.const 0)
       (then
-        (ref.null none) ;; this will turn into noextern
+        (ref.null noextern) ;; The change from stringref to externref does not
+                            ;; cause problems here, nothing needs to change.
       )
       (else
         (local.get $ref)
@@ -263,7 +264,7 @@
         (local.get $ref)
       )
       (else
-        (ref.null none)
+        (ref.null noextern)
       )
     )
   )
@@ -378,10 +379,13 @@
   ;; CHECK-NEXT:  )
   ;; CHECK-NEXT: )
   (func $struct-of-string
-    ;; Test lowering of struct fields from stringref to externref.
+    ;; Test lowering of struct fields from stringref to externref. This was more
+    ;; useful of a test when stringref was a subtype of anyref, but it is still
+    ;; useful to verify nothing here goes wrong. (Now we convert stringref to
+    ;; externref, a supertype, which is much simpler - same bottom type, etc.)
     (drop
       (struct.new $struct-of-string
-        (ref.null none) ;; This null must be fixed to be ext.
+        (ref.null noextern) ;; This null is already of the right type.
         (i32.const 10)
         (ref.null none) ;; Nothing to do here (field remains anyref).
       )
diff --git a/test/passes/translate-to-fuzz_all-features_metrics_noprint.txt b/test/passes/translate-to-fuzz_all-features_metrics_noprint.txt
index 55eb8b3f31a..57f9609abc3 100644
--- a/test/passes/translate-to-fuzz_all-features_metrics_noprint.txt
+++ b/test/passes/translate-to-fuzz_all-features_metrics_noprint.txt
@@ -1,58 +1,56 @@
 Metrics
 total
- [exports]      : 16      
+ [exports]      : 17      
  [funcs]        : 19      
  [globals]      : 4       
  [imports]      : 12      
  [memories]     : 1       
  [memory-data]  : 17      
- [table-data]   : 6       
+ [table-data]   : 5       
  [tables]       : 2       
- [tags]         : 2       
- [total]        : 850     
- [vars]         : 49      
- ArrayNewFixed  : 5       
+ [tags]         : 3       
+ [total]        : 917     
+ [vars]         : 42      
+ ArrayNewFixed  : 1       
  AtomicCmpxchg  : 1       
- AtomicFence    : 2       
  AtomicRMW      : 1       
- Binary         : 91      
- Block          : 112     
- Break          : 5       
- Call           : 29      
+ Binary         : 94      
+ Block          : 127     
+ Break          : 4       
+ Call           : 40      
  CallIndirect   : 1       
- CallRef        : 2       
- Const          : 171     
- Drop           : 8       
- GlobalGet      : 55      
- GlobalSet      : 48      
+ CallRef        : 1       
+ Const          : 170     
+ DataDrop       : 2       
+ Drop           : 16      
+ GlobalGet      : 61      
+ GlobalSet      : 54      
  I31Get         : 2       
- If             : 37      
- Load           : 26      
- LocalGet       : 53      
- LocalSet       : 31      
- Loop           : 6       
- MemoryCopy     : 2       
+ If             : 33      
+ Load           : 23      
+ LocalGet       : 70      
+ LocalSet       : 42      
+ Loop           : 9       
+ MemoryCopy     : 3       
  MemoryFill     : 1       
- Nop            : 6       
- Pop            : 2       
- RefAs          : 1       
- RefEq          : 3       
- RefFunc        : 11      
+ Nop            : 7       
+ Pop            : 4       
+ RefAs          : 2       
+ RefEq          : 5       
+ RefFunc        : 6       
  RefI31         : 8       
- RefIsNull      : 1       
- RefNull        : 5       
- RefTest        : 1       
- Return         : 13      
- SIMDExtract    : 1       
- Select         : 8       
+ RefNull        : 2       
+ Return         : 12      
+ SIMDExtract    : 3       
+ Select         : 7       
  Store          : 2       
- StringConst    : 12      
- StringEq       : 4       
- StringWTF16Get : 1       
- StructNew      : 6       
- Try            : 3       
+ StringConst    : 7       
+ StringEq       : 3       
+ StructNew      : 5       
+ Throw          : 1       
+ Try            : 4       
  TryTable       : 4       
- TupleExtract   : 3       
- TupleMake      : 5       
- Unary          : 37      
- Unreachable    : 24      
+ TupleExtract   : 13      
+ TupleMake      : 3       
+ Unary          : 36      
+ Unreachable    : 27      


Reviews:
user: login: tlively
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-14T00:20:17Z
user: login: kripken
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T18:50:55Z
user: login: kripken
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T18:53:21Z
user: login: kripken
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T18:55:14Z
user: login: kripken
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T19:36:52Z
user: login: tlively
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T21:28:58Z
user: login: tlively
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T21:30:35Z
user: login: kripken
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T22:49:31Z
user: login: kripken
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T22:50:41Z
user: login: tlively
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T22:54:25Z
user: login: kripken
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T23:15:26Z
user: login: tlively
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T23:32:14Z
user: login: kripken
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T23:45:19Z
user: login: tlively
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T00:13:21Z
user: login: kripken
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T17:39:21Z
user: login: kripken
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T17:44:01Z
user: login: tlively
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T17:46:46Z
user: login: tlively
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-18T17:47:44Z
user: login: tlively
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-18T20:58:59Z
user: login: kripken
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T18:43:37Z
user: login: tlively
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T18:52:33Z
user: login: kripken
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T19:43:00Z
user: login: tlively
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T22:05:03Z
user: login: tlively
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-19T22:50:59Z

Review comments:
diff_hunk: @@ -433,15 +438,17 @@ std::optional<HeapType> getBasicHeapTypeLUB(HeapType::BasicHeapType a,
       }
       break;
     case HeapType::array:
-    case HeapType::string:
       lubUnshared = HeapType::any;
       break;
+    case HeapType::string:
     case HeapType::none:
     case HeapType::noext:
     case HeapType::nofunc:
     case HeapType::nocont:
     case HeapType::noexn:
-      // Bottom types already handled.
+      // Bottom types already handled (including string as the switch value,
+      // which implies the other value is bottom, which again would have been
+      // handled).
path: src/wasm/wasm-type.cpp
user: login: tlively
body: I don't understand the addition to this comment. `string` isn't a bottom type, so why is it included in the bottom types already having been handled?
created_at: 2025-03-14T00:14:56Z
author_association: MEMBER
diff_hunk: @@ -949,8 +956,9 @@ std::optional<HeapType> HeapType::getSuperType() const {
       case none:
       case exn:
       case noexn:
-      case string:
path: src/wasm/wasm-type.cpp
user: login: tlively
body: Yikes! This doesn't look like it was correct before, since it should have returned `any`. Do you know how this wasn't caught by tests?
created_at: 2025-03-14T00:16:07Z
author_association: MEMBER
diff_hunk: @@ -476,24 +476,6 @@
       (string.const "five!")
     )
   )
-
-  ;; CHECK:      [fuzz-exec] calling extern
-  ;; CHECK-NEXT: [fuzz-exec] note result: extern => string("string")
-  (func $extern (export "extern") (result externref)
-    (extern.convert_any
-      (string.const "string")
-    )
-  )
-
-  ;; CHECK:      [fuzz-exec] calling extern-intern
-  ;; CHECK-NEXT: [fuzz-exec] note result: extern-intern => string("string")
-  (func $extern-intern (export "extern-intern") (result anyref)
-    (any.convert_extern
-      (extern.convert_any
-        (string.const "string")
path: test/lit/exec/strings.wast
user: login: tlively
body: It might still be useful to test `any.convert_extern` of a string.
created_at: 2025-03-14T00:18:55Z
author_association: MEMBER
diff_hunk: @@ -236,7 +236,8 @@
     (if (result stringref)
       (i32.const 0)
       (then
-        (ref.null none) ;; this will turn into noextern
+        (ref.null noextern) ;; The change from stringref to externref does not
+                            ;; cause problems here, nothing needs to change.
path: test/lit/passes/string-lowering-instructions.wast
user: login: tlively
body: Does this unlock any potential code simplifications in StringLowering.cpp?
created_at: 2025-03-14T00:19:59Z
author_association: MEMBER
diff_hunk: @@ -433,15 +438,17 @@ std::optional<HeapType> getBasicHeapTypeLUB(HeapType::BasicHeapType a,
       }
       break;
     case HeapType::array:
-    case HeapType::string:
       lubUnshared = HeapType::any;
       break;
+    case HeapType::string:
     case HeapType::none:
     case HeapType::noext:
     case HeapType::nofunc:
     case HeapType::nocont:
     case HeapType::noexn:
-      // Bottom types already handled.
+      // Bottom types already handled (including string as the switch value,
+      // which implies the other value is bottom, which again would have been
+      // handled).
path: src/wasm/wasm-type.cpp
user: login: kripken
body: Lines 399-400 swap to get the lower type as `a`. If `a` is string, then `b` is higher than `a`, which means (since string is the last of the non-bottom types in the list) that `b` is a bottom type. But we already handled bottom types before on 392-396.
created_at: 2025-03-17T18:50:55Z
author_association: MEMBER
diff_hunk: @@ -949,8 +956,9 @@ std::optional<HeapType> HeapType::getSuperType() const {
       case none:
       case exn:
       case noexn:
-      case string:
path: src/wasm/wasm-type.cpp
user: login: kripken
body: The test was just wrong, but it agreed with the code :smile: 

See line 1683 in the gtest modified in this PR.
created_at: 2025-03-17T18:53:21Z
author_association: MEMBER
diff_hunk: @@ -236,7 +236,8 @@
     (if (result stringref)
       (i32.const 0)
       (then
-        (ref.null none) ;; this will turn into noextern
+        (ref.null noextern) ;; The change from stringref to externref does not
+                            ;; cause problems here, nothing needs to change.
path: test/lit/passes/string-lowering-instructions.wast
user: login: kripken
body: I wondered this too, but the answer is actually no. While we are doing something much simpler, logically - swapping a type for a supertype, rather than an entirely unrelated type - the actual mechanical change is identical. We just swap all appearances of type X for Y, and don't need to handle any subtyping.
created_at: 2025-03-17T18:55:14Z
author_association: MEMBER
diff_hunk: @@ -476,24 +476,6 @@
       (string.const "five!")
     )
   )
-
-  ;; CHECK:      [fuzz-exec] calling extern
-  ;; CHECK-NEXT: [fuzz-exec] note result: extern => string("string")
-  (func $extern (export "extern") (result externref)
-    (extern.convert_any
-      (string.const "string")
-    )
-  )
-
-  ;; CHECK:      [fuzz-exec] calling extern-intern
-  ;; CHECK-NEXT: [fuzz-exec] note result: extern-intern => string("string")
-  (func $extern-intern (export "extern-intern") (result anyref)
-    (any.convert_extern
-      (extern.convert_any
-        (string.const "string")
path: test/lit/exec/strings.wast
user: login: kripken
body: I don't follow - convert it from what to what?

In the new model a string is an externref like a JS object is an externref: it doesn't have a wasm representation under `any`.
created_at: 2025-03-17T19:36:52Z
author_association: MEMBER
diff_hunk: @@ -476,24 +476,6 @@
       (string.const "five!")
     )
   )
-
-  ;; CHECK:      [fuzz-exec] calling extern
-  ;; CHECK-NEXT: [fuzz-exec] note result: extern => string("string")
-  (func $extern (export "extern") (result externref)
-    (extern.convert_any
-      (string.const "string")
-    )
-  )
-
-  ;; CHECK:      [fuzz-exec] calling extern-intern
-  ;; CHECK-NEXT: [fuzz-exec] note result: extern-intern => string("string")
-  (func $extern-intern (export "extern-intern") (result anyref)
-    (any.convert_extern
-      (extern.convert_any
-        (string.const "string")
path: test/lit/exec/strings.wast
user: login: tlively
body: `any.convert_extern` and `extern.convert_any` are infallible; all `any` and `extern` values, including strings, can be converted to the other representation.
created_at: 2025-03-17T21:28:58Z
author_association: MEMBER
diff_hunk: @@ -433,15 +438,17 @@ std::optional<HeapType> getBasicHeapTypeLUB(HeapType::BasicHeapType a,
       }
       break;
     case HeapType::array:
-    case HeapType::string:
       lubUnshared = HeapType::any;
       break;
+    case HeapType::string:
     case HeapType::none:
     case HeapType::noext:
     case HeapType::nofunc:
     case HeapType::nocont:
     case HeapType::noexn:
-      // Bottom types already handled.
+      // Bottom types already handled (including string as the switch value,
+      // which implies the other value is bottom, which again would have been
+      // handled).
path: src/wasm/wasm-type.cpp
user: login: tlively
body: Right, but I don't get that from reading the comment. Is there something special about string here that doesn't also apply to other types?
created_at: 2025-03-17T21:30:35Z
author_association: MEMBER
diff_hunk: @@ -433,15 +438,17 @@ std::optional<HeapType> getBasicHeapTypeLUB(HeapType::BasicHeapType a,
       }
       break;
     case HeapType::array:
-    case HeapType::string:
       lubUnshared = HeapType::any;
       break;
+    case HeapType::string:
     case HeapType::none:
     case HeapType::noext:
     case HeapType::nofunc:
     case HeapType::nocont:
     case HeapType::noexn:
-      // Bottom types already handled.
+      // Bottom types already handled (including string as the switch value,
+      // which implies the other value is bottom, which again would have been
+      // handled).
path: src/wasm/wasm-type.cpp
user: login: kripken
body: Just that strings are at the end (right before all the bottom types). I can add that to the comment.
created_at: 2025-03-17T22:49:31Z
author_association: MEMBER
diff_hunk: @@ -476,24 +476,6 @@
       (string.const "five!")
     )
   )
-
-  ;; CHECK:      [fuzz-exec] calling extern
-  ;; CHECK-NEXT: [fuzz-exec] note result: extern => string("string")
-  (func $extern (export "extern") (result externref)
-    (extern.convert_any
-      (string.const "string")
-    )
-  )
-
-  ;; CHECK:      [fuzz-exec] calling extern-intern
-  ;; CHECK-NEXT: [fuzz-exec] note result: extern-intern => string("string")
-  (func $extern-intern (export "extern-intern") (result anyref)
-    (any.convert_extern
-      (extern.convert_any
-        (string.const "string")
path: test/lit/exec/strings.wast
user: login: kripken
body: But what is the "other representation" of string? I.e. what are you saying that `any.convert_extern` of a string should return - what actual type?
created_at: 2025-03-17T22:50:41Z
author_association: MEMBER
diff_hunk: @@ -476,24 +476,6 @@
       (string.const "five!")
     )
   )
-
-  ;; CHECK:      [fuzz-exec] calling extern
-  ;; CHECK-NEXT: [fuzz-exec] note result: extern => string("string")
-  (func $extern (export "extern") (result externref)
-    (extern.convert_any
-      (string.const "string")
-    )
-  )
-
-  ;; CHECK:      [fuzz-exec] calling extern-intern
-  ;; CHECK-NEXT: [fuzz-exec] note result: extern-intern => string("string")
-  (func $extern-intern (export "extern-intern") (result anyref)
-    (any.convert_extern
-      (extern.convert_any
-        (string.const "string")
path: test/lit/exec/strings.wast
user: login: tlively
body: It's an anyref. There's no more specific type and trying to cast it to anything else will fail.
created_at: 2025-03-17T22:54:25Z
author_association: MEMBER
diff_hunk: @@ -476,24 +476,6 @@
       (string.const "five!")
     )
   )
-
-  ;; CHECK:      [fuzz-exec] calling extern
-  ;; CHECK-NEXT: [fuzz-exec] note result: extern => string("string")
-  (func $extern (export "extern") (result externref)
-    (extern.convert_any
-      (string.const "string")
-    )
-  )
-
-  ;; CHECK:      [fuzz-exec] calling extern-intern
-  ;; CHECK-NEXT: [fuzz-exec] note result: extern-intern => string("string")
-  (func $extern-intern (export "extern-intern") (result anyref)
-    (any.convert_extern
-      (extern.convert_any
-        (string.const "string")
path: test/lit/exec/strings.wast
user: login: kripken
body: ...so it *is* possible to make a value that has (exactly) type anyref? Hence `(ref exact any)` is *not* uninhabitable?
created_at: 2025-03-17T23:15:26Z
author_association: MEMBER
diff_hunk: @@ -476,24 +476,6 @@
       (string.const "five!")
     )
   )
-
-  ;; CHECK:      [fuzz-exec] calling extern
-  ;; CHECK-NEXT: [fuzz-exec] note result: extern => string("string")
-  (func $extern (export "extern") (result externref)
-    (extern.convert_any
-      (string.const "string")
-    )
-  )
-
-  ;; CHECK:      [fuzz-exec] calling extern-intern
-  ;; CHECK-NEXT: [fuzz-exec] note result: extern-intern => string("string")
-  (func $extern-intern (export "extern-intern") (result anyref)
-    (any.convert_extern
-      (extern.convert_any
-        (string.const "string")
path: test/lit/exec/strings.wast
user: login: tlively
body: Nope :) A good way to think about it is that the converted value has a hidden subtype of anyref. `any` is the most specific heap type we can name to describe the value, but it still isn't _exactly_ any.
created_at: 2025-03-17T23:32:14Z
author_association: MEMBER
diff_hunk: @@ -476,24 +476,6 @@
       (string.const "five!")
     )
   )
-
-  ;; CHECK:      [fuzz-exec] calling extern
-  ;; CHECK-NEXT: [fuzz-exec] note result: extern => string("string")
-  (func $extern (export "extern") (result externref)
-    (extern.convert_any
-      (string.const "string")
-    )
-  )
-
-  ;; CHECK:      [fuzz-exec] calling extern-intern
-  ;; CHECK-NEXT: [fuzz-exec] note result: extern-intern => string("string")
-  (func $extern-intern (export "extern-intern") (result anyref)
-    (any.convert_extern
-      (extern.convert_any
-        (string.const "string")
path: test/lit/exec/strings.wast
user: login: kripken
body: But you are saying that Binaryen should create a value with that type. That means that Binaryen can see a value with that type. And that means that if Binaryen sees a `ref.cast` to `(exact anyref)` it should succeed on that value...?

Or are you saying Binaryen should internally create a subtype of anyref to avoid that issue?

created_at: 2025-03-17T23:45:19Z
author_association: MEMBER
diff_hunk: @@ -476,24 +476,6 @@
       (string.const "five!")
     )
   )
-
-  ;; CHECK:      [fuzz-exec] calling extern
-  ;; CHECK-NEXT: [fuzz-exec] note result: extern => string("string")
-  (func $extern (export "extern") (result externref)
-    (extern.convert_any
-      (string.const "string")
-    )
-  )
-
-  ;; CHECK:      [fuzz-exec] calling extern-intern
-  ;; CHECK-NEXT: [fuzz-exec] note result: extern-intern => string("string")
-  (func $extern-intern (export "extern-intern") (result anyref)
-    (any.convert_extern
-      (extern.convert_any
-        (string.const "string")
path: test/lit/exec/strings.wast
user: login: tlively
body: I think the easiest fix in the interpreter will be to update the interpretation of casts to have them always fail if the cast target type is an exact reference to an abstract heap type. That seems simpler than actually realizing a hidden subtype of `any` in our type system.
created_at: 2025-03-18T00:13:21Z
author_association: MEMBER
diff_hunk: @@ -476,24 +476,6 @@
       (string.const "five!")
     )
   )
-
-  ;; CHECK:      [fuzz-exec] calling extern
-  ;; CHECK-NEXT: [fuzz-exec] note result: extern => string("string")
-  (func $extern (export "extern") (result externref)
-    (extern.convert_any
-      (string.const "string")
-    )
-  )
-
-  ;; CHECK:      [fuzz-exec] calling extern-intern
-  ;; CHECK-NEXT: [fuzz-exec] note result: extern-intern => string("string")
-  (func $extern-intern (export "extern-intern") (result anyref)
-    (any.convert_extern
-      (extern.convert_any
-        (string.const "string")
path: test/lit/exec/strings.wast
user: login: kripken
body: That might work in the interpreter but I think it is dangerous in the optimizer as well. Seems safer to just add an `anystring` type (or keep the existing one as that).

How about leaving this for later? We don't urgently need this functionality (no known codebase converts string to any, in practice).
created_at: 2025-03-18T17:39:21Z
author_association: MEMBER
diff_hunk: @@ -433,15 +438,17 @@ std::optional<HeapType> getBasicHeapTypeLUB(HeapType::BasicHeapType a,
       }
       break;
     case HeapType::array:
-    case HeapType::string:
       lubUnshared = HeapType::any;
       break;
+    case HeapType::string:
     case HeapType::none:
     case HeapType::noext:
     case HeapType::nofunc:
     case HeapType::nocont:
     case HeapType::noexn:
-      // Bottom types already handled.
+      // Bottom types already handled (including string as the switch value,
+      // which implies the other value is bottom, which again would have been
+      // handled).
path: src/wasm/wasm-type.cpp
user: login: kripken
body: Comment improved.
created_at: 2025-03-18T17:44:00Z
author_association: MEMBER
diff_hunk: @@ -476,24 +476,6 @@
       (string.const "five!")
     )
   )
-
-  ;; CHECK:      [fuzz-exec] calling extern
-  ;; CHECK-NEXT: [fuzz-exec] note result: extern => string("string")
-  (func $extern (export "extern") (result externref)
-    (extern.convert_any
-      (string.const "string")
-    )
-  )
-
-  ;; CHECK:      [fuzz-exec] calling extern-intern
-  ;; CHECK-NEXT: [fuzz-exec] note result: extern-intern => string("string")
-  (func $extern-intern (export "extern-intern") (result anyref)
-    (any.convert_extern
-      (extern.convert_any
-        (string.const "string")
path: test/lit/exec/strings.wast
user: login: tlively
body: Sure, I guess we can leave it for later. I need to update the interpretation of casts to handle exact types at some point anyway.
created_at: 2025-03-18T17:46:46Z
author_association: MEMBER
diff_hunk: @@ -476,24 +476,6 @@
       (string.const "five!")
     )
   )
-
-  ;; CHECK:      [fuzz-exec] calling extern
-  ;; CHECK-NEXT: [fuzz-exec] note result: extern => string("string")
-  (func $extern (export "extern") (result externref)
-    (extern.convert_any
-      (string.const "string")
-    )
-  )
-
-  ;; CHECK:      [fuzz-exec] calling extern-intern
-  ;; CHECK-NEXT: [fuzz-exec] note result: extern-intern => string("string")
-  (func $extern-intern (export "extern-intern") (result anyref)
-    (any.convert_extern
-      (extern.convert_any
-        (string.const "string")
path: test/lit/exec/strings.wast
user: login: kripken
body: The fuzzer hits this, so we do need to resolve the matter in this PR somehow. I'm not sure it is easy to work around it in the fuzzer.
created_at: 2025-03-19T18:43:37Z
author_association: MEMBER
diff_hunk: @@ -476,24 +476,6 @@
       (string.const "five!")
     )
   )
-
-  ;; CHECK:      [fuzz-exec] calling extern
-  ;; CHECK-NEXT: [fuzz-exec] note result: extern => string("string")
-  (func $extern (export "extern") (result externref)
-    (extern.convert_any
-      (string.const "string")
-    )
-  )
-
-  ;; CHECK:      [fuzz-exec] calling extern-intern
-  ;; CHECK-NEXT: [fuzz-exec] note result: extern-intern => string("string")
-  (func $extern-intern (export "extern-intern") (result anyref)
-    (any.convert_extern
-      (extern.convert_any
-        (string.const "string")
path: test/lit/exec/strings.wast
user: login: tlively
body: We already support `Literal`s with type externref that can be unwrapped to access the underlying `any` value. This is the same thing in reverse.
created_at: 2025-03-19T18:52:31Z
author_association: MEMBER
diff_hunk: @@ -476,24 +476,6 @@
       (string.const "five!")
     )
   )
-
-  ;; CHECK:      [fuzz-exec] calling extern
-  ;; CHECK-NEXT: [fuzz-exec] note result: extern => string("string")
-  (func $extern (export "extern") (result externref)
-    (extern.convert_any
-      (string.const "string")
-    )
-  )
-
-  ;; CHECK:      [fuzz-exec] calling extern-intern
-  ;; CHECK-NEXT: [fuzz-exec] note result: extern-intern => string("string")
-  (func $extern-intern (export "extern-intern") (result anyref)
-    (any.convert_extern
-      (extern.convert_any
-        (string.const "string")
path: test/lit/exec/strings.wast
user: login: kripken
body: Do/should exact casts to externref work on such values?
created_at: 2025-03-19T19:43:00Z
author_association: MEMBER
diff_hunk: @@ -476,24 +476,6 @@
       (string.const "five!")
     )
   )
-
-  ;; CHECK:      [fuzz-exec] calling extern
-  ;; CHECK-NEXT: [fuzz-exec] note result: extern => string("string")
-  (func $extern (export "extern") (result externref)
-    (extern.convert_any
-      (string.const "string")
-    )
-  )
-
-  ;; CHECK:      [fuzz-exec] calling extern-intern
-  ;; CHECK-NEXT: [fuzz-exec] note result: extern-intern => string("string")
-  (func $extern-intern (export "extern-intern") (result anyref)
-    (any.convert_extern
-      (extern.convert_any
-        (string.const "string")
path: test/lit/exec/strings.wast
user: login: tlively
body: No, but if they do for now, it might be ok since there are no real engines to compare such executions to.
created_at: 2025-03-19T22:05:03Z
author_association: MEMBER

PR comments:
user: login: kripken
created_at: 2025-03-18T17:44:25Z
body: We can no longer fuzz strings in v8, as we represent them in a nonstandard way, so I disabled that.
author_association: MEMBER
user: login: kripken
created_at: 2025-03-18T20:44:43Z
body: I added a readme mention with references to the relevant proposals in the last commit.
author_association: MEMBER
user: login: kripken
created_at: 2025-03-18T21:11:07Z
body: @tlively I am going to fix this fuzztest error:

https://github.com/WebAssembly/binaryen/actions/runs/13933139917/job/38994921125?pr=7373

but only because luckily it seems obvious what the issue is. In general, how are people intended to debug errors like that?
author_association: MEMBER
user: login: tlively
created_at: 2025-03-19T00:27:02Z
body: `FUZZTEST_PRNG_SEED=(whatever was printed in the failing log) ./bin/binaryen-unittest --gtest_filter=TypeFuzzTest.TestHeapTypeRelationsFuzz` should deterministically reproduce the failure.
author_association: MEMBER
user: login: tlively
created_at: 2025-03-19T00:29:55Z
body: The fact that this shows up in CI at all is a nice property of FuzzTest over our other fuzzer patterns. (Although of course we could run the other fuzzers for limited time on CI as well.)
author_association: MEMBER
user: login: kripken
created_at: 2025-03-19T16:41:43Z
body: Thanks, now I see it... it was separated from the error itself, for some reason:
```
FUZZTEST_PRNG_SEED=8gPJDq0F9ObaC0X1WRajSUh_YPiNJkLQ3mvZBc8xll8
[..50 lines of stuff..]
5: Heap type has an invalid supertype
binaryen-unittests: /home/runner/work/binaryen/binaryen/test/gtest/type-domains.cpp:1048: std::vector<HeapType> wasm::(anonymous namespace)::BuildHeapTypes(TypeBuilderPlan): Assertion `built' failed.

=================================================================
=== SETUP FAILURE!

/home/runner/work/binaryen/binaryen/test/gtest/type-builder.cpp:1170: There was a problem with TypeFuzzTest.TestHeapTypeRelationsFuzz.
=================================================================
```
author_association: MEMBER
user: login: kripken
created_at: 2025-03-19T22:16:38Z
body: I added commits for fuzztest, and the last commit adds hacks to allow converting string to anyref and back.
author_association: MEMBER
user: login: kripken
created_at: 2025-03-20T16:48:47Z
body: Fuzzer looks happy now.
author_association: MEMBER