diff --git a/pkg/_js_interop_checks/lib/src/transformations/js_util_wasm_optimizer.dart b/pkg/_js_interop_checks/lib/src/transformations/js_util_wasm_optimizer.dart index 878fcfa28c3..1e7169f29fa 100644 --- a/pkg/_js_interop_checks/lib/src/transformations/js_util_wasm_optimizer.dart +++ b/pkg/_js_interop_checks/lib/src/transformations/js_util_wasm_optimizer.dart @@ -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 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); diff --git a/pkg/dart2wasm/bin/run_wasm.js b/pkg/dart2wasm/bin/run_wasm.js index cc42c376af7..9edf69fd82c 100644 --- a/pkg/dart2wasm/bin/run_wasm.js +++ b/pkg/dart2wasm/bin/run_wasm.js @@ -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)); } diff --git a/sdk/lib/_internal/wasm/lib/js_helper.dart b/sdk/lib/_internal/wasm/lib/js_helper.dart index ec24b666803..8e493286682 100644 --- a/sdk/lib/_internal/wasm/lib/js_helper.dart +++ b/sdk/lib/_internal/wasm/lib/js_helper.dart @@ -510,15 +510,18 @@ List toDartList(WasmExternRef? ref) => List.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 f, String trampolineName) { +F _wrapDartFunction( + 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; } diff --git a/tests/web/wasm/callback_test.dart b/tests/web/wasm/callback_test.dart index 2530977d3b9..24d35d371e4 100644 --- a/tests/web/wasm/callback_test.dart +++ b/tests/web/wasm/callback_test.dart @@ -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(sumString)); - Expect.equals('hello world!', dartFromJSCallbackHelper.doSum1()); - Expect.equals('foobar', dartFromJSCallbackHelper.doSum2('foo', 'bar')); + final helper = DartFromJSCallbackHelper.factory( + allowInterop(sumTwoPositional), + allowInterop( + sumOnePositionalAndOneOptional), + allowInterop(sumTwoOptional), + allowInterop( + sumOnePositionalAndOneOptionalNonNull), + allowInterop(sumTwoOptionalNonNull)); + + Expect.equals('hello world!', helper.doSum1()); + Expect.equals('foobar', helper.doSum2('foo', 'bar')); + Expect.equals('hello world!', + helper.doSum3(allowInterop((a, b) => a + b))); + + Expect.equals('foobar', helper.doSumOnePositionalAndOneOptionalA('foo')); Expect.equals( - 'hello world!', - dartFromJSCallbackHelper - .doSum3(allowInterop((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((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((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( + (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( + ([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( + (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( + ([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() { diff --git a/tests/web/wasm/static_interop_test.dart b/tests/web/wasm/static_interop_test.dart index 90656c2ed77..c6423347a84 100644 --- a/tests/web/wasm/static_interop_test.dart +++ b/tests/web/wasm/static_interop_test.dart @@ -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')