| // Copyright (c) 2022, 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:kernel/ast.dart'; |
| import 'package:kernel/class_hierarchy.dart'; |
| import 'package:kernel/core_types.dart'; |
| import 'package:kernel/type_environment.dart'; |
| |
| import '../js_interop.dart' |
| show |
| getJSName, |
| hasAnonymousAnnotation, |
| hasStaticInteropAnnotation, |
| hasJSInteropAnnotation; |
| |
| /// Replaces: |
| /// 1) Factory constructors in classes with `@staticInterop` annotations with |
| /// calls to `js_util_wasm.callConstructorVarArgs`. |
| /// 2) External methods in `@staticInterop` class extensions to their |
| /// corresponding `js_util_wasm` calls. |
| /// TODO(joshualitt): In the long term we'd like to have the same |
| /// `JsUtilOptimizer` for all web backends. This is to ensure uniform semantics |
| /// across all web backends. Some known challenges remain, and there may be |
| /// unknown challenges that appear as we proceed. Known challenges include: |
| /// 1) Some constructs may need to be restricted on Dart2wasm, for example |
| /// callbacks must be fully typed for the time being, though we could |
| /// generalize this using `Function.apply`, but this is currently not |
| /// implemented on Dart2wasm. |
| /// 2) We may want to handle this lowering differently on Dart2wasm in the |
| /// long term. Currently, js_util on Wasm is implemented using a single |
| /// trampoline per js_util function. We may want to specialize these |
| /// trampolines based off the interop type, to avoid megamorphic behavior. |
| /// This would have code size implications though, so we would need to |
| /// proceed carefully. |
| /// TODO(joshualitt): A few of the functions in this file use js_utils in a |
| /// naive way, and could be optimized further. In particular, there are a few |
| /// complex operations in [JsUtilWasmOptimizer] where it is obvious a value is |
| /// flowing from / to JS, and we have a few options for optimization: |
| /// 1) Integrate with `js_ast` and emit custom JavaScript for each of these |
| /// operations. |
| /// 2) Use the `raw` variants of the js_util calls. |
| /// 3) Move more of the logic for these calls into JS where it will likely be |
| /// faster. |
| class JsUtilWasmOptimizer extends Transformer { |
| final Procedure _callMethodTarget; |
| final Procedure _callConstructorTarget; |
| final Procedure _globalThisTarget; |
| final Procedure _getPropertyTarget; |
| final Procedure _setPropertyTarget; |
| final Procedure _jsifyTarget; |
| final Procedure _dartifyTarget; |
| final Procedure _newObjectTarget; |
| final Class _jsValueClass; |
| |
| final CoreTypes _coreTypes; |
| final StatefulStaticTypeContext _staticTypeContext; |
| Map<Reference, ExtensionMemberDescriptor>? _extensionMemberIndex; |
| final Set<Class> _transformedClasses = {}; |
| |
| JsUtilWasmOptimizer(this._coreTypes, ClassHierarchy hierarchy) |
| : _callMethodTarget = _coreTypes.index |
| .getTopLevelProcedure('dart:js_util_wasm', 'callMethodVarArgs'), |
| _globalThisTarget = _coreTypes.index |
| .getTopLevelProcedure('dart:js_util_wasm', 'globalThis'), |
| _callConstructorTarget = _coreTypes.index.getTopLevelProcedure( |
| 'dart:js_util_wasm', 'callConstructorVarArgs'), |
| _getPropertyTarget = _coreTypes.index |
| .getTopLevelProcedure('dart:js_util_wasm', 'getProperty'), |
| _setPropertyTarget = _coreTypes.index |
| .getTopLevelProcedure('dart:js_util_wasm', 'setProperty'), |
| _jsifyTarget = |
| _coreTypes.index.getTopLevelProcedure('dart:js_util_wasm', 'jsify'), |
| _dartifyTarget = _coreTypes.index |
| .getTopLevelProcedure('dart:js_util_wasm', 'dartify'), |
| _newObjectTarget = _coreTypes.index |
| .getTopLevelProcedure('dart:js_util_wasm', 'newObject'), |
| _jsValueClass = |
| _coreTypes.index.getClass('dart:js_util_wasm', 'JSValue'), |
| _staticTypeContext = StatefulStaticTypeContext.stacked( |
| TypeEnvironment(_coreTypes, hierarchy)) {} |
| |
| @override |
| Library visitLibrary(Library lib) { |
| _staticTypeContext.enterLibrary(lib); |
| lib.transformChildren(this); |
| _staticTypeContext.leaveLibrary(lib); |
| _extensionMemberIndex = null; |
| _transformedClasses.clear(); |
| return lib; |
| } |
| |
| @override |
| Member defaultMember(Member node) { |
| _staticTypeContext.enterMember(node); |
| node.transformChildren(this); |
| _staticTypeContext.leaveMember(node); |
| return node; |
| } |
| |
| @override |
| Procedure visitProcedure(Procedure node) { |
| _staticTypeContext.enterMember(node); |
| Statement? transformedBody; |
| if (node.isExternal) { |
| if (node.isFactory) { |
| Class cls = node.enclosingClass!; |
| if (hasStaticInteropAnnotation(cls)) { |
| if (hasAnonymousAnnotation(cls)) { |
| transformedBody = _getExternalAnonymousConstructorBody(node); |
| } else { |
| String jsName = getJSName(cls); |
| String constructorName = jsName == '' ? cls.name : jsName; |
| transformedBody = |
| _getExternalCallConstructorBody(node, constructorName); |
| } |
| } |
| } else if (node.isExtensionMember) { |
| var index = _extensionMemberIndex ??= |
| _createExtensionMembersIndex(node.enclosingLibrary); |
| var nodeDescriptor = index[node.reference]; |
| if (nodeDescriptor != null) { |
| if (!nodeDescriptor.isStatic) { |
| if (nodeDescriptor.kind == ExtensionMemberKind.Getter) { |
| transformedBody = _getExternalExtensionGetterBody(node); |
| } else if (nodeDescriptor.kind == ExtensionMemberKind.Setter) { |
| transformedBody = _getExternalExtensionSetterBody(node); |
| } else if (nodeDescriptor.kind == ExtensionMemberKind.Method) { |
| transformedBody = _getExternalExtensionMethodBody(node); |
| } |
| } |
| } |
| } else if (hasJSInteropAnnotation(node)) { |
| String selectorString = getJSName(node); |
| late Expression target; |
| if (selectorString.isEmpty) { |
| target = _globalThis; |
| } else { |
| List<String> selectors = selectorString.split('.'); |
| target = getObjectOffGlobalThis(node, selectors); |
| } |
| if (node.isGetter) { |
| transformedBody = _getExternalTopLevelGetterBody(node, target); |
| } else if (node.isSetter) { |
| transformedBody = _getExternalTopLevelSetterBody(node, target); |
| } else { |
| assert(node.kind == ProcedureKind.Method); |
| transformedBody = _getExternalTopLevelMethodBody(node, target); |
| } |
| } |
| } |
| if (transformedBody != null) { |
| node.function.body = transformedBody..parent = node.function; |
| node.isExternal = false; |
| } else { |
| node.transformChildren(this); |
| } |
| _staticTypeContext.leaveMember(node); |
| return node; |
| } |
| |
| /// Returns and initializes `_extensionMemberIndex` to an index of the member |
| /// reference to the member `ExtensionMemberDescriptor`, for all extension |
| /// members in the given [library] of classes annotated with |
| /// `@staticInterop`. |
| Map<Reference, ExtensionMemberDescriptor> _createExtensionMembersIndex( |
| Library library) { |
| _extensionMemberIndex = {}; |
| library.extensions.forEach((extension) { |
| DartType onType = extension.onType; |
| if (onType is InterfaceType && |
| hasStaticInteropAnnotation(onType.className.asClass)) { |
| extension.members.forEach((descriptor) { |
| _extensionMemberIndex![descriptor.member] = descriptor; |
| }); |
| } |
| }); |
| return _extensionMemberIndex!; |
| } |
| |
| DartType get _nullableJSValueType => |
| _jsValueClass.getThisType(_coreTypes, Nullability.nullable); |
| |
| DartType get _nonNullableJSValueType => |
| _jsValueClass.getThisType(_coreTypes, Nullability.nonNullable); |
| |
| Expression _jsifyVariable(VariableDeclaration variable) => |
| StaticInvocation(_jsifyTarget, Arguments([VariableGet(variable)])); |
| |
| StaticInvocation get _globalThis => |
| StaticInvocation(_globalThisTarget, Arguments([])); |
| |
| /// Takes a list of [selectors] and returns an object off of |
| /// `globalThis`. We could optimize this with a custom method built with |
| /// js_ast. |
| Expression getObjectOffGlobalThis(Procedure node, List<String> selectors) { |
| Expression currentTarget = _globalThis; |
| for (String selector in selectors) { |
| currentTarget = _getProperty(node, currentTarget, selector); |
| } |
| return currentTarget; |
| } |
| |
| /// Returns a new function body for the given [node] external factory method |
| /// for a class annotated with `@anonymous`. |
| /// |
| /// This lowers a factory function with named arguments to the creation of a |
| /// new object literal, and a series of `setProperty` calls. |
| Block _getExternalAnonymousConstructorBody(Procedure node) { |
| List<Statement> body = []; |
| final object = VariableDeclaration('|anonymousObject', |
| initializer: StaticInvocation(_newObjectTarget, Arguments([])), |
| type: _nonNullableJSValueType); |
| body.add(object); |
| for (VariableDeclaration variable in node.function.namedParameters) { |
| body.add(ExpressionStatement( |
| _setProperty(node, VariableGet(object), variable.name!, variable))); |
| } |
| body.add(ReturnStatement(VariableGet(object))); |
| return Block(body); |
| } |
| |
| /// Returns a new function body for the given [node] external method. |
| /// |
| /// The new function body will call `js_util_wasm.callConstructorVarArgs` |
| /// for the given external method. |
| ReturnStatement _getExternalCallConstructorBody( |
| Procedure node, String constructorName) { |
| var function = node.function; |
| var callConstructorInvocation = StaticInvocation( |
| _callConstructorTarget, |
| Arguments([ |
| _globalThis, |
| StringLiteral(constructorName), |
| ListLiteral( |
| function.positionalParameters.map(_jsifyVariable).toList(), |
| typeArgument: _nonNullableJSValueType) |
| ])) |
| ..fileOffset = node.fileOffset; |
| return ReturnStatement(callConstructorInvocation); |
| } |
| |
| Expression _dartify(Expression expression) => |
| StaticInvocation(_dartifyTarget, Arguments([expression])); |
| |
| /// Returns a new [Expression] for the given [node] external getter. |
| /// |
| /// The new [Expression] is equivalent to: |
| /// `js_util_wasm.getProperty([object], [getterName])`. |
| Expression _getProperty( |
| Procedure node, Expression object, String getterName) => |
| StaticInvocation( |
| _getPropertyTarget, Arguments([object, StringLiteral(getterName)])) |
| ..fileOffset = node.fileOffset; |
| |
| /// Returns a new function body for the given [node] external getter. |
| ReturnStatement _getExternalGetterBody( |
| Procedure node, Expression object, String getterName) => |
| ReturnStatement(_dartify(_getProperty(node, object, getterName))); |
| |
| ReturnStatement _getExternalExtensionGetterBody(Procedure node) => |
| _getExternalGetterBody( |
| node, |
| VariableGet(node.function.positionalParameters.single), |
| _getExtensionMemberName(node)); |
| |
| ReturnStatement _getExternalTopLevelGetterBody( |
| Procedure node, Expression target) => |
| _getExternalGetterBody(node, target, node.name.text); |
| |
| /// Returns a new [Expression] for the given [node] external setter. |
| /// |
| /// The new [Expression] is equivalent to: |
| /// `js_util_wasm.setProperty([object], [setterName], [value])`. |
| Expression _setProperty(Procedure node, Expression object, String setterName, |
| VariableDeclaration value) => |
| StaticInvocation(_setPropertyTarget, |
| Arguments([object, StringLiteral(setterName), _jsifyVariable(value)])) |
| ..fileOffset = node.fileOffset; |
| |
| /// Returns a new function body for the given [node] external setter. |
| ReturnStatement _getExternalSetterBody(Procedure node, Expression object, |
| String setterName, VariableDeclaration value) => |
| ReturnStatement(_dartify(_setProperty(node, object, setterName, value))); |
| |
| ReturnStatement _getExternalExtensionSetterBody(Procedure node) { |
| final parameters = node.function.positionalParameters; |
| assert(parameters.length == 2); |
| return _getExternalSetterBody(node, VariableGet(parameters.first), |
| _getExtensionMemberName(node), parameters.last); |
| } |
| |
| ReturnStatement _getExternalTopLevelSetterBody( |
| Procedure node, Expression target) => |
| _getExternalSetterBody(node, target, node.name.text, |
| node.function.positionalParameters.single); |
| |
| /// Returns a new function body for the given [node] external method. |
| /// |
| /// The new function body is equivalent to: |
| /// `js_util_wasm.callMethodVarArgs([object], [methodName], [values])`. |
| ReturnStatement _getExternalMethodBody(Procedure node, Expression object, |
| String methodName, List<VariableDeclaration> values) { |
| final callMethodInvocation = _dartify(StaticInvocation( |
| _callMethodTarget, |
| Arguments([ |
| object, |
| StringLiteral(methodName), |
| ListLiteral(values.map(_jsifyVariable).toList(), |
| typeArgument: _nullableJSValueType) |
| ]))) |
| ..fileOffset = node.fileOffset; |
| return ReturnStatement(callMethodInvocation); |
| } |
| |
| ReturnStatement _getExternalExtensionMethodBody(Procedure node) { |
| final parameters = node.function.positionalParameters; |
| assert(parameters.length > 0); |
| return _getExternalMethodBody(node, VariableGet(parameters.first), |
| _getExtensionMemberName(node), parameters.sublist(1)); |
| } |
| |
| ReturnStatement _getExternalTopLevelMethodBody( |
| Procedure node, Expression target) => |
| _getExternalMethodBody( |
| node, target, node.name.text, node.function.positionalParameters); |
| |
| /// Returns the extension member name. |
| /// |
| /// Returns either the name from the `@JS` annotation if non-empty, or the |
| /// declared name of the extension member. Does not return the CFE generated |
| /// name for the top level member for this extension member. |
| String _getExtensionMemberName(Procedure node) { |
| var jsAnnotationName = getJSName(node); |
| if (jsAnnotationName.isNotEmpty) { |
| return jsAnnotationName; |
| } |
| return _extensionMemberIndex![node.reference]!.name.text; |
| } |
| } |