[dart2wasm] Implement optional parameters for JS interop callbacks.

Also added tests of optional parameters for static interop functions.

Change-Id: Id23237b96d0de5a4a4b948b3f23fd1bfe40b218e
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/259101
Reviewed-by: Srujan Gaddam <srujzs@google.com>
Commit-Queue: Joshua Litt <joshualitt@google.com>
This commit is contained in:
Joshua Litt 2022-09-14 22:33:02 +00:00 committed by Commit Bot
parent 9e9768c89d
commit 8429f01593
5 changed files with 305 additions and 34 deletions

View file

@ -240,7 +240,8 @@ class JsUtilWasmOptimizer extends Transformer {
/// [String] function name representing the name of the wrapping function.
/// TODO(joshualitt): Share callback trampolines if the [FunctionType]
/// matches.
String _createFunctionTrampoline(Procedure node, FunctionType function) {
String _createFunctionTrampoline(
Procedure node, FunctionType function, Expression argument) {
int fileOffset = node.fileOffset;
// Create arguments for each positional parameter in the function. These
@ -257,11 +258,50 @@ class JsUtilWasmOptimizer extends Transformer {
List<Expression> callbackArguments = [];
DartType nullableObjectType =
_objectClass.getThisType(_coreTypes, Nullability.nullable);
for (DartType type in function.positionalParameters) {
for (int i = 0; i < function.positionalParameters.length; i++) {
DartType type = function.positionalParameters[i];
Expression? defaultExpression;
bool hasDefault = i >= function.requiredParameterCount;
if (hasDefault) {
// We can only generate default values if we have a statically typed
// function argument.
Expression? initializer;
if (argument is ConstantExpression) {
Procedure callbackTarget = (argument.constant as TearOffConstant)
.targetReference
.asProcedure;
initializer =
callbackTarget.function.positionalParameters[i].initializer;
} else if (argument is FunctionExpression) {
initializer = argument.function.positionalParameters[i].initializer;
} else {
throw 'Cannot pass default arguments.';
}
// The initializer for a default argument must be a
// [ConstantExpression].
ConstantExpression init = initializer as ConstantExpression;
defaultExpression = ConstantExpression(init.constant, init.type);
}
VariableDeclaration variable =
VariableDeclaration('x${parameterId++}', type: nullableObjectType);
positionalParameters.add(variable);
callbackArguments.add(AsExpression(VariableGet(variable), type));
Expression body;
if (hasDefault) {
body = ConditionalExpression(
StaticInvocation(
_coreTypes.identicalProcedure,
Arguments([
VariableGet(variable),
ConstantExpression(NullConstant())
])),
defaultExpression ?? ConstantExpression(NullConstant()),
VariableGet(variable),
nullableObjectType);
} else {
body = VariableGet(variable);
}
callbackArguments.add(AsExpression(body, type));
}
// Create a new procedure for the callback trampoline. This procedure will
@ -308,11 +348,17 @@ class JsUtilWasmOptimizer extends Transformer {
/// [_createFunctionTrampoline] followed by `_wrapDartFunction`.
StaticInvocation _allowInterop(
Procedure node, FunctionType type, Expression argument) {
String functionTrampolineName = _createFunctionTrampoline(node, type);
String functionTrampolineName =
_createFunctionTrampoline(node, type, argument);
return StaticInvocation(
_wrapDartFunctionTarget,
Arguments([argument, StringLiteral(functionTrampolineName)],
types: [type]));
Arguments([
argument,
StringLiteral(functionTrampolineName),
ConstantExpression(IntConstant(type.positionalParameters.length))
], types: [
type
]));
}
StaticGet get _globalThis => StaticGet(_globalThisMember);

View file

@ -150,8 +150,13 @@ var dart2wasm = {
},
stringFromDartString: stringFromDartString,
stringToDartString: stringToDartString,
wrapDartFunction: function(dartFunction, exportFunctionName) {
wrapDartFunction: function(dartFunction, exportFunctionName, argCount) {
var wrapped = function (...args) {
// Pad `undefined` for optional arguments that aren't passed so that
// the trampoline can replace these values with defaults.
while (args.length < argCount) {
args.push(undefined);
}
return dartInstance.exports[`${exportFunctionName}`](
dartFunction, ...args.map(dartInstance.exports.$dartifyRaw));
}

View file

@ -510,15 +510,18 @@ List<Object?> toDartList(WasmExternRef? ref) => List<Object?>.generate(
objectLength(ref).round(), (int n) => dartifyRaw(objectReadIndex(ref, n)));
@pragma("wasm:import", "dart2wasm.wrapDartFunction")
external WasmExternRef? _wrapDartFunctionRaw(
WasmExternRef? dartFunction, WasmExternRef? trampolineName);
external WasmExternRef? _wrapDartFunctionRaw(WasmExternRef? dartFunction,
WasmExternRef? trampolineName, WasmExternRef? argCount);
F _wrapDartFunction<F extends Function>(F f, String trampolineName) {
F _wrapDartFunction<F extends Function>(
F f, String trampolineName, int argCount) {
if (functionToJSWrapper.containsKey(f)) {
return f;
}
JSValue wrappedFunction = JSValue(_wrapDartFunctionRaw(
f.toJS().toExternRef(), trampolineName.toJS().toExternRef())!);
f.toJS().toExternRef(),
trampolineName.toJS().toExternRef(),
argCount.toDouble().toJS().toExternRef())!);
functionToJSWrapper[f] = wrappedFunction;
return f;
}

View file

@ -11,51 +11,148 @@ import 'package:js/js.dart';
@JS()
external void eval(String code);
typedef SumStringCallback = String Function(String a, String b);
typedef SumTwoPositionalFun = String Function(String a, String b);
typedef SumOnePositionalAndOneOptionalFun = String Function(String a,
[String? b]);
typedef SumTwoOptionalFun = String Function([String? a, String? b]);
typedef SumOnePositionalAndOneOptionalNonNullFun = String Function(String a,
[String b]);
typedef SumTwoOptionalNonNullFun = String Function([String a, String b]);
@JS()
@staticInterop
class DartFromJSCallbackHelper {
external factory DartFromJSCallbackHelper.factory(SumStringCallback summer);
external factory DartFromJSCallbackHelper.factory(
SumTwoPositionalFun sumTwoPositional,
SumOnePositionalAndOneOptionalFun sumOnePositionalOneOptional,
SumTwoOptionalFun sumTwoOptional,
SumOnePositionalAndOneOptionalNonNullFun
sumOnePositionalAndOneOptionalNonNull,
SumTwoOptionalNonNullFun sumTwoOptionalNonNull);
}
extension DartFromJSCallbackHelperMethods on DartFromJSCallbackHelper {
external String doSum1();
external String doSum2(String a, String b);
external String doSum3(Object summer);
external String doSumOnePositionalAndOneOptionalA(String a);
external String doSumOnePositionalAndOneOptionalB(String a, String b);
external String doSumTwoOptionalA();
external String doSumTwoOptionalB(String a);
external String doSumTwoOptionalC(String a, String b);
external String doSumOnePositionalAndOneOptionalANonNull(String a);
external String doSumOnePositionalAndOneOptionalBNonNull(String a, String b);
external String doSumTwoOptionalANonNull();
external String doSumTwoOptionalBNonNull(String a);
external String doSumTwoOptionalCNonNull(String a, String b);
}
String sumString(String a, String b) {
String sumTwoPositional(String a, String b) {
return a + b;
}
String sumOnePositionalAndOneOptional(String a, [String? b]) {
return a + (b ?? 'bar');
}
String sumTwoOptional([String? a, String? b]) {
return (a ?? 'foo') + (b ?? 'bar');
}
String sumOnePositionalAndOneOptionalNonNull(String a, [String b = 'bar']) {
return a + b;
}
String sumTwoOptionalNonNull([String a = 'foo', String b = 'bar']) {
return a + b;
}
void staticInteropCallbackTest() {
eval(r'''
globalThis.DartFromJSCallbackHelper = function(summer) {
globalThis.DartFromJSCallbackHelper = function(
sumTwoPositional, sumOnePositionalOneOptional, sumTwoOptional,
sumOnePositionalAndOneOptionalNonNull, sumTwoOptionalNonNull) {
this.a = 'hello ';
this.b = 'world!';
this.sum = null;
this.summer = summer;
this.sumTwoPositional = sumTwoPositional;
this.sumOnePositionalOneOptional = sumOnePositionalOneOptional;
this.sumTwoOptional = sumTwoOptional;
this.sumOnePositionalAndOneOptionalNonNull = sumOnePositionalAndOneOptionalNonNull;
this.sumTwoOptionalNonNull = sumTwoOptionalNonNull;
this.doSum1 = () => {
return this.summer(this.a, this.b);
return this.sumTwoPositional(this.a, this.b);
}
this.doSum2 = (a, b) => {
return this.summer(a, b);
return this.sumTwoPositional(a, b);
}
this.doSum3 = (summer) => {
return summer(this.a, this.b);
}
this.doSumOnePositionalAndOneOptionalA = (a) => {
return sumOnePositionalOneOptional(a);
}
this.doSumOnePositionalAndOneOptionalB = (a, b) => {
return sumOnePositionalOneOptional(a, b);
}
this.doSumTwoOptionalA = () => {
return sumTwoOptional();
}
this.doSumTwoOptionalB = (a) => {
return sumTwoOptional(a);
}
this.doSumTwoOptionalC = (a, b) => {
return sumTwoOptional(a, b);
}
this.doSumOnePositionalAndOneOptionalANonNull = (a) => {
return sumOnePositionalAndOneOptionalNonNull(a);
}
this.doSumOnePositionalAndOneOptionalBNonNull = (a, b) => {
return sumOnePositionalAndOneOptionalNonNull(a, b);
}
this.doSumTwoOptionalANonNull = () => {
return sumTwoOptionalNonNull();
}
this.doSumTwoOptionalBNonNull = (a) => {
return sumTwoOptionalNonNull(a);
}
this.doSumTwoOptionalCNonNull = (a, b) => {
return sumTwoOptionalNonNull(a, b);
}
}
''');
final dartFromJSCallbackHelper = DartFromJSCallbackHelper.factory(
allowInterop<SumStringCallback>(sumString));
Expect.equals('hello world!', dartFromJSCallbackHelper.doSum1());
Expect.equals('foobar', dartFromJSCallbackHelper.doSum2('foo', 'bar'));
final helper = DartFromJSCallbackHelper.factory(
allowInterop<SumTwoPositionalFun>(sumTwoPositional),
allowInterop<SumOnePositionalAndOneOptionalFun>(
sumOnePositionalAndOneOptional),
allowInterop<SumTwoOptionalFun>(sumTwoOptional),
allowInterop<SumOnePositionalAndOneOptionalNonNullFun>(
sumOnePositionalAndOneOptionalNonNull),
allowInterop<SumTwoOptionalNonNullFun>(sumTwoOptionalNonNull));
Expect.equals('hello world!', helper.doSum1());
Expect.equals('foobar', helper.doSum2('foo', 'bar'));
Expect.equals('hello world!',
helper.doSum3(allowInterop<SumTwoPositionalFun>((a, b) => a + b)));
Expect.equals('foobar', helper.doSumOnePositionalAndOneOptionalA('foo'));
Expect.equals(
'hello world!',
dartFromJSCallbackHelper
.doSum3(allowInterop<SumStringCallback>((a, b) => a + b)));
'foobar', helper.doSumOnePositionalAndOneOptionalB('foo', 'bar'));
Expect.equals('foobar', helper.doSumTwoOptionalA());
Expect.equals('foobar', helper.doSumTwoOptionalB('foo'));
Expect.equals('foobar', helper.doSumTwoOptionalC('foo', 'bar'));
Expect.equals(
'foobar', helper.doSumOnePositionalAndOneOptionalANonNull('foo'));
Expect.equals(
'foobar', helper.doSumOnePositionalAndOneOptionalBNonNull('foo', 'bar'));
Expect.equals('foobar', helper.doSumTwoOptionalANonNull());
Expect.equals('foobar', helper.doSumTwoOptionalBNonNull('foo'));
Expect.equals('foobar', helper.doSumTwoOptionalCNonNull('foo', 'bar'));
}
void allowInteropCallbackTest() {
@ -66,17 +163,109 @@ void allowInteropCallbackTest() {
globalThis.doSum2 = function(a, b) {
return globalThis.summer(a, b);
}
globalThis.doSumOnePositionalAndOneOptionalA = function(a) {
return summer(a);
}
globalThis.doSumOnePositionalAndOneOptionalB = function(a, b) {
return summer(a, b);
}
globalThis.doSumTwoOptionalA = function() {
return summer();
}
globalThis.doSumTwoOptionalB = function(a) {
return summer(a);
}
globalThis.doSumTwoOptionalC = function(a, b) {
return summer(a, b);
}
globalThis.doSumOnePositionalAndOneOptionalANonNull = function(a) {
return summer(a);
}
globalThis.doSumOnePositionalAndOneOptionalBNonNull = function(a, b) {
return summer(a, b);
}
globalThis.doSumTwoOptionalANonNull = function() {
return summer();
}
globalThis.doSumTwoOptionalBNonNull = function(a) {
return summer(a);
}
globalThis.doSumTwoOptionalCNonNull = function(a, b) {
return summer(a, b);
}
''');
final interopCallback = allowInterop<SumStringCallback>((a, b) => a + b);
Expect.equals(
'foobar', callMethod(globalThis, 'doSum1', [interopCallback]).toString());
setProperty(globalThis, 'summer', interopCallback);
Expect.equals(
'foobar', callMethod(globalThis, 'doSum2', ['foo', 'bar']).toString());
final roundTripCallback = getProperty(globalThis, 'summer');
Expect.equals('foobar',
(dartify(roundTripCallback) as SumStringCallback)('foo', 'bar'));
// General
{
final interopCallback = allowInterop<SumTwoPositionalFun>((a, b) => a + b);
Expect.equals('foobar',
callMethod(globalThis, 'doSum1', [interopCallback]).toString());
setProperty(globalThis, 'summer', interopCallback);
Expect.equals(
'foobar', callMethod(globalThis, 'doSum2', ['foo', 'bar']).toString());
final roundTripCallback = getProperty(globalThis, 'summer');
Expect.equals('foobar',
(dartify(roundTripCallback) as SumTwoPositionalFun)('foo', 'bar'));
}
// 1 nullable optional argument
{
final interopCallback = allowInterop<SumOnePositionalAndOneOptionalFun>(
(a, [b]) => a + (b ?? 'bar'));
setProperty(globalThis, 'summer', interopCallback);
Expect.equals(
'foobar',
callMethod(globalThis, 'doSumOnePositionalAndOneOptionalA', ['foo'])
.toString());
Expect.equals(
'foobar',
callMethod(
globalThis, 'doSumOnePositionalAndOneOptionalB', ['foo', 'bar'])
.toString());
}
// All nullable optional arguments
{
final interopCallback = allowInterop<SumTwoOptionalFun>(
([a, b]) => (a ?? 'foo') + (b ?? 'bar'));
setProperty(globalThis, 'summer', interopCallback);
Expect.equals(
'foobar', callMethod(globalThis, 'doSumTwoOptionalA', []).toString());
Expect.equals('foobar',
callMethod(globalThis, 'doSumTwoOptionalB', ['foo']).toString());
Expect.equals('foobar',
callMethod(globalThis, 'doSumTwoOptionalC', ['foo', 'bar']).toString());
}
// 1 non-nullable optional argument
{
final interopCallback =
allowInterop<SumOnePositionalAndOneOptionalNonNullFun>(
(a, [b = 'bar']) => a + b);
setProperty(globalThis, 'summer', interopCallback);
Expect.equals(
'foobar',
callMethod(globalThis, 'doSumOnePositionalAndOneOptionalA', ['foo'])
.toString());
Expect.equals(
'foobar',
callMethod(
globalThis, 'doSumOnePositionalAndOneOptionalB', ['foo', 'bar'])
.toString());
}
// All non-nullable optional arguments
{
final interopCallback = allowInterop<SumTwoOptionalNonNullFun>(
([a = 'foo', b = 'bar']) => a + b);
setProperty(globalThis, 'summer', interopCallback);
Expect.equals(
'foobar', callMethod(globalThis, 'doSumTwoOptionalA', []).toString());
Expect.equals('foobar',
callMethod(globalThis, 'doSumTwoOptionalB', ['foo']).toString());
Expect.equals('foobar',
callMethod(globalThis, 'doSumTwoOptionalC', ['foo', 'bar']).toString());
}
}
void main() {

View file

@ -24,6 +24,10 @@ extension StaticJSClassMethods on StaticJSClass {
external int? get nullableInt;
external int nonNullableIntReturnMethod();
external int? nullableIntReturnMethod(bool returnNull);
external String doSum1Or2(String a, [String? b]);
external String doSumUpTo2([String? a, String? b]);
external String doSum1Or2NonNull(String a, [String b = 'bar']);
external String doSumUpTo2NonNull([String a = 'foo', String b = 'bar']);
}
void createClassTest() {
@ -49,6 +53,18 @@ void createClassTest() {
this.nonNullableInt = 60.5;
this.nullableInt = 100.5;
}
this.doSum1Or2 = function(a, b) {
return a + (b ?? 'bar');
}
this.doSumUpTo2 = function(a, b) {
return (a ?? 'foo') + (b ?? 'bar');
}
this.doSum1Or2NonNull = function(a, b) {
return a + b;
}
this.doSumUpTo2NonNull = function(a, b) {
return a + b;
}
}
''');
final foo = StaticJSClass.factory('foo');
@ -68,6 +84,18 @@ void createClassTest() {
foo.doublifyNumbers();
Expect.equals(100, foo.nullableInt);
Expect.equals(60, foo.nonNullableInt);
Expect.equals('foobar', foo.doSum1Or2('foo'));
Expect.equals('foobar', foo.doSum1Or2('foo', 'bar'));
Expect.equals('foobar', foo.doSumUpTo2());
Expect.equals('foobar', foo.doSumUpTo2('foo'));
Expect.equals('foobar', foo.doSumUpTo2('foo', 'bar'));
Expect.equals('foobar', foo.doSum1Or2NonNull('foo'));
Expect.equals('foobar', foo.doSum1Or2NonNull('foo', 'bar'));
Expect.equals('foobar', foo.doSumUpTo2NonNull());
Expect.equals('foobar', foo.doSumUpTo2NonNull('foo'));
Expect.equals('foobar', foo.doSumUpTo2NonNull('foo', 'bar'));
}
@JS('JSClass.NestedJSClass')