| // 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:_js_interop_checks/src/js_interop.dart' |
| show |
| calculateTransitiveImportsOfJsInteropIfUsed, |
| getJSName, |
| hasAnonymousAnnotation, |
| hasStaticInteropAnnotation, |
| hasJSInteropAnnotation; |
| import 'package:_js_interop_checks/src/transformations/js_util_optimizer.dart' |
| show InlineExtensionIndex; |
| import 'package:_js_interop_checks/src/transformations/static_interop_class_eraser.dart'; |
| import 'package:dart2wasm/js_runtime_blob.dart'; |
| import 'package:kernel/ast.dart'; |
| import 'package:kernel/class_hierarchy.dart'; |
| import 'package:kernel/core_types.dart'; |
| import 'package:kernel/type_environment.dart'; |
| |
| enum _AnnotationType { import, export } |
| |
| enum _MethodType { |
| jsObjectLiteralConstructor, |
| constructor, |
| getter, |
| method, |
| setter, |
| operator, |
| } |
| |
| bool parametersNeedParens(List<String> parameters) => |
| parameters.isEmpty || parameters.length > 1; |
| |
| class _MethodLoweringConfig { |
| final Procedure procedure; |
| final _MethodType type; |
| final String jsString; |
| final InlineExtensionIndex _inlineExtensionIndex; |
| late final bool isConstructor = |
| type == _MethodType.jsObjectLiteralConstructor || |
| type == _MethodType.constructor; |
| late final bool firstParameterIsObject = |
| _inlineExtensionIndex.isInstanceInteropMember(procedure); |
| late final List<VariableDeclaration> parameters = |
| type == _MethodType.jsObjectLiteralConstructor |
| ? function.namedParameters |
| : function.positionalParameters; |
| late String tag = procedure.name.text.replaceAll(RegExp(r'[^a-zA-Z_]'), '_'); |
| |
| _MethodLoweringConfig( |
| this.procedure, this.type, this.jsString, this._inlineExtensionIndex); |
| |
| FunctionNode get function => procedure.function; |
| Uri get fileUri => procedure.fileUri; |
| |
| String generateJS(List<String> parameters) { |
| String object = isConstructor |
| ? '' |
| : firstParameterIsObject |
| ? parameters[0] |
| : 'globalThis'; |
| List<String> callArguments = |
| firstParameterIsObject ? parameters.sublist(1) : parameters; |
| String callArgumentsString = callArguments.join(','); |
| String functionParameters = firstParameterIsObject |
| ? '$object${callArguments.isEmpty ? '' : ',$callArgumentsString'}' |
| : callArgumentsString; |
| String bodyString; |
| switch (type) { |
| case _MethodType.jsObjectLiteralConstructor: |
| List<String> keys = |
| function.namedParameters.map((named) => named.name!).toList(); |
| List<String> keyValuePairs = []; |
| for (int i = 0; i < parameters.length; i++) { |
| keyValuePairs.add('${keys[i]}: ${parameters[i]}'); |
| } |
| bodyString = '({${keyValuePairs.join(',')}})'; |
| break; |
| case _MethodType.constructor: |
| bodyString = 'new $jsString($callArgumentsString)'; |
| break; |
| case _MethodType.getter: |
| bodyString = '$object.$jsString'; |
| break; |
| case _MethodType.method: |
| bodyString = '$object.$jsString($callArgumentsString)'; |
| break; |
| case _MethodType.setter: |
| bodyString = '$object.$jsString = $callArgumentsString'; |
| break; |
| case _MethodType.operator: |
| if (jsString == '[]') { |
| bodyString = '$object[${callArguments[0]}]'; |
| } else if (jsString == '[]=') { |
| bodyString = '$object[${callArguments[0]}] = ${callArguments[1]}'; |
| } else { |
| throw 'Unsupported operator: $jsString'; |
| } |
| break; |
| } |
| if (parametersNeedParens(parameters)) { |
| return '($functionParameters) => $bodyString'; |
| } else { |
| return '$functionParameters => $bodyString'; |
| } |
| } |
| } |
| |
| /// Lowers static interop to JS, generating specialized JS methods as required. |
| /// We lower methods to JS, but wait to emit the runtime until after we complete |
| /// translation. Ideally, we'd do everything after translation, but |
| /// unfortunately the TFA assumes classes with external factory constructors |
| /// that aren't mark with `entry-point` are abstract, and their methods thus get |
| /// replaced with `throw`s. Since we have to lower factory methods anyways, we |
| /// go ahead and lower everything, let the TFA tree shake, and then emit JS only |
| /// for the remaining nodes. We can revisit this if it becomes a performance |
| /// issue. |
| /// TODO(joshualitt): Only support JS types in static interop APIs, then |
| /// simpify this code significantly and clean up the nullabilities. |
| class _JSLowerer extends Transformer { |
| final Procedure _dartifyRawTarget; |
| final Procedure _jsifyRawTarget; |
| final Procedure _isDartFunctionWrappedTarget; |
| final Procedure _wrapDartFunctionTarget; |
| final Procedure _jsObjectFromDartObjectTarget; |
| final Procedure _jsValueBoxTarget; |
| final Procedure _jsValueUnboxTarget; |
| final Procedure _allowInteropTarget; |
| final Procedure _functionToJSTarget; |
| final Procedure _inlineJSTarget; |
| final Procedure _numToIntTarget; |
| final Constructor _jsValueConstructor; |
| final Class _wasmExternRefClass; |
| final Class _pragmaClass; |
| final Field _pragmaName; |
| final Field _pragmaOptions; |
| bool _replaceProcedureWithInlineJS = false; |
| late String _inlineJSImportName; |
| final Map<Procedure, String> jsMethods = {}; |
| int _methodN = 1; |
| late Library _library; |
| late String _libraryJSString; |
| final Map<Procedure, Map<int, Procedure>> _overloadedProcedures = {}; |
| |
| final CoreTypes _coreTypes; |
| late InlineExtensionIndex _inlineExtensionIndex; |
| final StatefulStaticTypeContext _staticTypeContext; |
| |
| _JSLowerer(this._coreTypes, ClassHierarchy hierarchy) |
| : _dartifyRawTarget = _coreTypes.index |
| .getTopLevelProcedure('dart:_js_helper', 'dartifyRaw'), |
| _jsifyRawTarget = _coreTypes.index |
| .getTopLevelProcedure('dart:_js_helper', 'jsifyRaw'), |
| _isDartFunctionWrappedTarget = _coreTypes.index |
| .getTopLevelProcedure('dart:_js_helper', '_isDartFunctionWrapped'), |
| _wrapDartFunctionTarget = _coreTypes.index |
| .getTopLevelProcedure('dart:_js_helper', '_wrapDartFunction'), |
| _jsObjectFromDartObjectTarget = _coreTypes.index |
| .getTopLevelProcedure('dart:_js_helper', 'jsObjectFromDartObject'), |
| _jsValueConstructor = _coreTypes.index |
| .getClass('dart:_js_helper', 'JSValue') |
| .constructors |
| .single, |
| _jsValueBoxTarget = _coreTypes.index |
| .getClass('dart:_js_helper', 'JSValue') |
| .procedures |
| .firstWhere((p) => p.name.text == 'box'), |
| _jsValueUnboxTarget = _coreTypes.index |
| .getClass('dart:_js_helper', 'JSValue') |
| .procedures |
| .firstWhere((p) => p.name.text == 'unbox'), |
| _allowInteropTarget = _coreTypes.index |
| .getTopLevelProcedure('dart:js_util', 'allowInterop'), |
| _inlineJSTarget = |
| _coreTypes.index.getTopLevelProcedure('dart:_js_helper', 'JS'), |
| _wasmExternRefClass = |
| _coreTypes.index.getClass('dart:wasm', 'WasmExternRef'), |
| _numToIntTarget = _coreTypes.index |
| .getClass('dart:core', 'num') |
| .procedures |
| .firstWhere((p) => p.name.text == 'toInt'), |
| _functionToJSTarget = _coreTypes.index.getTopLevelProcedure( |
| 'dart:js_interop', 'FunctionToJSExportedDartFunction|get#toJS'), |
| _pragmaClass = _coreTypes.pragmaClass, |
| _pragmaName = _coreTypes.pragmaName, |
| _pragmaOptions = _coreTypes.pragmaOptions, |
| _staticTypeContext = StatefulStaticTypeContext.stacked( |
| TypeEnvironment(_coreTypes, hierarchy)) {} |
| |
| @override |
| Library visitLibrary(Library lib) { |
| _library = lib; |
| _inlineExtensionIndex = InlineExtensionIndex(_library); |
| _libraryJSString = getJSName(_library); |
| if (_libraryJSString.isNotEmpty) { |
| _libraryJSString = '$_libraryJSString.'; |
| } |
| _staticTypeContext.enterLibrary(lib); |
| lib.transformChildren(this); |
| _staticTypeContext.leaveLibrary(lib); |
| return lib; |
| } |
| |
| @override |
| Member defaultMember(Member node) { |
| _staticTypeContext.enterMember(node); |
| node.transformChildren(this); |
| _staticTypeContext.leaveMember(node); |
| return node; |
| } |
| |
| @override |
| Expression visitStaticInvocation(StaticInvocation node) { |
| node = super.visitStaticInvocation(node) as StaticInvocation; |
| List<Expression> positional = node.arguments.positional; |
| Procedure target = node.target; |
| if (target == _allowInteropTarget) { |
| Expression argument = positional.single; |
| DartType functionType = argument.getStaticType(_staticTypeContext); |
| return _allowInterop(node.target, functionType as FunctionType, argument); |
| } else if (target == _functionToJSTarget) { |
| Expression argument = positional.single; |
| DartType functionType = argument.getStaticType(_staticTypeContext); |
| return _functionToJS(target, functionType as FunctionType, argument); |
| } else if (node.target == _inlineJSTarget) { |
| return _expandInlineJS(node.target, node); |
| } else if (target.isExternal) { |
| tryTransformProcedure(target); |
| } |
| |
| if (_overloadedProcedures.containsKey(target)) { |
| final overloads = _overloadedProcedures[target]!; |
| int positionalLength = positional.length; |
| if (overloads.containsKey(positionalLength)) { |
| return StaticInvocation(overloads[positionalLength]!, node.arguments); |
| } |
| } |
| return node; |
| } |
| |
| 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)}'; |
| |
| _MethodType _getTypeForNonExtensionMember(Procedure node) { |
| if (node.isGetter) { |
| return _MethodType.getter; |
| } else if (node.isSetter) { |
| return _MethodType.setter; |
| } else { |
| assert(node.kind == ProcedureKind.Method); |
| return _MethodType.method; |
| } |
| } |
| |
| _MethodType _getTypeForInlineClassMember(InlineClassMemberKind kind) { |
| if (kind == InlineClassMemberKind.Getter) { |
| return _MethodType.getter; |
| } else if (kind == InlineClassMemberKind.Setter) { |
| return _MethodType.setter; |
| } else { |
| assert(kind == InlineClassMemberKind.Method); |
| return _MethodType.method; |
| } |
| } |
| |
| bool tryTransformProcedure(Procedure node) { |
| if (_overloadedProcedures.containsKey(node)) { |
| return true; |
| } |
| |
| if (node.isExternal) { |
| _MethodType? type; |
| String jsString = ''; |
| if (node.enclosingClass != null && |
| hasJSInteropAnnotation(node.enclosingClass!)) { |
| Class cls = node.enclosingClass!; |
| jsString = _getTopLevelJSString(cls, cls.name); |
| if (node.isFactory) { |
| if (hasAnonymousAnnotation(cls)) { |
| // TODO(joshualitt): These should really be lowered at the |
| // invocation level. |
| _specializeJSObjectLiteral(_MethodLoweringConfig( |
| node, |
| _MethodType.jsObjectLiteralConstructor, |
| jsString, |
| _inlineExtensionIndex)); |
| return true; |
| } else { |
| type = _MethodType.constructor; |
| } |
| } else { |
| String memberSelectorString = _getJSString(node, node.name.text); |
| jsString = '$jsString.$memberSelectorString'; |
| type = _getTypeForNonExtensionMember(node); |
| } |
| } else if (node.isInlineClassMember) { |
| InlineClassMemberDescriptor? nodeDescriptor = |
| _inlineExtensionIndex.getInlineDescriptor(node.reference); |
| if (nodeDescriptor != null) { |
| InlineClass cls = |
| _inlineExtensionIndex.getInlineClass(node.reference)!; |
| InlineClassMemberKind kind = nodeDescriptor.kind; |
| jsString = _getTopLevelJSString(cls, cls.name); |
| if (kind == InlineClassMemberKind.Constructor || |
| kind == InlineClassMemberKind.Factory) { |
| type = _MethodType.constructor; |
| } else if (nodeDescriptor.isStatic) { |
| String memberSelectorString = |
| _getJSString(node, nodeDescriptor.name.text); |
| jsString = '$jsString.$memberSelectorString'; |
| type = _getTypeForInlineClassMember(kind); |
| } else { |
| jsString = _getJSString(node, nodeDescriptor.name.text); |
| if (_inlineExtensionIndex.isGetter(node)) { |
| type = _MethodType.getter; |
| } else if (_inlineExtensionIndex.isSetter(node)) { |
| type = _MethodType.setter; |
| } else if (_inlineExtensionIndex.isMethod(node)) { |
| type = _MethodType.method; |
| } else if (_inlineExtensionIndex.isOperator(node)) { |
| type = _MethodType.operator; |
| } |
| } |
| } |
| } else if (node.isExtensionMember) { |
| ExtensionMemberDescriptor? nodeDescriptor = |
| _inlineExtensionIndex.getExtensionDescriptor(node.reference); |
| if (nodeDescriptor != null) { |
| if (!nodeDescriptor.isStatic) { |
| jsString = _getJSString(node, nodeDescriptor.name.text); |
| if (_inlineExtensionIndex.isGetter(node)) { |
| type = _MethodType.getter; |
| } else if (_inlineExtensionIndex.isSetter(node)) { |
| type = _MethodType.setter; |
| } else if (_inlineExtensionIndex.isMethod(node)) { |
| type = _MethodType.method; |
| } else if (_inlineExtensionIndex.isOperator(node)) { |
| type = _MethodType.operator; |
| } |
| } |
| } |
| } else if (hasJSInteropAnnotation(node)) { |
| jsString = _getTopLevelJSString(node, node.name.text); |
| type = _getTypeForNonExtensionMember(node); |
| } |
| if (type != null) { |
| _specializeProcedureWithOptionalParameters( |
| _MethodLoweringConfig(node, type, jsString, _inlineExtensionIndex)); |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| @override |
| Procedure visitProcedure(Procedure node) { |
| _staticTypeContext.enterMember(node); |
| if (!tryTransformProcedure(node)) { |
| // Under very restricted circumstances, we will make a procedure external |
| // and clear it's body. See the description on [_expandInlineJS] for more |
| // details. |
| _replaceProcedureWithInlineJS = false; |
| node.transformChildren(this); |
| if (_replaceProcedureWithInlineJS) { |
| node.isStatic = true; |
| node.isExternal = true; |
| node.function.body = null; |
| _annotateProcedure(node, _inlineJSImportName, _AnnotationType.import); |
| _replaceProcedureWithInlineJS = false; |
| } |
| } |
| _staticTypeContext.leaveMember(node); |
| return node; |
| } |
| |
| bool _isStaticInteropType(DartType type) => |
| (type is InterfaceType && |
| hasStaticInteropAnnotation(type.className.asClass)) || |
| (type is InlineType && hasJSInteropAnnotation(type.inlineClass)); |
| |
| // We could generate something more human readable, but for now we just |
| // generate something short and unique. |
| String generateMethodName() => '_${_methodN++}'; |
| |
| DartType get _nonNullableObjectType => |
| _coreTypes.objectRawType(Nullability.nonNullable); |
| |
| DartType get _nullableWasmExternRefType => |
| _wasmExternRefClass.getThisType(_coreTypes, Nullability.nullable); |
| |
| DartType get _nonNullableWasmExternRefType => |
| _wasmExternRefClass.getThisType(_coreTypes, Nullability.nonNullable); |
| |
| Expression _variableCheckConstant( |
| VariableDeclaration variable, Constant constant) => |
| StaticInvocation(_coreTypes.identicalProcedure, |
| Arguments([VariableGet(variable), ConstantExpression(constant)])); |
| |
| Procedure _jsifyTarget(DartType type) { |
| if (_isStaticInteropType(type)) { |
| return _jsValueUnboxTarget; |
| } else { |
| return _jsifyRawTarget; |
| } |
| } |
| |
| void _annotateProcedure( |
| Procedure procedure, String pragmaOptionString, _AnnotationType type) { |
| String pragmaName; |
| switch (type) { |
| case _AnnotationType.import: |
| pragmaName = 'import'; |
| break; |
| case _AnnotationType.export: |
| pragmaName = 'export'; |
| break; |
| } |
| procedure.addAnnotation( |
| ConstantExpression(InstanceConstant(_pragmaClass.reference, [], { |
| _pragmaName.fieldReference: StringConstant('wasm:$pragmaName'), |
| _pragmaOptions.fieldReference: StringConstant('$pragmaOptionString') |
| }))); |
| } |
| |
| Procedure _addInteropProcedure(String name, String pragmaOptionString, |
| FunctionNode function, Uri fileUri, _AnnotationType type, |
| {required bool isExternal}) { |
| final procedure = Procedure( |
| Name(name, _library), ProcedureKind.Method, function, |
| isStatic: true, isExternal: isExternal, fileUri: fileUri) |
| ..isNonNullableByDefault = true; |
| _annotateProcedure(procedure, pragmaOptionString, type); |
| _library.addProcedure(procedure); |
| return procedure; |
| } |
| |
| StaticInvocation _invokeOneArg(Procedure target, Expression arg) => |
| StaticInvocation(target, Arguments([arg])); |
| |
| Statement _generateDispatchCase( |
| FunctionType function, |
| VariableDeclaration callbackVariable, |
| List<VariableDeclaration> positionalParameters, |
| int requiredParameterCount, |
| {required bool boxExternRef}) { |
| List<Expression> callbackArguments = []; |
| for (int i = 0; i < requiredParameterCount; i++) { |
| DartType callbackParameterType = function.positionalParameters[i]; |
| Expression expression; |
| VariableGet v = VariableGet(positionalParameters[i]); |
| if (_isStaticInteropType(callbackParameterType) && boxExternRef) { |
| expression = _createJSValue(v); |
| } else { |
| expression = AsExpression( |
| _invokeOneArg(_dartifyRawTarget, v), callbackParameterType); |
| } |
| callbackArguments.add(expression); |
| } |
| return ReturnStatement(StaticInvocation( |
| _jsifyTarget(function.returnType), |
| Arguments([ |
| FunctionInvocation( |
| FunctionAccessKind.FunctionType, |
| AsExpression(VariableGet(callbackVariable), function), |
| Arguments(callbackArguments), |
| functionType: function), |
| ]))); |
| } |
| |
| bool _needsArgumentsLength(FunctionType type) => |
| type.requiredParameterCount < type.positionalParameters.length; |
| |
| /// Creates a callback trampoline for the given [function]. This callback |
| /// trampoline expects a Dart callback as its first argument, then an integer |
| /// value(double type) indicating the position of the last defined |
| /// argument(only for callbacks that take optional parameters), followed by |
| /// all of the arguments to the Dart callback as JS objects. The trampoline |
| /// will `dartifyRaw` all incoming JS objects and then cast them to their |
| /// appropriate types, dispatch, and then `jsifyRaw` any returned value. |
| /// [_createFunctionTrampoline] Returns a [String] function name representing |
| /// the name of the wrapping function. |
| String _createFunctionTrampoline(Procedure node, FunctionType function, |
| {required bool boxExternRef}) { |
| // Create arguments for each positional parameter in the function. These |
| // arguments will be JS objects. The generated wrapper will cast each |
| // argument to the correct type. The first argument to this function will |
| // be the Dart callback, which will be cast to the supplied [FunctionType] |
| // before being invoked. If the callback takes optional parameters then, the |
| // second argument will be a `double` indicating the last defined argument. |
| int parameterId = 1; |
| final callbackVariable = VariableDeclaration('callback', |
| type: _nonNullableObjectType, isSynthesized: true); |
| VariableDeclaration? argumentsLength; |
| if (_needsArgumentsLength(function)) { |
| argumentsLength = VariableDeclaration('argumentsLength', |
| type: _coreTypes.doubleNonNullableRawType, isSynthesized: true); |
| } |
| |
| // Initialize variable declarations. |
| List<VariableDeclaration> positionalParameters = []; |
| for (int j = 0; j < function.positionalParameters.length; j++) { |
| positionalParameters.add(VariableDeclaration('x${parameterId++}', |
| type: _nullableWasmExternRefType, isSynthesized: true)); |
| } |
| |
| // Build the body of a function trampoline. To support default arguments, we |
| // find the last defined argument in JS, that is the last argument which was |
| // explicitly passed by the user, and then we dispatch to a Dart function |
| // with the right number of arguments. |
| // |
| // First we handle cases where some or all arguments are undefined. |
| // TODO(joshualitt): Consider using a switch instead. |
| List<Statement> dispatchCases = []; |
| for (int i = function.requiredParameterCount + 1; |
| i <= function.positionalParameters.length; |
| i++) { |
| dispatchCases.add(IfStatement( |
| _variableCheckConstant( |
| argumentsLength!, DoubleConstant(i.toDouble())), |
| _generateDispatchCase( |
| function, callbackVariable, positionalParameters, i, |
| boxExternRef: boxExternRef), |
| null)); |
| } |
| |
| // Finally handle the case where only required parameters are passed. |
| dispatchCases.add(_generateDispatchCase(function, callbackVariable, |
| positionalParameters, function.requiredParameterCount, |
| boxExternRef: boxExternRef)); |
| Statement functionTrampolineBody = Block(dispatchCases); |
| |
| // Create a new procedure for the callback trampoline. This procedure will |
| // be exported from Wasm to JS so it can be called from JS. The argument |
| // returned from the supplied callback will be converted with `jsifyRaw` to |
| // a native JS value before being returned to JS. |
| final functionTrampolineName = generateMethodName(); |
| _addInteropProcedure( |
| functionTrampolineName, |
| functionTrampolineName, |
| FunctionNode(functionTrampolineBody, |
| positionalParameters: [ |
| callbackVariable, |
| if (argumentsLength != null) argumentsLength |
| ].followedBy(positionalParameters).toList(), |
| returnType: _nullableWasmExternRefType) |
| ..fileOffset = node.fileOffset, |
| node.fileUri, |
| _AnnotationType.export, |
| isExternal: false); |
| return functionTrampolineName; |
| } |
| |
| /// Returns a JS method that wraps a Dart callback in a JS wrapper. |
| Procedure _getJSWrapperFunction( |
| FunctionType type, String functionTrampolineName, Uri fileUri) { |
| List<String> jsParameters = []; |
| for (int i = 0; i < type.positionalParameters.length; i++) { |
| jsParameters.add('x$i'); |
| } |
| String jsParametersString = jsParameters.join(','); |
| String dartArguments = 'f'; |
| bool needsArguments = _needsArgumentsLength(type); |
| if (needsArguments) { |
| dartArguments = '$dartArguments,arguments.length'; |
| } |
| if (jsParameters.isNotEmpty) { |
| dartArguments = '$dartArguments,$jsParametersString'; |
| } |
| |
| // Create Dart procedure stub. |
| final jsMethodName = functionTrampolineName; |
| Procedure dartProcedure = _addInteropProcedure( |
| '|$jsMethodName', |
| 'dart2wasm.$jsMethodName', |
| FunctionNode(null, |
| positionalParameters: [ |
| VariableDeclaration('dartFunction', |
| type: _nonNullableWasmExternRefType, isSynthesized: true) |
| ], |
| returnType: _nonNullableWasmExternRefType), |
| fileUri, |
| _AnnotationType.import, |
| isExternal: true); |
| |
| // Create JS method. |
| // Note: We have to use a regular function for the inner closure in some |
| // cases because we need access to `arguments`. |
| if (needsArguments) { |
| jsMethods[dartProcedure] = "$jsMethodName: f => " |
| "finalizeWrapper(f, function($jsParametersString) {" |
| " return dartInstance.exports.$functionTrampolineName($dartArguments) " |
| "})"; |
| } else { |
| if (parametersNeedParens(jsParameters)) { |
| jsParametersString = '($jsParametersString)'; |
| } |
| jsMethods[dartProcedure] = "$jsMethodName: f => " |
| "finalizeWrapper(f,$jsParametersString => " |
| "dartInstance.exports.$functionTrampolineName($dartArguments))"; |
| } |
| |
| return dartProcedure; |
| } |
| |
| /// Lowers an invocation of `allowInterop<type>(foo)` to: |
| /// |
| /// let #var = foo in |
| /// _isDartFunctionWrapped<type>(#var) ? |
| /// #var : |
| /// _wrapDartFunction<type>(#var, jsWrapperFunction(#var)); |
| /// |
| /// The use of two functions here is necessary because we do not allow |
| /// `WasmExternRef` to be an argument or return type for a tear off. |
| /// |
| /// Note: _wrapDartFunction tracks wrapped Dart functions in a map. When |
| /// these Dart functions flow to JS, they are replaced by their wrappers. If |
| /// the wrapper should ever flow back into Dart then it will be replaced by |
| /// the original Dart function. |
| Expression _allowInterop( |
| Procedure node, FunctionType type, Expression argument) { |
| String functionTrampolineName = |
| _createFunctionTrampoline(node, type, boxExternRef: false); |
| Procedure jsWrapperFunction = |
| _getJSWrapperFunction(type, functionTrampolineName, node.fileUri); |
| VariableDeclaration v = VariableDeclaration('#var', |
| initializer: argument, type: type, isSynthesized: true); |
| return Let( |
| v, |
| ConditionalExpression( |
| StaticInvocation(_isDartFunctionWrappedTarget, |
| Arguments([VariableGet(v)], types: [type])), |
| VariableGet(v), |
| StaticInvocation( |
| _wrapDartFunctionTarget, |
| Arguments([ |
| VariableGet(v), |
| StaticInvocation( |
| jsWrapperFunction, |
| Arguments([ |
| StaticInvocation(_jsObjectFromDartObjectTarget, |
| Arguments([VariableGet(v)])) |
| ])), |
| ], types: [ |
| type |
| ])), |
| type)); |
| } |
| |
| Expression _createJSValue(Expression value) => |
| ConstructorInvocation(_jsValueConstructor, Arguments([value])); |
| |
| /// Lowers an invocation of `<Function>.toJS` to: |
| /// |
| /// JSValue(jsWrapperFunction(<Function>)) |
| Expression _functionToJS( |
| Procedure node, FunctionType type, Expression argument) { |
| String functionTrampolineName = |
| _createFunctionTrampoline(node, type, boxExternRef: true); |
| Procedure jsWrapperFunction = |
| _getJSWrapperFunction(type, functionTrampolineName, node.fileUri); |
| return _createJSValue(StaticInvocation( |
| jsWrapperFunction, |
| Arguments([ |
| StaticInvocation(_jsObjectFromDartObjectTarget, Arguments([argument])) |
| ]))); |
| } |
| |
| InstanceInvocation _invokeMethod( |
| VariableDeclaration receiver, Procedure target) => |
| InstanceInvocation(InstanceAccessKind.Instance, VariableGet(receiver), |
| target.name, Arguments([]), |
| interfaceTarget: target, |
| functionType: |
| target.function.computeFunctionType(Nullability.nonNullable)); |
| |
| void _specializeJSObjectLiteral(_MethodLoweringConfig config) { |
| Procedure procedure = config.procedure; |
| Statement? transformedBody = |
| _specializeProcedure(config, config.parameters); |
| procedure.function.body = transformedBody..parent = procedure.function; |
| procedure.isExternal = false; |
| } |
| |
| /// Creates a Dart procedure that calls out to a specialized JS method for the |
| /// given [config] and returns the created procedure. |
| Statement _specializeProcedure(_MethodLoweringConfig config, |
| List<VariableDeclaration> originalParameters) { |
| // Initialize variable declarations. |
| List<String> jsParameterStrings = []; |
| List<VariableDeclaration> dartPositionalParameters = []; |
| for (int j = 0; j < originalParameters.length; j++) { |
| String parameterString = 'x$j'; |
| dartPositionalParameters.add(VariableDeclaration(parameterString, |
| type: _nullableWasmExternRefType, isSynthesized: true)); |
| jsParameterStrings.add(parameterString); |
| } |
| |
| // Create Dart procedure stub for JS method. |
| String jsMethodName = generateMethodName(); |
| final dartProcedure = _addInteropProcedure( |
| '|$jsMethodName', |
| 'dart2wasm.$jsMethodName', |
| FunctionNode(null, |
| positionalParameters: dartPositionalParameters, |
| returnType: _nullableWasmExternRefType), |
| config.fileUri, |
| _AnnotationType.import, |
| isExternal: true); |
| |
| // Create JS method |
| jsMethods[dartProcedure] = |
| "$jsMethodName: ${config.generateJS(jsParameterStrings)}"; |
| |
| // Return the replacement body. |
| // Because we simply don't have enough information, we leave all JS numbers |
| // as doubles. However, in cases where we know the user expects an `int` we |
| // insert a cast. We also let static interop types flow through without |
| // conversion, both as arguments, and as the return type. |
| DartType returnType = config.function.returnType; |
| Expression invocation = StaticInvocation( |
| dartProcedure, |
| Arguments(originalParameters |
| .map<Expression>((value) => StaticInvocation( |
| _jsifyTarget(value.type), Arguments([VariableGet(value)]))) |
| .toList())); |
| DartType returnTypeOverride = returnType == _coreTypes.intNullableRawType |
| ? _coreTypes.doubleNullableRawType |
| : returnType == _coreTypes.intNonNullableRawType |
| ? _coreTypes.doubleNonNullableRawType |
| : returnType; |
| if (returnType is VoidType) { |
| return ExpressionStatement(invocation); |
| } else { |
| Expression expression; |
| if (_isStaticInteropType(returnType)) { |
| // TODO(joshualitt): Expose boxed `JSNull` and `JSUndefined` to Dart |
| // code after migrating existing users of js interop on Dart2Wasm. |
| // expression = _createJSValue(invocation); |
| expression = _invokeOneArg(_jsValueBoxTarget, invocation); |
| } else { |
| expression = AsExpression( |
| _convertReturnType(returnType, returnTypeOverride, |
| _invokeOneArg(_dartifyRawTarget, invocation)), |
| returnType); |
| } |
| return ReturnStatement(expression); |
| } |
| } |
| |
| // Handles any necessary return type conversions. Today this is just for |
| // handling the case where a user wants us to coerce a JS number to an int |
| // instead of a double. |
| Expression _convertReturnType( |
| DartType returnType, DartType returnTypeOverride, Expression expression) { |
| if (returnType == _coreTypes.intNullableRawType || |
| returnType == _coreTypes.intNonNullableRawType) { |
| VariableDeclaration v = VariableDeclaration('#var', |
| initializer: expression, |
| type: returnTypeOverride, |
| isSynthesized: true); |
| return Let( |
| v, |
| ConditionalExpression( |
| _variableCheckConstant(v, NullConstant()), |
| ConstantExpression(NullConstant()), |
| _invokeMethod(v, _numToIntTarget), |
| returnType)); |
| } else { |
| return expression; |
| } |
| } |
| |
| /// Specializes a JS method for a given [config] while handling optional |
| /// parameters. We will generate one procedure for every optional argument, |
| /// and make all of the arguments to that procedure required. For the time |
| /// being to support tearoffs we simply replace the body of the original |
| /// procedure, but leave 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. |
| void _specializeProcedureWithOptionalParameters( |
| _MethodLoweringConfig config) { |
| // First handle optional arguments by creating a specialized procedure for |
| // each optional, and make all of the arguments required. These will be used |
| // when we visit static invocations to specialize calls. |
| Procedure procedure = config.procedure; |
| FunctionNode function = procedure.function; |
| int requiredParameterCount = function.requiredParameterCount; |
| List<VariableDeclaration> positionalParameters = |
| function.positionalParameters; |
| Map<int, Procedure> overloadMap = {}; |
| for (int i = requiredParameterCount; i < positionalParameters.length; i++) { |
| List<VariableDeclaration> newParameters = positionalParameters |
| .sublist(0, i) |
| .map((v) => VariableDeclaration(v.name, flags: v.flags, type: v.type)) |
| .toList(); |
| Statement body = _specializeProcedure(config, newParameters); |
| String procedureName = '|${generateMethodName()}'; |
| Procedure specializedProcedure = Procedure( |
| Name(procedureName, _library), |
| procedure.kind, |
| FunctionNode(body, |
| requiredParameterCount: i, |
| positionalParameters: newParameters, |
| returnType: function.returnType), |
| fileUri: procedure.fileUri) |
| ..isStatic = procedure.isStatic |
| ..fileOffset = procedure.fileOffset; |
| if (procedure.parent is Class) { |
| procedure.enclosingClass!.addProcedure(specializedProcedure); |
| } else { |
| procedure.enclosingLibrary.addProcedure(specializedProcedure); |
| } |
| overloadMap[i] = specializedProcedure; |
| } |
| |
| // Finally, create a specialized body to replace the original external |
| // procedure. This will be used for tearoffs and in cases where all |
| // arguments are specified at the call site. |
| Statement transformedBody = |
| _specializeProcedure(config, positionalParameters); |
| function.body = transformedBody..parent = function; |
| procedure.isExternal = false; |
| overloadMap[positionalParameters.length] = procedure; |
| _overloadedProcedures[procedure] = overloadMap; |
| } |
| |
| Procedure? _tryGetEnclosingProcedure(TreeNode? node) { |
| while (node is! Procedure) { |
| node = node?.parent; |
| if (node == null) { |
| return null; |
| } |
| } |
| return node; |
| } |
| |
| /// We will replace the enclosing procedure if: |
| /// 1) The enclosing procedure is static. |
| /// 2) The enclosing procedure has a body with a single statement, and that |
| /// statement is just a [StaticInvocation] of the inline JS helper. |
| /// 3) All of the arguments to `inlineJS` are [VariableGet]s. (this is |
| /// checked by [_expandInlineJS]). |
| bool _shouldReplaceEnclosingProcedure(StaticInvocation node) { |
| Procedure? enclosingProcedure = _tryGetEnclosingProcedure(node); |
| if (enclosingProcedure == null) { |
| return false; |
| } |
| Statement enclosingBody = enclosingProcedure.function.body!; |
| Expression? expression; |
| if (enclosingBody is ReturnStatement) { |
| expression = enclosingBody.expression; |
| } else if (enclosingBody is Block && enclosingBody.statements.length == 1) { |
| Statement statement = enclosingBody.statements.single; |
| if (statement is ExpressionStatement) { |
| expression = statement.expression; |
| } else if (statement is ReturnStatement) { |
| expression = statement.expression; |
| } |
| } |
| return expression == node; |
| } |
| |
| /// Calls to the `JS` helper are replaced in one of two ways: |
| /// 1) By a static invocation to an external stub method that imports |
| /// the JS function. |
| /// 2) Under restricted circumstances the entire enclosing procedure will be |
| /// replaced by an external stub method that imports the JS function. See |
| /// [_shouldReplaceEnclosingProcedure] for more details. |
| Expression _expandInlineJS(Procedure inlineJSNode, StaticInvocation node) { |
| Arguments arguments = node.arguments; |
| List<Expression> originalArguments = arguments.positional.sublist(1); |
| List<VariableDeclaration> dartPositionalParameters = []; |
| bool allArgumentsAreGet = true; |
| for (int j = 0; j < originalArguments.length; j++) { |
| Expression originalArgument = originalArguments[j]; |
| String parameterString = 'x$j'; |
| DartType type = originalArgument.getStaticType(_staticTypeContext); |
| dartPositionalParameters.add(VariableDeclaration(parameterString, |
| type: type, isSynthesized: true)); |
| if (originalArgument is! VariableGet) { |
| allArgumentsAreGet = false; |
| } |
| } |
| |
| assert(arguments.positional[0] is StringLiteral, |
| "Code template must be a StringLiteral"); |
| String codeTemplate = (arguments.positional[0] as StringLiteral).value; |
| String jsMethodName = generateMethodName(); |
| _inlineJSImportName = 'dart2wasm.$jsMethodName'; |
| _replaceProcedureWithInlineJS = |
| allArgumentsAreGet && _shouldReplaceEnclosingProcedure(node); |
| Procedure dartProcedure; |
| Expression result; |
| if (_replaceProcedureWithInlineJS) { |
| dartProcedure = _tryGetEnclosingProcedure(node)!; |
| result = InvalidExpression("Unreachable"); |
| } else { |
| dartProcedure = _addInteropProcedure( |
| '|$jsMethodName', |
| _inlineJSImportName, |
| FunctionNode(null, |
| positionalParameters: dartPositionalParameters, |
| returnType: arguments.types.single), |
| inlineJSNode.fileUri, |
| _AnnotationType.import, |
| isExternal: true); |
| result = StaticInvocation(dartProcedure, Arguments(originalArguments)); |
| } |
| jsMethods[dartProcedure] = "$jsMethodName: $codeTemplate"; |
| return result; |
| } |
| } |
| |
| Map<Procedure, String> _performJSInteropTransformations( |
| Component component, |
| CoreTypes coreTypes, |
| ClassHierarchy classHierarchy, |
| Set<Library> interopDependentLibraries) { |
| final jsLowerer = _JSLowerer(coreTypes, classHierarchy); |
| for (Library library in interopDependentLibraries) { |
| jsLowerer.visitLibrary(library); |
| } |
| |
| // We want static types to help us specialize methods based on receivers. |
| // Therefore, erasure must come after the lowering. |
| final staticInteropClassEraser = StaticInteropClassEraser(coreTypes, null, |
| libraryForJavaScriptObject: 'dart:_js_helper', |
| classNameOfJavaScriptObject: 'JSValue', |
| additionalCoreLibraries: {'_js_helper', '_js_types', 'js_interop'}); |
| for (Library library in interopDependentLibraries) { |
| staticInteropClassEraser.visitLibrary(library); |
| } |
| return jsLowerer.jsMethods; |
| } |
| |
| class JSRuntimeFinalizer { |
| final Map<Procedure, String> allJSMethods; |
| |
| JSRuntimeFinalizer(this.allJSMethods); |
| |
| String generate(Iterable<Procedure> translatedProcedures) { |
| Set<Procedure> usedProcedures = {}; |
| List<String> usedJSMethods = []; |
| for (Procedure p in translatedProcedures) { |
| if (usedProcedures.add(p) && allJSMethods.containsKey(p)) { |
| usedJSMethods.add(allJSMethods[p]!); |
| } |
| } |
| return ''' |
| $jsRuntimeBlobPart1 |
| ${usedJSMethods.join(',\n')} |
| $jsRuntimeBlobPart2 |
| '''; |
| } |
| } |
| |
| JSRuntimeFinalizer createJSRuntimeFinalizer( |
| Component component, CoreTypes coreTypes, ClassHierarchy classHierarchy) { |
| Set<Library> transitiveImportingJSInterop = { |
| ...?calculateTransitiveImportsOfJsInteropIfUsed( |
| component, Uri.parse("package:js/js.dart")), |
| ...?calculateTransitiveImportsOfJsInteropIfUsed( |
| component, Uri.parse("dart:_js_annotations")), |
| ...?calculateTransitiveImportsOfJsInteropIfUsed( |
| component, Uri.parse("dart:_js_helper")), |
| }; |
| Map<Procedure, String> jsInteropMethods = {}; |
| jsInteropMethods = _performJSInteropTransformations( |
| component, coreTypes, classHierarchy, transitiveImportingJSInterop); |
| return JSRuntimeFinalizer(jsInteropMethods); |
| } |