blob: 23e0093e231548d449b5c26d4e0e1a98c953e42a [file] [log] [blame]
// 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 _jsifyRawTarget;
final Procedure _dartifyTarget;
final Procedure _newObjectTarget;
final Procedure _wrapDartCallbackTarget;
final Procedure _allowInteropTarget;
final Class _jsValueClass;
final Class _wasmAnyRefClass;
final Class _objectClass;
final Class _pragmaClass;
final Field _pragmaName;
final Field _pragmaOptions;
int _callbackTrampolineN = 1;
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'),
_jsifyRawTarget = _coreTypes.index
.getTopLevelProcedure('dart:js_util_wasm', 'jsifyRaw'),
_dartifyTarget = _coreTypes.index
.getTopLevelProcedure('dart:js_util_wasm', 'dartify'),
_wrapDartCallbackTarget = _coreTypes.index
.getTopLevelProcedure('dart:js_util_wasm', '_wrapDartCallback'),
_newObjectTarget = _coreTypes.index
.getTopLevelProcedure('dart:js_util_wasm', 'newObject'),
_allowInteropTarget = _coreTypes.index
.getTopLevelProcedure('dart:js_util_wasm', 'allowInterop'),
_jsValueClass =
_coreTypes.index.getClass('dart:js_util_wasm', 'JSValue'),
_wasmAnyRefClass = _coreTypes.index.getClass('dart:wasm', 'WasmAnyRef'),
_objectClass = _coreTypes.objectClass,
_pragmaClass = _coreTypes.pragmaClass,
_pragmaName = _coreTypes.pragmaName,
_pragmaOptions = _coreTypes.pragmaOptions,
_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
StaticInvocation visitStaticInvocation(StaticInvocation node) {
if (node.target == _allowInteropTarget) {
Expression argument = node.arguments.positional.single;
DartType functionType = argument.getStaticType(_staticTypeContext);
return _allowInterop(node.target, functionType as FunctionType, argument);
}
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 _dartify(Expression expression) =>
StaticInvocation(_dartifyTarget, Arguments([expression]));
/// Creates a callback trampoline for the given [function]. This callback
/// trampoline expects a Dart callback as its first argument, followed by all
/// of the arguments to the Dart callback as Dart objects. The trampoline will
/// cast all incoming Dart objects to the appropriate types, dispatch, and
/// then `jsifyRaw` any returned value. [_createCallbackTrampoline] Returns a
/// [String] function name representing the name of the wrapping function.
/// TODO(joshualitt): Share callback trampolines if the [FunctionType]
/// matches.
String _createCallbackTrampoline(Procedure node, FunctionType function) {
int fileOffset = node.fileOffset;
Library library = node.enclosingLibrary;
// Create arguments for each positional parameter in the function. These
// arguments will be converted in JS to Dart 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.
int parameterId = 1;
DartType nonNullableObjectType =
_objectClass.getThisType(_coreTypes, Nullability.nonNullable);
final callbackVariable =
VariableDeclaration('callback', type: nonNullableObjectType);
List<VariableDeclaration> positionalParameters = [callbackVariable];
List<Expression> callbackArguments = [];
DartType nullableObjectType =
_objectClass.getThisType(_coreTypes, Nullability.nullable);
for (DartType type in function.positionalParameters) {
VariableDeclaration variable =
VariableDeclaration('x${parameterId++}', type: nullableObjectType);
positionalParameters.add(variable);
callbackArguments.add(AsExpression(VariableGet(variable), type));
}
// 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.
DartType nullableWasmAnyRefType =
_wasmAnyRefClass.getThisType(_coreTypes, Nullability.nullable);
final callbackTrampolineName =
'|_callbackTrampoline${_callbackTrampolineN++}';
final callbackTrampolineImportName = '\$$callbackTrampolineName';
final callbackTrampoline = Procedure(
Name(callbackTrampolineName, library),
ProcedureKind.Method,
FunctionNode(
ReturnStatement(StaticInvocation(
_jsifyRawTarget,
Arguments([
FunctionInvocation(
FunctionAccessKind.FunctionType,
AsExpression(VariableGet(callbackVariable), function),
Arguments(callbackArguments),
functionType: function),
]))),
positionalParameters: positionalParameters,
returnType: nullableWasmAnyRefType)
..fileOffset = fileOffset,
isStatic: true,
fileUri: node.fileUri)
..fileOffset = fileOffset
..isNonNullableByDefault = true;
callbackTrampoline.addAnnotation(
ConstantExpression(InstanceConstant(_pragmaClass.reference, [], {
_pragmaName.fieldReference: StringConstant('wasm:export'),
_pragmaOptions.fieldReference:
StringConstant(callbackTrampolineImportName)
})));
library.addProcedure(callbackTrampoline);
return callbackTrampolineImportName;
}
/// Lowers a [StaticInvocation] of `allowInterop` to
/// [_createCallbackTrampoline] followed by `_wrapDartCallback`.
StaticInvocation _allowInterop(
Procedure node, FunctionType type, Expression argument) {
String callbackTrampolineName = _createCallbackTrampoline(node, type);
return StaticInvocation(_wrapDartCallbackTarget,
Arguments([argument, StringLiteral(callbackTrampolineName)]));
}
Expression _jsifyVariable(Procedure node, VariableDeclaration variable) {
if (variable.type is FunctionType) {
return _allowInterop(
node, variable.type as FunctionType, VariableGet(variable));
} else {
return 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((arg) => _jsifyVariable(node, arg))
.toList(),
typeArgument: _nonNullableJSValueType)
]))
..fileOffset = node.fileOffset;
return ReturnStatement(callConstructorInvocation);
}
/// 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(node, 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((value) => _jsifyVariable(node, value)).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;
}
}