[dart2wasm] Pass Dart double as f64 to JS interop

Don't convert Dart `double`s to `externref`s when calling JS interop
functions, pass them as `double`.

We pass other unboxed values (`int`s and `bool`s) as `externref`s, as
before:

- `int`s are most efficiently passed as `externref`s, as small integers
  can be converted to `i31ref` and externalized without allocation.

- `bool`s passed as `i32` mostly work because JS treats 0 as false and
  everything else as true, but the values can still be observed as
  numbers rather than bools, which causes some test failures.

Changes:

- Make raw interop procedures take `double` as argument, when the Dart
  type for the interop function argument is non-nullable `double`.

- Pass static type of the value and expected type (by the interop
  function) to `jsifyValue`.

- `jsifyValue` then takes the static type and expected type into account
  to avoid conversions when both are `double`s.

- `jsifyValue` is refactored to avoid the type conversion mapping
  allocation on every call.

New benchmark result before the changes:

    WasmJSInterop.call.void.1ArgsDouble(RunTimeRaw): 0.018275229357798167 ns.

After:

    WasmJSInterop.call.void.1ArgsDouble(RunTimeRaw): 0.014034965034965034 ns.

Issue: https://github.com/dart-lang/sdk/issues/60357
Change-Id: Ia70671f9a8e14f359f1119beda123e94aacdd2cd
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/422480
Commit-Queue: Ömer Ağacan <omersa@google.com>
Reviewed-by: Srujan Gaddam <srujzs@google.com>
diff --git a/benchmarks/WasmJSInterop/WasmJSInterop.dart b/benchmarks/WasmJSInterop/WasmJSInterop.dart
index 8b58d15..33d50cf 100644
--- a/benchmarks/WasmJSInterop/WasmJSInterop.dart
+++ b/benchmarks/WasmJSInterop/WasmJSInterop.dart
@@ -19,12 +19,15 @@
 @JS()
 external void eval(String code);
 
-// This returns `void` to avoid adding `dartify` overheads to the benchmark
+// These return `void` to avoid adding `dartify` overheads to the benchmark
 // results.
-// V8 can't figure out this doesn't do anything so the loop and JS calls aren't
-// eliminated.
+// V8 can't figure out that these don't do anything so the loops and JS calls
+// aren't eliminated.
 @JS()
-external void intId(int i);
+external void intFun(int i);
+
+@JS()
+external void doubleFun(double d);
 
 // Run benchmarked code for at least 2 seconds.
 const int minimumMeasureDurationMillis = 2000;
@@ -38,15 +41,34 @@
   double measure() =>
       BenchmarkBase.measureFor(() {
         for (int i = start; i < end; i += 1) {
-          intId(i);
+          intFun(i);
         }
       }, minimumMeasureDurationMillis) /
       (end - start);
 }
 
+class DoublePassingBenchmark {
+  final double start;
+  final double step;
+  final int calls;
+
+  DoublePassingBenchmark(this.start, this.step, this.calls);
+
+  double measure() =>
+      BenchmarkBase.measureFor(() {
+        double d = start;
+        for (int i = 0; i < calls; i += 1) {
+          doubleFun(d);
+          d *= step;
+        }
+      }, minimumMeasureDurationMillis) /
+      calls;
+}
+
 void main() {
   eval('''
-    self.intId = (i) => i;
+    self.intFun = (i) => i;
+    self.doubleFun = (d) => d;
     ''');
 
   final maxI31 = (1 << 30) - 1;
@@ -56,6 +78,12 @@
 
   final large = IntPassingBenchmark(maxI31 + 1, maxI31 + 1000001).measure();
   report('WasmJSInterop.call.void.1ArgsInt', large);
+
+  // Have more than one call site to the `double` benchmark to avoid inlining
+  // too much, and for fair comparison with the `int` benchmark above.
+  DoublePassingBenchmark(1.0, 1.0, 10).measure();
+  final double = DoublePassingBenchmark(1.0, 12.34, 1000000).measure();
+  report('WasmJSInterop.call.void.1ArgsDouble', double);
 }
 
 /// Reports in Golem-specific format.
diff --git a/pkg/dart2wasm/lib/js/callback_specializer.dart b/pkg/dart2wasm/lib/js/callback_specializer.dart
index 35f2465..a150f74 100644
--- a/pkg/dart2wasm/lib/js/callback_specializer.dart
+++ b/pkg/dart2wasm/lib/js/callback_specializer.dart
@@ -53,7 +53,8 @@
         type: callExpr.getStaticType(_staticTypeContext),
         isSynthesized: true);
 
-    final jsified = jsifyValue(temp, _util, _staticTypeContext.typeEnvironment);
+    final jsified = jsifyValue(temp, _util.nullableWasmExternRefType, _util,
+        _staticTypeContext.typeEnvironment);
 
     return ReturnStatement(Let(temp, jsified));
   }
diff --git a/pkg/dart2wasm/lib/js/interop_specializer.dart b/pkg/dart2wasm/lib/js/interop_specializer.dart
index 3e2ea1c..2f1a298 100644
--- a/pkg/dart2wasm/lib/js/interop_specializer.dart
+++ b/pkg/dart2wasm/lib/js/interop_specializer.dart
@@ -80,10 +80,16 @@
     // Initialize variable declarations.
     List<String> jsParameterStrings = [];
     List<VariableDeclaration> dartPositionalParameters = [];
-    for (int j = 0; j < parameters.length; j++) {
-      String parameterString = 'x$j';
+    for (int i = 0; i < parameters.length; i++) {
+      final VariableDeclaration parameter = parameters[i];
+      final DartType parameterType = parameter.type;
+      final interopFunctionParameterType =
+          parameterType == _util.coreTypes.doubleNonNullableRawType
+              ? _util.coreTypes.doubleNonNullableRawType
+              : _util.nullableWasmExternRefType;
+      String parameterString = 'x$i';
       dartPositionalParameters.add(VariableDeclaration(parameterString,
-          type: _util.nullableWasmExternRefType, isSynthesized: true));
+          type: interopFunctionParameterType, isSynthesized: true));
       jsParameterStrings.add(parameterString);
     }
 
@@ -130,12 +136,19 @@
   @override
   Expression specialize() {
     // Return the replacement body.
-    Expression invocation = StaticInvocation(
-        _getOrCreateInteropProcedure(),
-        Arguments(parameters
-            .map<Expression>((variable) => jsifyValue(variable, factory._util,
-                factory._staticTypeContext.typeEnvironment))
-            .toList()));
+    final interopProcedure = _getOrCreateInteropProcedure();
+    final interopProcedureType =
+        interopProcedure.computeSignatureOrFunctionType();
+    final List<Expression> jsifiedArguments = [];
+    for (int i = 0; i < parameters.length; i += 1) {
+      jsifiedArguments.add(jsifyValue(
+          parameters[i],
+          interopProcedureType.positionalParameters[i],
+          factory._util,
+          factory._staticTypeContext.typeEnvironment));
+    }
+    final invocation =
+        StaticInvocation(interopProcedure, Arguments(jsifiedArguments));
     return _util.castInvocationForReturn(invocation, function.returnType);
   }
 }
@@ -225,18 +238,23 @@
   Expression specialize() {
     // Create or get the specialized procedure for the invoked number of
     // arguments. Cast as needed and return the final invocation.
-    final staticInvocation = StaticInvocation(
-        _getOrCreateInteropProcedure(),
-        Arguments(invocation.arguments.positional.map<Expression>((expr) {
-          final temp = VariableDeclaration(null,
-              initializer: expr,
-              type: expr.getStaticType(factory._staticTypeContext),
-              isSynthesized: true);
-          return Let(
-              temp,
-              jsifyValue(temp, factory._util,
-                  factory._staticTypeContext.typeEnvironment));
-        }).toList()));
+    final interopProcedure = _getOrCreateInteropProcedure();
+    final interopProcedureType =
+        interopProcedure.computeSignatureOrFunctionType();
+    final List<Expression> jsifiedArguments = [];
+    final List<Expression> arguments = invocation.arguments.positional;
+    for (int i = 0; i < arguments.length; i += 1) {
+      final temp = VariableDeclaration(null,
+          initializer: arguments[i],
+          type: arguments[i].getStaticType(factory._staticTypeContext),
+          isSynthesized: true);
+      jsifiedArguments.add(Let(
+          temp,
+          jsifyValue(temp, interopProcedureType.positionalParameters[i],
+              factory._util, factory._staticTypeContext.typeEnvironment)));
+    }
+    final staticInvocation =
+        StaticInvocation(interopProcedure, Arguments(jsifiedArguments));
     return _util.castInvocationForReturn(
         staticInvocation, invocation.getStaticType(_staticTypeContext));
   }
@@ -321,21 +339,24 @@
     for (NamedExpression expr in invocation.arguments.named) {
       namedArgs[expr.name] = expr.value;
     }
+    final interopProcedureType =
+        interopProcedure.computeSignatureOrFunctionType();
     final arguments =
         parameters.map<Expression>((decl) => namedArgs[decl.name!]!).toList();
-    final positionalArgs = arguments.map<Expression>((expr) {
+    final List<Expression> jsifiedArguments = [];
+    for (int i = 0; i < arguments.length; i += 1) {
       final temp = VariableDeclaration(null,
-          initializer: expr,
-          type: expr.getStaticType(_staticTypeContext),
+          initializer: arguments[i],
+          type: arguments[i].getStaticType(factory._staticTypeContext),
           isSynthesized: true);
-      return Let(
+      jsifiedArguments.add(Let(
           temp,
-          jsifyValue(
-              temp, factory._util, factory._staticTypeContext.typeEnvironment));
-    }).toList();
+          jsifyValue(temp, interopProcedureType.positionalParameters[i],
+              factory._util, factory._staticTypeContext.typeEnvironment)));
+    }
     assert(factory._extensionIndex.isStaticInteropType(function.returnType));
     return invokeOneArg(_util.jsValueBoxTarget,
-        StaticInvocation(interopProcedure, Arguments(positionalArgs)));
+        StaticInvocation(interopProcedure, Arguments(jsifiedArguments)));
   }
 }
 
diff --git a/pkg/dart2wasm/lib/js/util.dart b/pkg/dart2wasm/lib/js/util.dart
index 3df9c88..4f8b8ab 100644
--- a/pkg/dart2wasm/lib/js/util.dart
+++ b/pkg/dart2wasm/lib/js/util.dart
@@ -92,6 +92,41 @@
   final Class jsArrayBufferImplClass;
   final Class byteBufferClass;
 
+  // NB. We rely on iteration ordering being insertion order to handle subtypes
+  // before supertypes to convert as `int` and `double` before `num`.
+  late final Map<Class, Procedure> _externRefConverterMap = {
+    coreTypes.boolClass: toJSBoolean,
+    coreTypes.intClass: jsifyInt,
+    coreTypes.doubleClass: toJSNumber,
+    coreTypes.numClass: jsifyNum,
+    jsValueClass: jsifyJSValue,
+    coreTypes.stringClass: jsifyString,
+    jsInt8ArrayImplClass: jsifyJSInt8ArrayImpl,
+    jsUint8ArrayImplClass: jsifyJSUint8ArrayImpl,
+    jsUint8ClampedArrayImplClass: jsifyJSUint8ClampedArrayImpl,
+    jsInt16ArrayImplClass: jsifyJSInt16ArrayImpl,
+    jsUint16ArrayImplClass: jsifyJSUint16ArrayImpl,
+    jsInt32ArrayImplClass: jsifyJSInt32ArrayImpl,
+    jsUint32ArrayImplClass: jsifyJSUint32ArrayImpl,
+    jsFloat32ArrayImplClass: jsifyJSFloat32ArrayImpl,
+    jsFloat64ArrayImplClass: jsifyJSFloat64ArrayImpl,
+    int8ListClass: jsInt8ArrayFromDartInt8List,
+    uint8ListClass: jsUint8ArrayFromDartUint8List,
+    uint8ClampedListClass: jsUint8ClampedArrayFromDartUint8ClampedList,
+    int16ListClass: jsInt16ArrayFromDartInt16List,
+    uint16ListClass: jsUint16ArrayFromDartUint16List,
+    int32ListClass: jsInt32ArrayFromDartInt32List,
+    uint32ListClass: jsUint32ArrayFromDartUint32List,
+    float32ListClass: jsFloat32ArrayFromDartFloat32List,
+    float64ListClass: jsFloat64ArrayFromDartFloat64List,
+    jsDataViewImplClass: jsifyJSDataViewImpl,
+    byteDataClass: jsifyByteData,
+    coreTypes.listClass: jsifyRawList,
+    jsArrayBufferImplClass: jsifyJSArrayBufferImpl,
+    byteBufferClass: jsArrayBufferFromDartByteBuffer,
+    coreTypes.functionClass: jsifyFunction,
+  };
+
   CoreTypesUtil(this.coreTypes, this.extensionIndex)
       : allowInteropTarget = coreTypes.index
             .getTopLevelProcedure('dart:js_util', 'allowInterop'),
@@ -367,6 +402,43 @@
     }
     return AsExpression(expression, staticType);
   }
+
+  /// Return the function to convert a value with type [valueType] to Dart
+  /// interop type [expectedType].
+  ///
+  /// [expectedType] can be any interop type, but for now this only handles the
+  /// interop types generated by [_Specializer._getRawInteropProcedure].
+  ///
+  /// `null` return value means no conversion is needed, the value can be passed
+  /// to the interop function directly.
+  ///
+  /// The argument passed to the returned conversion function needs to be
+  /// non-nullable. This function does not check the nullability of [valueType]
+  /// and assume that the argument passed to the conversion function won't be
+  /// `null`.
+  Procedure? _conversionProcedure(
+      DartType valueType, DartType expectedType, TypeEnvironment typeEnv) {
+    if (expectedType == coreTypes.doubleNonNullableRawType) {
+      assert(valueType is InterfaceType &&
+          valueType.classNode == coreTypes.doubleClass);
+      return null;
+    }
+
+    assert(expectedType == nullableWasmExternRefType,
+        'Unexpected expected type: $expectedType');
+
+    for (final entry in _externRefConverterMap.entries) {
+      if (typeEnv.isSubtypeOf(
+          valueType,
+          InterfaceType(entry.key, Nullability.nonNullable),
+          SubtypeCheckMode.withNullabilities)) {
+        return entry.value;
+      }
+    }
+
+    // `dynamic` or `Object?`, convert based on runtime type.
+    return jsifyRawTarget;
+  }
 }
 
 StaticInvocation invokeOneArg(Procedure target, Expression arg) =>
@@ -383,20 +455,22 @@
 bool parametersNeedParens(List<String> parameters) =>
     parameters.isEmpty || parameters.length > 1;
 
-Expression jsifyValue(VariableDeclaration variable, CoreTypesUtil coreTypes,
-    TypeEnvironment typeEnv) {
-  final Procedure conversionProcedure;
+Expression jsifyValue(VariableDeclaration variable, DartType expectedType,
+    CoreTypesUtil coreTypes, TypeEnvironment typeEnv) {
+  final Procedure? conversionProcedure;
 
   if (coreTypes.extensionIndex.isStaticInteropType(variable.type) ||
       coreTypes.extensionIndex.isExternalDartReferenceType(variable.type)) {
     conversionProcedure = coreTypes.jsValueUnboxTarget;
   } else {
     conversionProcedure =
-        _conversionProcedure(variable.type, coreTypes, typeEnv);
+        coreTypes._conversionProcedure(variable.type, expectedType, typeEnv);
   }
 
-  final conversion =
-      StaticInvocation(conversionProcedure, Arguments([VariableGet(variable)]));
+  final conversion = conversionProcedure == null
+      ? VariableGet(variable)
+      : StaticInvocation(
+          conversionProcedure, Arguments([VariableGet(variable)]));
 
   if (variable.type.isPotentiallyNullable) {
     return ConditionalExpression(
@@ -408,54 +482,3 @@
     return conversion;
   }
 }
-
-Procedure _conversionProcedure(
-    DartType type, CoreTypesUtil util, TypeEnvironment typeEnv) {
-  // NB. We rely on iteration ordering being insertion order to handle subtypes
-  // before supertypes to convert as `int` and `double` before `num`.
-  final Map<Class, Procedure> converterMap = {
-    util.coreTypes.boolClass: util.toJSBoolean,
-    util.coreTypes.intClass: util.jsifyInt,
-    util.coreTypes.doubleClass: util.toJSNumber,
-    util.coreTypes.numClass: util.jsifyNum,
-    util.jsValueClass: util.jsifyJSValue,
-    util.coreTypes.stringClass: util.jsifyString,
-    util.jsInt8ArrayImplClass: util.jsifyJSInt8ArrayImpl,
-    util.jsUint8ArrayImplClass: util.jsifyJSUint8ArrayImpl,
-    util.jsUint8ClampedArrayImplClass: util.jsifyJSUint8ClampedArrayImpl,
-    util.jsInt16ArrayImplClass: util.jsifyJSInt16ArrayImpl,
-    util.jsUint16ArrayImplClass: util.jsifyJSUint16ArrayImpl,
-    util.jsInt32ArrayImplClass: util.jsifyJSInt32ArrayImpl,
-    util.jsUint32ArrayImplClass: util.jsifyJSUint32ArrayImpl,
-    util.jsFloat32ArrayImplClass: util.jsifyJSFloat32ArrayImpl,
-    util.jsFloat64ArrayImplClass: util.jsifyJSFloat64ArrayImpl,
-    util.int8ListClass: util.jsInt8ArrayFromDartInt8List,
-    util.uint8ListClass: util.jsUint8ArrayFromDartUint8List,
-    util.uint8ClampedListClass:
-        util.jsUint8ClampedArrayFromDartUint8ClampedList,
-    util.int16ListClass: util.jsInt16ArrayFromDartInt16List,
-    util.uint16ListClass: util.jsUint16ArrayFromDartUint16List,
-    util.int32ListClass: util.jsInt32ArrayFromDartInt32List,
-    util.uint32ListClass: util.jsUint32ArrayFromDartUint32List,
-    util.float32ListClass: util.jsFloat32ArrayFromDartFloat32List,
-    util.float64ListClass: util.jsFloat64ArrayFromDartFloat64List,
-    util.jsDataViewImplClass: util.jsifyJSDataViewImpl,
-    util.byteDataClass: util.jsifyByteData,
-    util.coreTypes.listClass: util.jsifyRawList,
-    util.jsArrayBufferImplClass: util.jsifyJSArrayBufferImpl,
-    util.byteBufferClass: util.jsArrayBufferFromDartByteBuffer,
-    util.coreTypes.functionClass: util.jsifyFunction,
-  };
-
-  for (final entry in converterMap.entries) {
-    if (typeEnv.isSubtypeOf(
-        type,
-        InterfaceType(entry.key, Nullability.nonNullable),
-        SubtypeCheckMode.withNullabilities)) {
-      return entry.value;
-    }
-  }
-
-  // `dynamic` or `Object?`, convert based on runtime type.
-  return util.jsifyRawTarget;
-}