[dart2wasm] Use WasmArray<>s for string interpolation expressions

* Use `WasmArray<>`s in string interoplation expressions
* Use `WasmArray<>`s in string buffer implementation
* Avoid making OneByteString+JSString imply TwoByteString

This also increases performance significantly, e.g.
JsonObjectRoundTrip & StringBuffer by 40% in -O4 and even more
speedup in -O2

Change-Id: I25485a6c532c3afed7d92943b6de4d1452930606
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/352000
Reviewed-by: Slava Egorov <vegorov@google.com>
Commit-Queue: Martin Kustermann <kustermann@google.com>
diff --git a/pkg/dart2wasm/lib/code_generator.dart b/pkg/dart2wasm/lib/code_generator.dart
index 0eb61bb..080ee16 100644
--- a/pkg/dart2wasm/lib/code_generator.dart
+++ b/pkg/dart2wasm/lib/code_generator.dart
@@ -2854,10 +2854,8 @@
       return visitStringLiteral(expr, expectedType);
     }
 
-    makeListFromExpressions(
-        node.expressions,
-        InterfaceType(
-            translator.coreTypes.stringClass, Nullability.nonNullable));
+    makeArrayFromExpressions(node.expressions,
+        translator.coreTypes.objectRawType(Nullability.nullable));
     return translator.outputOrVoid(call(translator.options.jsCompatibility
         ? translator.jsStringInterpolate.reference
         : translator.stringInterpolate.reference));
diff --git a/pkg/dart2wasm/lib/kernel_nodes.dart b/pkg/dart2wasm/lib/kernel_nodes.dart
index 69d1dad..8f8bea1 100644
--- a/pkg/dart2wasm/lib/kernel_nodes.dart
+++ b/pkg/dart2wasm/lib/kernel_nodes.dart
@@ -176,7 +176,7 @@
   late final Procedure jsStringEquals =
       index.getProcedure("dart:_js_types", "JSStringImpl", "==");
   late final Procedure jsStringInterpolate =
-      index.getProcedure("dart:_js_types", "JSStringImpl", "interpolate");
+      index.getProcedure("dart:_js_types", "JSStringImpl", "_interpolate");
 
   // dart:collection procedures
   late final Procedure mapFactory =
diff --git a/sdk/lib/_internal/wasm/lib/js_string.dart b/sdk/lib/_internal/wasm/lib/js_string.dart
index 1db58a5..af47437 100644
--- a/sdk/lib/_internal/wasm/lib/js_string.dart
+++ b/sdk/lib/_internal/wasm/lib/js_string.dart
@@ -29,7 +29,7 @@
   bool get isNotEmpty => !isEmpty;
 
   @pragma("wasm:entry-point")
-  static String interpolate(List<Object?> values) {
+  static String _interpolate(WasmArray<Object?> values) {
     final valuesLength = values.length;
     final array = JSArrayImpl.fromLength(valuesLength);
     for (int i = 0; i < valuesLength; i++) {
diff --git a/sdk/lib/_internal/wasm/lib/string.dart b/sdk/lib/_internal/wasm/lib/string.dart
index 3989b37..56ce152 100644
--- a/sdk/lib/_internal/wasm/lib/string.dart
+++ b/sdk/lib/_internal/wasm/lib/string.dart
@@ -13,7 +13,7 @@
         unsafeCast,
         WasmStringBase;
 
-import 'dart:_js_helper' show JS;
+import 'dart:_js_helper' show JS, jsStringToDartString;
 import 'dart:_js_types' show JSStringImpl;
 import 'dart:_object_helper';
 import 'dart:_string_helper';
@@ -299,7 +299,9 @@
 
   bool get isNotEmpty => !isEmpty;
 
-  String operator +(String other) => _interpolate([this, other]);
+  String operator +(String other) {
+    return _interpolate(WasmArray<Object?>.literal([this, other]));
+  }
 
   String toString() {
     return this;
@@ -891,34 +893,30 @@
     return buffer.toString();
   }
 
-  /**
-   * Convert all objects in [values] to strings and concat them
-   * into a result string.
-   * Modifies the input list if it contains non-`String` values.
-   */
+  // Used in string interpolation expressions where ownership of array is passed
+  // to this function.
+  //
+  // It special cases all [OneByteString]s. We could also special case all
+  // [TwoByteString] & all [JSStringImpl] cases.
   @pragma("wasm:entry-point", "call")
-  static String _interpolate(final List<Object?> values) {
-    final numValues = values.length;
+  static String _interpolate(final WasmArray<Object?> values) {
     int totalLength = 0;
-    int i = 0;
-    while (i < numValues) {
-      final e = values[i];
-      final s = e.toString();
-      values[i] = s;
-      if (s is OneByteString) {
-        totalLength += s.length;
-        i++;
-      } else {
-        // Handle remaining elements without checking for one-byte-ness.
-        while (++i < numValues) {
-          final e = values[i];
-          values[i] = e.toString();
-        }
-        return _concatRangeNative(values, 0, numValues);
+    bool isOneByteString = true;
+    final numValues = values.length;
+    for (int i = 0; i < numValues; ++i) {
+      final value = values[i];
+      var stringValue = value is String ? value : value.toString();
+      if (stringValue is JSStringImpl) {
+        stringValue = jsStringToDartString(stringValue.toExternRef);
       }
+      values[i] = stringValue;
+      isOneByteString = isOneByteString && stringValue is OneByteString;
+      totalLength += stringValue.length;
     }
-    // All strings were one-byte strings.
-    return OneByteString._concatAll(values, totalLength);
+    if (isOneByteString) {
+      return OneByteString._concatAll(values, totalLength);
+    }
+    return StringBase._concatAllFallback(values, totalLength);
   }
 
   static ArgumentError _interpolationError(Object? o, Object? result) {
@@ -992,40 +990,52 @@
 
   String toLowerCase() => _toLowerCase(this);
 
+  // To be called if not all of the given [StringBase] strings are
+  // [OneByteString]s.
+  static String _concatAllFallback(
+      WasmArray<Object?> strings, int totalLength) {
+    final result = TwoByteString.withLength(totalLength);
+    int offset = 0;
+    for (int i = 0; i < strings.length; i++) {
+      final s = unsafeCast<StringBase>(strings[i]);
+      offset = s._copyIntoTwoByteString(result, offset);
+    }
+    return result;
+  }
+
   // Concatenate ['start', 'end'[ elements of 'strings'.
-  static String concatRange(List<String> strings, int start, int end) {
+  //
+  // It special cases all [OneByteString]s. We could also special case all
+  // [TwoByteString] & all [JSStringImpl] cases.
+  static String concatRange(WasmArray<String> strings, int start, int end) {
     if ((end - start) == 1) {
       return strings[start];
     }
-    return _concatRangeNative(strings, start, end);
+    int totalLength = 0;
+    bool isOneByteString = true;
+    for (int i = start; i < end; i++) {
+      String stringValue = strings[i];
+      if (stringValue is JSStringImpl) {
+        stringValue = jsStringToDartString(stringValue.toExternRef);
+        strings[i] = stringValue;
+      }
+      isOneByteString = isOneByteString && stringValue is OneByteString;
+      totalLength += stringValue.length;
+    }
+    if (isOneByteString) {
+      return OneByteString._concatRange(strings, start, end, totalLength);
+    }
+    return _concatRangeFallback(strings, start, end, totalLength);
   }
 
-  // Call this method if all elements of [strings] are known to be strings
-  // but not all are known to be OneByteString(s).
-  static String _concatRangeNative(List<Object?> strings, int start, int end) {
-    int totalLength = 0;
-    for (int i = start; i < end; i++) {
-      final str = strings[i];
-      if (str is JSStringImpl) {
-        totalLength += str.length;
-      } else {
-        totalLength += unsafeCast<StringBase>(str).length;
-      }
-    }
-    TwoByteString result = TwoByteString.withLength(totalLength);
+  // To be called if not all strings are [OneByteString]s.
+  static String _concatRangeFallback(
+      WasmArray<String> strings, int start, int end, int totalLength) {
+    final result = TwoByteString.withLength(totalLength);
     int offset = 0;
     for (int i = start; i < end; i++) {
-      final str = strings[i];
-      if (str is JSStringImpl) {
-        final length = str.length;
-        final to = result._array;
-        for (int j = 0; j < length; j++) {
-          to.write(offset++, str.codeUnitAt(j));
-        }
-      } else {
-        StringBase s = unsafeCast<StringBase>(strings[i]);
-        offset = s._copyIntoTwoByteString(result, offset);
-      }
+      final s = unsafeCast<StringBase>(strings[i]);
+      offset = s._copyIntoTwoByteString(result, offset);
     }
     return result;
   }
@@ -1036,7 +1046,7 @@
 @pragma("wasm:entry-point")
 final class OneByteString extends StringBase {
   @pragma("wasm:entry-point")
-  WasmArray<WasmI8> _array;
+  final WasmArray<WasmI8> _array;
 
   OneByteString.withLength(int length) : _array = WasmArray<WasmI8>(length);
 
@@ -1101,16 +1111,28 @@
   }
 
   // All element of 'strings' must be OneByteStrings.
-  static _concatAll(List strings, int totalLength) {
+  static OneByteString _concatAll(WasmArray<Object?> strings, int totalLength) {
     final result = OneByteString.withLength(totalLength);
-    final to = result._array;
-    final stringsLength = strings.length;
+    final resultBytes = result._array;
     int resultOffset = 0;
-    for (int s = 0; s < stringsLength; s++) {
-      final OneByteString e = unsafeCast<OneByteString>(strings[s]);
-      final length = e._array.length;
-      to.copy(resultOffset, e._array, 0, length);
-      resultOffset += length;
+    for (int i = 0; i < strings.length; i++) {
+      final bytes = unsafeCast<OneByteString>(strings[i])._array;
+      resultBytes.copy(resultOffset, bytes, 0, bytes.length);
+      resultOffset += bytes.length;
+    }
+    return result;
+  }
+
+  // All element of 'strings' must be OneByteStrings.
+  static OneByteString _concatRange(
+      WasmArray<String> strings, int start, int end, int totalLength) {
+    final result = OneByteString.withLength(totalLength);
+    final resultBytes = result._array;
+    int resultOffset = 0;
+    for (int i = start; i < end; i++) {
+      final bytes = unsafeCast<OneByteString>(strings[i])._array;
+      resultBytes.copy(resultOffset, bytes, 0, bytes.length);
+      resultOffset += bytes.length;
     }
     return result;
   }
@@ -1360,7 +1382,7 @@
 @pragma("wasm:entry-point")
 final class TwoByteString extends StringBase {
   @pragma("wasm:entry-point")
-  WasmArray<WasmI16> _array;
+  final WasmArray<WasmI16> _array;
 
   TwoByteString.withLength(int length) : _array = WasmArray<WasmI16>(length);
 
diff --git a/sdk/lib/_internal/wasm/lib/string_buffer_patch.dart b/sdk/lib/_internal/wasm/lib/string_buffer_patch.dart
index a6d0de0..d5d74d8 100644
--- a/sdk/lib/_internal/wasm/lib/string_buffer_patch.dart
+++ b/sdk/lib/_internal/wasm/lib/string_buffer_patch.dart
@@ -4,6 +4,7 @@
 
 import "dart:_internal" show patch;
 import "dart:_string" show StringBase, TwoByteString;
+import "dart:_wasm";
 
 import "dart:typed_data" show Uint16List;
 
@@ -17,7 +18,8 @@
    * When strings are written to the string buffer, we add them to a
    * list of string parts.
    */
-  List<String>? _parts;
+  WasmArray<String>? _parts = null;
+  int _partsWriteIndex = -1;
 
   /**
     * Total number of code units in the string parts. Does not include
@@ -120,6 +122,7 @@
   @patch
   void clear() {
     _parts = null;
+    _partsWriteIndex = -1;
     _partsCodeUnits = _bufferPosition = _bufferCodeUnitMagnitude = 0;
   }
 
@@ -130,7 +133,7 @@
     final localParts = _parts;
     return (_partsCodeUnits == 0 || localParts == null)
         ? ""
-        : StringBase.concatRange(localParts, 0, localParts.length);
+        : StringBase.concatRange(localParts, 0, _partsWriteIndex);
   }
 
   /** Ensures that the buffer has enough capacity to add n code units. */
@@ -161,23 +164,29 @@
    * many code units are contained in the parts.
    */
   void _addPart(String str) {
-    final localParts = _parts;
+    WasmArray<String>? localParts = _parts;
     int length = str.length;
     _partsCodeUnits += length;
     _partsCodeUnitsSinceCompaction += length;
 
     if (localParts == null) {
-      // Empirically this is a good capacity to minimize total bytes allocated.
-      // _parts = _GrowableList.withCapacity(10)..add(str);
-      // TODO(omersa): Uncomment the line above after moving list
-      // implementations to a library.
-      _parts = [str];
-    } else {
-      localParts.add(str);
-      int partsSinceCompaction = localParts.length - _partsCompactionIndex;
-      if (partsSinceCompaction == _PARTS_TO_COMPACT) {
-        _compact();
-      }
+      _parts = WasmArray<String>.filled(10, str);
+      _partsWriteIndex = 1;
+      return;
+    }
+
+    // Possibly grow the backing array.
+    if (localParts.length <= _partsWriteIndex) {
+      localParts =
+          WasmArray<String>.filled(2 * localParts.length, localParts[0])
+            ..copy(0, localParts, 0, _partsWriteIndex);
+      _parts = localParts;
+    }
+
+    localParts[_partsWriteIndex++] = str;
+    int partsSinceCompaction = _partsWriteIndex - _partsCompactionIndex;
+    if (partsSinceCompaction == _PARTS_TO_COMPACT) {
+      _compact();
     }
   }
 
@@ -193,11 +202,11 @@
           _partsCompactionIndex, // Start
           _partsCompactionIndex + _PARTS_TO_COMPACT // End
           );
-      localParts.length = localParts.length - _PARTS_TO_COMPACT;
-      localParts.add(compacted);
+      _partsWriteIndex = _partsWriteIndex - _PARTS_TO_COMPACT;
+      localParts[_partsWriteIndex++] = compacted;
     }
     _partsCodeUnitsSinceCompaction = 0;
-    _partsCompactionIndex = localParts.length;
+    _partsCompactionIndex = _partsWriteIndex;
   }
 
   /**