| // Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| import 'package:_js_interop_checks/src/js_interop.dart' |
| show getJSName, hasAnonymousAnnotation, hasJSInteropAnnotation; |
| import 'package:_js_interop_checks/src/transformations/js_util_optimizer.dart' |
| show InlineExtensionIndex; |
| import 'package:dart2wasm/js/method_collector.dart'; |
| import 'package:dart2wasm/js/util.dart'; |
| import 'package:kernel/ast.dart'; |
| import 'package:kernel/type_environment.dart'; |
| |
| /// A general config class for an interop method. |
| /// |
| /// dart2wasm needs to create a trampoline method in JS that then calls the |
| /// interop member in question. In order to do so, we need information on things |
| /// like the name of the member, how many parameters it takes in, and more. |
| abstract class _Specializer { |
| final InteropSpecializerFactory factory; |
| final Procedure interopMethod; |
| final String jsString; |
| late final bool firstParameterIsObject = |
| factory._inlineExtensionIndex.isInstanceInteropMember(interopMethod); |
| |
| _Specializer(this.factory, this.interopMethod, this.jsString); |
| |
| StatefulStaticTypeContext get _staticTypeContext => |
| factory._staticTypeContext; |
| CoreTypesUtil get _util => factory._util; |
| MethodCollector get _methodCollector => factory._methodCollector; |
| Map<Procedure, Map<int, Procedure>> get _overloadedProcedures => |
| factory._overloadedProcedures; |
| Map<Procedure, Map<String, Procedure>> get _jsObjectLiteralMethods => |
| factory._jsObjectLiteralMethods; |
| FunctionNode get function => interopMethod.function; |
| Uri get fileUri => interopMethod.fileUri; |
| bool get hasOptionalPositionalParameters => |
| function.requiredParameterCount < function.positionalParameters.length; |
| |
| /// Whether this config is associated with a constructor or factory. |
| bool get isConstructor; |
| |
| /// The parameters that determine arity of the interop procedure that is |
| /// created from this config. |
| List<VariableDeclaration> get parameters; |
| |
| /// Returns the string that will be the body of the JS trampoline. |
| /// |
| /// [object] is the callee if there is one for this config. [callArguments] is |
| /// the remaining arguments of the `interopMethod`. |
| String bodyString(String object, List<String> callArguments); |
| |
| /// Compute and return the JS trampoline string needed for this method |
| /// lowering. |
| String generateJS(List<String> parameterNames) { |
| final object = isConstructor |
| ? '' |
| : firstParameterIsObject |
| ? parameterNames[0] |
| : 'globalThis'; |
| final callArguments = |
| firstParameterIsObject ? parameterNames.sublist(1) : parameterNames; |
| final callArgumentsString = callArguments.join(','); |
| final functionParameters = firstParameterIsObject |
| ? '$object${callArguments.isEmpty ? '' : ',$callArgumentsString'}' |
| : callArgumentsString; |
| final body = bodyString(object, callArguments); |
| if (parametersNeedParens(parameterNames)) { |
| return '($functionParameters) => $body'; |
| } else { |
| return '$functionParameters => $body'; |
| } |
| } |
| |
| /// Returns an [Expression] representing the specialization of a given |
| /// [StaticInvocation] or [Procedure]. |
| Expression specialize(); |
| |
| Procedure _getRawInteropProcedure() { |
| // Initialize variable declarations. |
| List<String> jsParameterStrings = []; |
| List<VariableDeclaration> dartPositionalParameters = []; |
| for (int j = 0; j < parameters.length; j++) { |
| String parameterString = 'x$j'; |
| dartPositionalParameters.add(VariableDeclaration(parameterString, |
| type: _util.nullableWasmExternRefType, isSynthesized: true)); |
| jsParameterStrings.add(parameterString); |
| } |
| |
| // Create Dart procedure stub for JS method. |
| String jsMethodName = _methodCollector.generateMethodName(); |
| final dartProcedure = _methodCollector.addInteropProcedure( |
| '|$jsMethodName', |
| 'dart2wasm.$jsMethodName', |
| FunctionNode(null, |
| positionalParameters: dartPositionalParameters, |
| returnType: _util.nullableWasmExternRefType), |
| fileUri, |
| AnnotationType.import, |
| isExternal: true); |
| _methodCollector.addMethod( |
| dartProcedure, jsMethodName, generateJS(jsParameterStrings)); |
| return dartProcedure; |
| } |
| |
| /// Creates a Dart procedure that calls out to a specialized JS method for the |
| /// given [config] and returns the created procedure. |
| Procedure _getOrCreateInteropProcedure() { |
| // Procedures with optional arguments are specialized at the |
| // invocation-level, so we cache if we've already created an interop |
| // procedure for the given number of parameters. |
| Procedure? cachedProcedure = |
| _overloadedProcedures[interopMethod]?[parameters.length]; |
| if (cachedProcedure != null) return cachedProcedure; |
| final dartProcedure = _getRawInteropProcedure(); |
| _overloadedProcedures.putIfAbsent( |
| interopMethod, () => {})[parameters.length] = dartProcedure; |
| return dartProcedure; |
| } |
| |
| Procedure _getInteropProcedure() => hasOptionalPositionalParameters |
| ? _getOrCreateInteropProcedure() |
| : _getRawInteropProcedure(); |
| } |
| |
| /// Config class for interop members that get lowered on the procedure side. |
| abstract class _ProcedureSpecializer extends _Specializer { |
| _ProcedureSpecializer(super.context, super.interopMethod, super.jsString); |
| |
| @override |
| List<VariableDeclaration> get parameters => function.positionalParameters; |
| |
| /// Returns an invocation of a specialized JS method meant to be used in a |
| /// procedure-level lowering. |
| @override |
| Expression specialize() { |
| // Return the replacement body. |
| Expression invocation = StaticInvocation( |
| _getInteropProcedure(), |
| Arguments(parameters |
| .map<Expression>((value) => StaticInvocation( |
| _util.jsifyTarget(value.type), Arguments([VariableGet(value)]))) |
| .toList())); |
| return _util.castInvocationForReturn(invocation, function.returnType); |
| } |
| } |
| |
| class _ConstructorSpecializer extends _ProcedureSpecializer { |
| _ConstructorSpecializer(InteropSpecializerFactory factory, |
| Procedure interopMethod, String jsString) |
| : super(factory, interopMethod, jsString); |
| |
| @override |
| bool get isConstructor => true; |
| |
| @override |
| String bodyString(String object, List<String> callArguments) => |
| "new $jsString(${callArguments.join(',')})"; |
| } |
| |
| class _GetterSpecializer extends _ProcedureSpecializer { |
| _GetterSpecializer(super.factory, super.interopMethod, super.jsString); |
| |
| @override |
| bool get isConstructor => false; |
| |
| @override |
| String bodyString(String object, List<String> callArguments) => |
| '$object.$jsString'; |
| } |
| |
| class _SetterSpecializer extends _ProcedureSpecializer { |
| _SetterSpecializer(super.factory, super.interopMethod, super.jsString); |
| |
| @override |
| bool get isConstructor => false; |
| |
| @override |
| String bodyString(String object, List<String> callArguments) => |
| '$object.$jsString = ${callArguments[0]}'; |
| } |
| |
| class _MethodSpecializer extends _ProcedureSpecializer { |
| _MethodSpecializer(super.factory, super.interopMethod, super.jsString); |
| |
| @override |
| bool get isConstructor => false; |
| |
| @override |
| String bodyString(String object, List<String> callArguments) => |
| "$object.$jsString(${callArguments.join(',')})"; |
| } |
| |
| class _OperatorSpecializer extends _ProcedureSpecializer { |
| _OperatorSpecializer(super.factory, super.interopMethod, super.jsString); |
| |
| @override |
| bool get isConstructor => false; |
| |
| @override |
| String bodyString(String object, List<String> callArguments) { |
| if (jsString == '[]') { |
| return '$object[${callArguments[0]}]'; |
| } else if (jsString == '[]=') { |
| return '$object[${callArguments[0]}] = ${callArguments[1]}'; |
| } else { |
| throw 'Unsupported operator: $jsString'; |
| } |
| } |
| } |
| |
| /// Config class for interop members that get lowered on the invocation side. |
| abstract class _InvocationSpecializer extends _Specializer { |
| final StaticInvocation invocation; |
| _InvocationSpecializer( |
| super.factory, super.interopMethod, super.jsString, this.invocation); |
| } |
| |
| /// Config class for procedures that are lowered on the invocation-side, but |
| /// only contain positional parameters. |
| abstract class _PositionalInvocationSpecializer extends _InvocationSpecializer { |
| _PositionalInvocationSpecializer( |
| super.factory, super.interopMethod, super.jsString, super.invocation); |
| |
| @override |
| List<VariableDeclaration> get parameters => function.positionalParameters |
| .sublist(0, invocation.arguments.positional.length); |
| |
| /// Returns an invocation of a specialized JS method meant to be used in an |
| /// invocation-level lowering. |
| @override |
| 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( |
| _getInteropProcedure(), |
| Arguments(invocation.arguments.positional |
| .map<Expression>((expr) => StaticInvocation( |
| _util.jsifyTarget(expr.getStaticType(_staticTypeContext)), |
| Arguments([expr]))) |
| .toList())); |
| return _util.castInvocationForReturn(staticInvocation, function.returnType); |
| } |
| } |
| |
| class _ConstructorInvocationSpecializer |
| extends _PositionalInvocationSpecializer { |
| _ConstructorInvocationSpecializer( |
| super.factory, super.interopMethod, super.jsString, super.invocation); |
| |
| @override |
| bool get isConstructor => true; |
| |
| @override |
| String bodyString(String object, List<String> callArguments) => |
| "new $jsString(${callArguments.join(',')})"; |
| } |
| |
| class _MethodInvocationSpecializer extends _PositionalInvocationSpecializer { |
| _MethodInvocationSpecializer( |
| super.factory, super.interopMethod, super.jsString, super.invocation); |
| |
| @override |
| bool get isConstructor => false; |
| |
| @override |
| String bodyString(String object, List<String> callArguments) => |
| "$object.$jsString(${callArguments.join(',')})"; |
| } |
| |
| /// Config class for object literals, which only use named arguments and are |
| /// only lowered at the invocation-level. |
| class _ObjectLiteralSpecializer extends _InvocationSpecializer { |
| _ObjectLiteralSpecializer(InteropSpecializerFactory factory, |
| Procedure interopMethod, StaticInvocation invocation) |
| : super(factory, interopMethod, '', invocation); |
| |
| @override |
| bool get isConstructor => true; |
| |
| @override |
| List<VariableDeclaration> get parameters { |
| // Compute the named parameters that were used in the given `invocation`. |
| // Note that we preserve the procedure's ordering and not the invocation's. |
| // This is also used below for the names of object literal arguments in |
| // `generateJS`. |
| final usedArgs = |
| invocation.arguments.named.map((expr) => expr.name).toSet(); |
| return function.namedParameters |
| .where((decl) => usedArgs.contains(decl.name)) |
| .toList(); |
| } |
| |
| @override |
| String bodyString(String object, List<String> callArguments) { |
| final keys = parameters.map((named) => named.name!).toList(); |
| final keyValuePairs = <String>[]; |
| for (int i = 0; i < callArguments.length; i++) { |
| keyValuePairs.add('${keys[i]}: ${callArguments[i]}'); |
| } |
| return '({${keyValuePairs.join(',')}})'; |
| } |
| |
| /// Returns an invocation of a specialized JS method that creates an object |
| /// literal using the arguments from the invocation. |
| @override |
| Expression specialize() { |
| // To avoid one method for every invocation, we optimize and compute one |
| // method per invocation shape. For example, `Cons(a: 0, b: 0)`, |
| // `Cons(a: 0)`, and `Cons(a: 1, b: 1)` only create two shapes: |
| // `{a: value, b: value}` and `{a: value}`. Therefore, we only need two |
| // methods to handle the `Cons` invocations. |
| final shape = |
| parameters.map((VariableDeclaration decl) => decl.name).join('|'); |
| final interopProcedure = _jsObjectLiteralMethods |
| .putIfAbsent(interopMethod, () => {}) |
| .putIfAbsent(shape, () => _getRawInteropProcedure()); |
| |
| // Return the args in the order of the procedure's parameters and not |
| // the invocation. |
| final namedArgs = <String, Expression>{}; |
| for (NamedExpression expr in invocation.arguments.named) { |
| namedArgs[expr.name] = expr.value; |
| } |
| final arguments = |
| parameters.map<Expression>((decl) => namedArgs[decl.name!]!).toList(); |
| final positionalArgs = arguments |
| .map<Expression>((expr) => StaticInvocation( |
| _util.jsifyTarget(expr.getStaticType(_staticTypeContext)), |
| Arguments([expr]))) |
| .toList(); |
| assert( |
| factory._inlineExtensionIndex.isStaticInteropType(function.returnType)); |
| return invokeOneArg(_util.jsValueBoxTarget, |
| StaticInvocation(interopProcedure, Arguments(positionalArgs))); |
| } |
| } |
| |
| class InteropSpecializerFactory { |
| final StatefulStaticTypeContext _staticTypeContext; |
| final CoreTypesUtil _util; |
| final MethodCollector _methodCollector; |
| final Map<Procedure, Map<int, Procedure>> _overloadedProcedures = {}; |
| final Map<Procedure, Map<String, Procedure>> _jsObjectLiteralMethods = {}; |
| late String _libraryJSString; |
| late final InlineExtensionIndex _inlineExtensionIndex; |
| |
| InteropSpecializerFactory(this._staticTypeContext, this._util, |
| this._methodCollector, this._inlineExtensionIndex); |
| |
| void enterLibrary(Library library) { |
| _libraryJSString = getJSName(library); |
| if (_libraryJSString.isNotEmpty) { |
| _libraryJSString = '$_libraryJSString.'; |
| } |
| } |
| |
| String _getJSString(Annotatable a, String initial) { |
| String selectorString = getJSName(a); |
| if (selectorString.isEmpty) { |
| selectorString = initial; |
| } |
| return selectorString; |
| } |
| |
| String _getTopLevelJSString(Annotatable a, String initial) => |
| '$_libraryJSString${_getJSString(a, initial)}'; |
| |
| /// Get the `_Specializer` for the non-constructor [node] with its |
| /// associated [jsString] name, and the [invocation] it's used in if this is |
| /// an invocation-level lowering. |
| _Specializer? _getSpecializerForMember(Procedure node, String jsString, |
| [StaticInvocation? invocation]) { |
| if (invocation == null) { |
| if (_inlineExtensionIndex.isGetter(node)) { |
| return _GetterSpecializer(this, node, jsString); |
| } else if (_inlineExtensionIndex.isSetter(node)) { |
| return _SetterSpecializer(this, node, jsString); |
| } else if (_inlineExtensionIndex.isOperator(node)) { |
| return _OperatorSpecializer(this, node, jsString); |
| } else if (_inlineExtensionIndex.isMethod(node)) { |
| return _MethodSpecializer(this, node, jsString); |
| } |
| } else { |
| if (_inlineExtensionIndex.isMethod(node)) { |
| return _MethodInvocationSpecializer(this, node, jsString, invocation); |
| } |
| } |
| return null; |
| } |
| |
| /// Get the `_Specializer` for the constructor [node], whether it |
| /// [isObjectLiteral] or not, with its associated [jsString] name, and the |
| /// [invocation] it's used in if this is an invocation-level lowering. |
| _Specializer? _getSpecializerForConstructor( |
| bool isObjectLiteral, Procedure node, String jsString, |
| [StaticInvocation? invocation]) { |
| if (invocation == null) { |
| if (!isObjectLiteral) { |
| return _ConstructorSpecializer(this, node, jsString); |
| } |
| } else { |
| if (isObjectLiteral) { |
| return _ObjectLiteralSpecializer(this, node, invocation); |
| } else { |
| return _ConstructorInvocationSpecializer( |
| this, node, jsString, invocation); |
| } |
| } |
| return null; |
| } |
| |
| /// Given a procedure [node], determines if it's an interop procedure that |
| /// needs to be specialized, and if so, returns the specializer associated |
| /// with it. |
| /// |
| /// If [invocation] is not null, returns an invocation-level config for the |
| /// [node] if it exists. |
| _Specializer? _getSpecializer(Procedure node, |
| [StaticInvocation? invocation]) { |
| if (node.enclosingClass != null && |
| hasJSInteropAnnotation(node.enclosingClass!)) { |
| final cls = node.enclosingClass!; |
| final clsString = _getTopLevelJSString(cls, cls.name); |
| if (node.isFactory) { |
| return _getSpecializerForConstructor( |
| hasAnonymousAnnotation(cls), node, clsString, invocation); |
| } else { |
| final memberSelectorString = _getJSString(node, node.name.text); |
| return _getSpecializerForMember( |
| node, '$clsString.$memberSelectorString', invocation); |
| } |
| } else if (node.isExtensionTypeMember) { |
| final nodeDescriptor = _inlineExtensionIndex.getInlineDescriptor(node); |
| if (nodeDescriptor != null) { |
| final cls = _inlineExtensionIndex.getInlineClass(node)!; |
| final clsString = _getTopLevelJSString(cls, cls.name); |
| final kind = nodeDescriptor.kind; |
| if ((kind == ExtensionTypeMemberKind.Constructor || |
| kind == ExtensionTypeMemberKind.Factory)) { |
| return _getSpecializerForConstructor( |
| _inlineExtensionIndex.isLiteralConstructor(node), |
| node, |
| clsString, |
| invocation); |
| } else { |
| final memberSelectorString = |
| _getJSString(node, nodeDescriptor.name.text); |
| if (nodeDescriptor.isStatic) { |
| return _getSpecializerForMember( |
| node, '$clsString.$memberSelectorString', invocation); |
| } else { |
| return _getSpecializerForMember( |
| node, memberSelectorString, invocation); |
| } |
| } |
| } |
| } else if (node.isExtensionMember) { |
| final nodeDescriptor = _inlineExtensionIndex.getExtensionDescriptor(node); |
| if (nodeDescriptor != null && !nodeDescriptor.isStatic) { |
| return _getSpecializerForMember( |
| node, _getJSString(node, nodeDescriptor.name.text), invocation); |
| } |
| } else if (hasJSInteropAnnotation(node)) { |
| return _getSpecializerForMember( |
| node, _getTopLevelJSString(node, node.name.text), invocation); |
| } |
| return null; |
| } |
| |
| Expression? maybeSpecializeInvocation( |
| Procedure target, StaticInvocation node) { |
| if (target.isExternal || _overloadedProcedures.containsKey(target)) { |
| return _getSpecializer(target, node)?.specialize(); |
| } |
| return null; |
| } |
| |
| bool maybeSpecializeProcedure(Procedure node) { |
| if (node.isExternal) { |
| final specializer = _getSpecializer(node); |
| if (specializer != null) { |
| final expression = specializer.specialize(); |
| final transformedBody = specializer.function.returnType is VoidType |
| ? ExpressionStatement(expression) |
| : ReturnStatement(expression); |
| |
| // For the time being to support tearoffs we simply replace the body of |
| // the original procedure, but leave all the optional arguments intact. |
| // This unfortunately results in inconsistent behavior between the |
| // tearoff and the original functions. |
| // TODO(joshualitt): Decide if we should disallow tearoffs of external |
| // functions, and if so we can clean this up. |
| FunctionNode function = node.function; |
| function.body = transformedBody..parent = function; |
| node.isExternal = false; |
| return true; |
| } |
| } |
| return false; |
| } |
| } |