[dart2wasm] Implement `toString` methods on double using JS interop.

Change-Id: I5843e9cae59b8f152b922a01796eaf05494808a0
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/250681
Reviewed-by: Aske Simon Christensen <askesc@google.com>
Commit-Queue: Joshua Litt <joshualitt@google.com>
diff --git a/pkg/dart2wasm/bin/run_wasm.js b/pkg/dart2wasm/bin/run_wasm.js
index abe9cc7..40d57a6 100644
--- a/pkg/dart2wasm/bin/run_wasm.js
+++ b/pkg/dart2wasm/bin/run_wasm.js
@@ -270,6 +270,34 @@
         }
         return null;
     },
+    stringify: function(o) {
+        return stringToDartString(String(o));
+    },
+    doubleToString: function(v) {
+        return stringToDartString(v.toString());
+    },
+    toFixed: function(double, digits) {
+        return stringToDartString(double.toFixed(digits));
+    },
+    toExponential: function(double, fractionDigits)  {
+        return stringToDartString(double.toExponential(fractionDigits));
+    },
+    toPrecision: function(double, precision) {
+        return stringToDartString(double.toPrecision(precision));
+    },
+    parseDouble: function(source) {
+        // Notice that JS parseFloat accepts garbage at the end of the string.
+        // Accept only:
+        // - [+/-]NaN
+        // - [+/-]Infinity
+        // - a Dart double literal
+        // We do allow leading or trailing whitespace.
+        var jsSource = stringFromDartString(source);
+        if (!/^\s*[+-]?(?:Infinity|NaN|(?:\.\d+|\d+(?:\.\d*)?)(?:[eE][+-]?\d+)?)\s*$/.test(jsSource)) {
+          return NaN;
+        }
+        return parseFloat(jsSource);
+    },
 };
 
 function instantiate(filename, imports) {
diff --git a/sdk/lib/_internal/wasm/lib/double.dart b/sdk/lib/_internal/wasm/lib/double.dart
index ea77a86..eef8c29 100644
--- a/sdk/lib/_internal/wasm/lib/double.dart
+++ b/sdk/lib/_internal/wasm/lib/double.dart
@@ -4,6 +4,49 @@
 
 // part of "core_patch.dart";
 
+@pragma("wasm:import", "dart2wasm.doubleToString")
+external String _doubleToString(double v);
+
+@pragma("wasm:import", "dart2wasm.toFixed")
+external String _toFixed(double d, double digits);
+
+@pragma("wasm:import", "dart2wasm.toExponential")
+external String _toExponential1(double d);
+
+@pragma("wasm:import", "dart2wasm.toExponential")
+external String _toExponential2(double d, double fractionDigits);
+
+@pragma("wasm:import", "dart2wasm.toPrecision")
+external String _toPrecision(double d, double precision);
+
+@pragma("wasm:import", "dart2wasm.parseDouble")
+external double _parseDouble(String doubleString);
+
+@patch
+class double {
+  @patch
+  static double parse(String source,
+      [@deprecated double onError(String source)?]) {
+    double? result = tryParse(source);
+    if (result == null) {
+      throw FormatException('Invalid double $source');
+    }
+    return result;
+  }
+
+  @patch
+  static double? tryParse(String source) {
+    double result = _parseDouble(source);
+    if (result.isNaN) {
+      String trimmed = source.trim();
+      if (!(trimmed == 'NaN' || trimmed == '+NaN' || trimmed == '-NaN')) {
+        return null;
+      }
+    }
+    return result;
+  }
+}
+
 @pragma("wasm:entry-point")
 class _BoxedDouble implements double {
   // A boxed double contains an unboxed double.
@@ -142,8 +185,6 @@
   static final List _cache = new List.filled(CACHE_LENGTH, null);
   static int _cacheEvictIndex = 0;
 
-  external String _toString();
-
   String toString() {
     // TODO(koda): Consider starting at most recently inserted.
     for (int i = 0; i < CACHE_LENGTH; i += 2) {
@@ -156,7 +197,7 @@
     if (identical(0.0, this)) {
       return "0.0";
     }
-    String result = _toString();
+    String result = _doubleToString(value);
     // Replace the least recently inserted entry.
     _cache[_cacheEvictIndex] = this;
     _cache[_cacheEvictIndex + 1] = result;
@@ -188,7 +229,8 @@
     return _toStringAsFixed(fractionDigits);
   }
 
-  external String _toStringAsFixed(int fractionDigits);
+  String _toStringAsFixed(int fractionDigits) =>
+      _toFixed(value, fractionDigits.toDouble());
 
   String toStringAsExponential([int? fractionDigits]) {
     // See ECMAScript-262, 15.7.4.6 for details.
@@ -208,14 +250,16 @@
     if (this == double.infinity) return "Infinity";
     if (this == -double.infinity) return "-Infinity";
 
-    // The dart function prints the shortest representation when fractionDigits
-    // equals null. The native function wants -1 instead.
-    fractionDigits = (fractionDigits == null) ? -1 : fractionDigits;
-
     return _toStringAsExponential(fractionDigits);
   }
 
-  external String _toStringAsExponential(int fractionDigits);
+  String _toStringAsExponential(int? fractionDigits) {
+    if (fractionDigits == null) {
+      return _toExponential1(value);
+    } else {
+      return _toExponential2(value, fractionDigits.toDouble());
+    }
+  }
 
   String toStringAsPrecision(int precision) {
     // See ECMAScript-262, 15.7.4.7 for details.
@@ -236,7 +280,8 @@
     return _toStringAsPrecision(precision);
   }
 
-  external String _toStringAsPrecision(int fractionDigits);
+  String _toStringAsPrecision(int fractionDigits) =>
+      _toPrecision(value, fractionDigits.toDouble());
 
   // Order is: NaN > Infinity > ... > 0.0 > -0.0 > ... > -Infinity.
   int compareTo(num other) {
diff --git a/sdk/lib/_internal/wasm/lib/js_helper.dart b/sdk/lib/_internal/wasm/lib/js_helper.dart
index 99e4db0..07cfb05 100644
--- a/sdk/lib/_internal/wasm/lib/js_helper.dart
+++ b/sdk/lib/_internal/wasm/lib/js_helper.dart
@@ -3,7 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 
 // Helpers for working with JS.
-library dart.js_helper;
+library dart._js_helper;
 
 import 'dart:_internal';
 import 'dart:typed_data';
@@ -23,6 +23,11 @@
   Object toObject() => jsObjectToDartObject(_ref);
 }
 
+extension DoubleToJS on double {
+  WasmAnyRef toAnyRef() => toJSNumber(this);
+  JSValue toJS() => JSValue(toAnyRef());
+}
+
 extension StringToJS on String {
   WasmAnyRef toAnyRef() => jsStringFromDartString(this);
   JSValue toJS() => JSValue(toAnyRef());
@@ -108,6 +113,9 @@
 @pragma("wasm:import", "dart2wasm.isJSObject")
 external bool isJSObject(WasmAnyRef? o);
 
+// The JS runtime will run helpful conversion routines between refs and bool /
+// double. In the longer term hopefully we can find a way to avoid the round
+// trip.
 @pragma("wasm:import", "dart2wasm.roundtrip")
 external double toDartNumber(WasmAnyRef ref);
 
@@ -193,6 +201,9 @@
 external WasmAnyRef? callMethodVarArgsRaw(
     WasmAnyRef o, WasmAnyRef method, WasmAnyRef? args);
 
+@pragma("wasm:import", "dart2wasm.stringify")
+external String stringifyRaw(WasmAnyRef? object);
+
 // Currently, `allowInterop` returns a Function type. This is unfortunate for
 // Dart2wasm because it means arbitrary Dart functions can flow to JS util
 // calls. Our only solutions is to cache every function called with