blob: 45ff5721e24c135372cb6b3d5d9dc78482666872 [file] [log] [blame] [edit]
// 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 ExtensionIndex;
import 'package:kernel/ast.dart';
import 'package:kernel/type_environment.dart';
import 'method_collector.dart';
import 'util.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._extensionIndex.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;
/// Whether this config is associated with a constructor or factory.
bool get isConstructor;
/// Whether this is a setter where there's no return value.
bool get isSetter;
/// 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(',');
String functionParameters = firstParameterIsObject
? '$object${callArguments.isEmpty ? '' : ',$callArgumentsString'}'
: callArgumentsString;
final body = bodyString(object, callArguments);
if (parametersNeedParens(parameterNames)) {
functionParameters = '($functionParameters)';
}
return isSetter
? '$functionParameters => { $body }'
: '$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 i = 0; i < parameters.length; i++) {
final VariableDeclaration parameter = parameters[i];
final DartType parameterType = parameter.type;
final interopFunctionParameterType =
parameterType == _util.coreTypes.doubleNonNullableRawType
? _util.coreTypes.doubleNonNullableRawType
: _util.nullableWasmExternRefType;
String parameterString = 'x$i';
dartPositionalParameters.add(VariableDeclaration(parameterString,
type: interopFunctionParameterType, 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: function.returnType is VoidType
? VoidType()
: _util.nullableWasmExternRefType),
fileUri,
AnnotationType.import,
library: interopMethod.enclosingLibrary,
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 are cached based on number of arguments they're passed at the
// call sites.
Procedure? cachedProcedure =
_overloadedProcedures[interopMethod]?[parameters.length];
if (cachedProcedure != null) return cachedProcedure;
final dartProcedure = _getRawInteropProcedure();
_overloadedProcedures.putIfAbsent(
interopMethod, () => {})[parameters.length] = dartProcedure;
return dartProcedure;
}
}
/// 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.
final interopProcedure = _getOrCreateInteropProcedure();
final interopProcedureType =
interopProcedure.computeSignatureOrFunctionType();
final List<Expression> jsifiedArguments = [];
for (int i = 0; i < parameters.length; i += 1) {
jsifiedArguments.add(jsifyValue(
parameters[i],
interopProcedureType.positionalParameters[i],
factory._util,
factory._staticTypeContext.typeEnvironment));
}
final invocation =
StaticInvocation(interopProcedure, Arguments(jsifiedArguments));
return _util.castInvocationForReturn(invocation, function.returnType);
}
}
class _ConstructorSpecializer extends _ProcedureSpecializer {
_ConstructorSpecializer(super.factory, super.interopMethod, super.jsString);
@override
bool get isConstructor => true;
@override
bool get isSetter => false;
@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
bool get isSetter => 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
bool get isSetter => true;
@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
bool get isSetter => 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
bool get isSetter => switch (jsString) {
'[]' => false,
'[]=' => true,
_ => throw UnimplementedError(
'External operator $jsString is unsupported for static interop. '
'Please file a request in the SDK if you want it to be supported.')
};
@override
String bodyString(String object, List<String> callArguments) {
return isSetter
? '$object[${callArguments[0]}] = ${callArguments[1]}'
: '$object[${callArguments[0]}]';
}
}
/// 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 interopProcedure = _getOrCreateInteropProcedure();
final interopProcedureType =
interopProcedure.computeSignatureOrFunctionType();
final List<Expression> jsifiedArguments = [];
final List<Expression> arguments = invocation.arguments.positional;
for (int i = 0; i < arguments.length; i += 1) {
final temp = VariableDeclaration(null,
initializer: arguments[i],
type: arguments[i].getStaticType(factory._staticTypeContext),
isSynthesized: true);
jsifiedArguments.add(Let(
temp,
jsifyValue(temp, interopProcedureType.positionalParameters[i],
factory._util, factory._staticTypeContext.typeEnvironment)));
}
final staticInvocation =
StaticInvocation(interopProcedure, Arguments(jsifiedArguments));
return _util.castInvocationForReturn(
staticInvocation, invocation.getStaticType(_staticTypeContext));
}
}
class _ConstructorInvocationSpecializer
extends _PositionalInvocationSpecializer {
_ConstructorInvocationSpecializer(
super.factory, super.interopMethod, super.jsString, super.invocation);
@override
bool get isConstructor => true;
@override
bool get isSetter => false;
@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
bool get isSetter => 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
bool get isSetter => false;
@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 interopProcedureType =
interopProcedure.computeSignatureOrFunctionType();
final arguments =
parameters.map<Expression>((decl) => namedArgs[decl.name!]!).toList();
final List<Expression> jsifiedArguments = [];
for (int i = 0; i < arguments.length; i += 1) {
final temp = VariableDeclaration(null,
initializer: arguments[i],
type: arguments[i].getStaticType(factory._staticTypeContext),
isSynthesized: true);
jsifiedArguments.add(Let(
temp,
jsifyValue(temp, interopProcedureType.positionalParameters[i],
factory._util, factory._staticTypeContext.typeEnvironment)));
}
assert(factory._extensionIndex.isStaticInteropType(function.returnType));
return invokeOneArg(_util.jsValueBoxTarget,
StaticInvocation(interopProcedure, Arguments(jsifiedArguments)));
}
}
class InteropSpecializerFactory {
final StatefulStaticTypeContext _staticTypeContext;
final CoreTypesUtil _util;
final MethodCollector _methodCollector;
/// Maps an interop procedure to the trampolines based on number of arguments
/// they take.
///
/// `_overloadedProcedures[procedure][numberOfArgs]` gives the trampoline that
/// should be called with `numberOfArgs` arguments.
final Map<Procedure, Map<int, Procedure>> _overloadedProcedures = {};
/// Maps an interop procedure for a JS object literal to trampolines based on
/// named arguments that are passed at invocation sites. For example:
///
/// ```
/// extension type Literal._(JSObject _) implements JSObject {
/// external factory Literal.fact({double a, String b, bool c});
/// }
///
/// Literal.fact(a: 1.2);
/// Literal.fact(a: 3.4, b: 'a');
/// Literal.fact(a: 5.6, b: 'b');
/// ```
///
/// Here we map the procedure for `Literal.fact` to interop procedures, based
/// on the named arguments passed.
///
/// The `String` keys are named arguments passed in the call sites, joined by
/// `|`. E.g. `"a|b"` in the second and third calls above.
final Map<Procedure, Map<String, Procedure>> _jsObjectLiteralMethods = {};
late final ExtensionIndex _extensionIndex;
InteropSpecializerFactory(this._staticTypeContext, this._util,
this._methodCollector, this._extensionIndex);
String _getJSString(Annotatable a, String initial) {
String selectorString = getJSName(a);
if (selectorString.isEmpty) {
selectorString = initial;
}
return selectorString;
}
String _getTopLevelJSString(
Annotatable a, String writtenName, Library enclosingLibrary) {
final name = _getJSString(a, writtenName);
final libraryName = getJSName(enclosingLibrary);
if (libraryName.isEmpty) return name;
return '$libraryName.$name';
}
/// 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 (_extensionIndex.isGetter(node)) {
return _GetterSpecializer(this, node, jsString);
} else if (_extensionIndex.isSetter(node)) {
return _SetterSpecializer(this, node, jsString);
} else if (_extensionIndex.isOperator(node)) {
return _OperatorSpecializer(this, node, jsString);
} else if (_extensionIndex.isMethod(node)) {
return _MethodSpecializer(this, node, jsString);
}
} else {
if (_extensionIndex.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, cls.enclosingLibrary);
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 = _extensionIndex.getExtensionTypeDescriptor(node);
if (nodeDescriptor != null) {
final cls = _extensionIndex.getExtensionType(node)!;
final clsString =
_getTopLevelJSString(cls, cls.name, node.enclosingLibrary);
final kind = nodeDescriptor.kind;
if (kind == ExtensionTypeMemberKind.Constructor ||
kind == ExtensionTypeMemberKind.Factory) {
return _getSpecializerForConstructor(
_extensionIndex.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 = _extensionIndex.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, node.enclosingLibrary),
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) {
return false;
}
final specializer = _getSpecializer(node);
if (specializer == null) {
return false;
}
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;
}
}