blob: 41599995626b9e65f7e9e7cfed57fdfdc53ce4d6 [file] [log] [blame]
// Copyright (c) 2021, 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 'package:kernel/kernel.dart';
/// Replaces js_util methods with inline calls to foreign_helper JS which
/// emits the code as a JavaScript code fragment.
class JsUtilOptimizer extends Transformer {
final Procedure _jsTarget;
final Procedure _callMethodTarget;
final List<Procedure> _callMethodUncheckedTargets;
final Procedure _getPropertyTarget;
final Procedure _setPropertyTarget;
final Procedure _setPropertyUncheckedTarget;
/// Dynamic members in js_util that interop allowed.
static final Iterable<String> _allowedInteropJsUtilMembers = <String>[
'callConstructor',
'callMethod',
'getProperty',
'jsify',
'newObject',
'setProperty'
];
final Iterable<Procedure> _allowedInteropJsUtilTargets;
final Procedure _allowInteropTarget;
final Procedure _listEmptyFactory;
final CoreTypes _coreTypes;
final StatefulStaticTypeContext _staticTypeContext;
JsUtilOptimizer(this._coreTypes, ClassHierarchy hierarchy)
: _jsTarget =
_coreTypes.index.getTopLevelProcedure('dart:_foreign_helper', 'JS'),
_callMethodTarget =
_coreTypes.index.getTopLevelProcedure('dart:js_util', 'callMethod'),
_callMethodUncheckedTargets = List<Procedure>.generate(
5,
(i) => _coreTypes.index.getTopLevelProcedure(
'dart:js_util', '_callMethodUnchecked$i')),
_getPropertyTarget = _coreTypes.index
.getTopLevelProcedure('dart:js_util', 'getProperty'),
_setPropertyTarget = _coreTypes.index
.getTopLevelProcedure('dart:js_util', 'setProperty'),
_setPropertyUncheckedTarget = _coreTypes.index
.getTopLevelProcedure('dart:js_util', '_setPropertyUnchecked'),
_allowInteropTarget =
_coreTypes.index.getTopLevelProcedure('dart:js', 'allowInterop'),
_allowedInteropJsUtilTargets = _allowedInteropJsUtilMembers.map(
(member) =>
_coreTypes.index.getTopLevelProcedure('dart:js_util', member)),
_listEmptyFactory =
_coreTypes.index.getProcedure('dart:core', 'List', 'empty'),
_staticTypeContext = StatefulStaticTypeContext.stacked(
TypeEnvironment(_coreTypes, hierarchy)) {}
@override
visitLibrary(Library lib) {
_staticTypeContext.enterLibrary(lib);
lib.transformChildren(this);
_staticTypeContext.leaveLibrary(lib);
return lib;
}
@override
defaultMember(Member node) {
_staticTypeContext.enterMember(node);
node.transformChildren(this);
_staticTypeContext.leaveMember(node);
return node;
}
/// Replaces js_util method calls with optimization when possible.
///
/// Lowers `getProperty` for any argument type straight to JS fragment call.
/// Lowers `setProperty` to `_setPropertyUnchecked` for values that are
/// not Function type and guaranteed to be interop allowed.
/// Lowers `callMethod` to `_callMethodUncheckedN` when the number of given
/// arguments is 0-4 and all arguments are guaranteed to be interop allowed.
@override
visitStaticInvocation(StaticInvocation node) {
if (node.target == _getPropertyTarget) {
node = _lowerGetProperty(node);
} else if (node.target == _setPropertyTarget) {
node = _lowerSetProperty(node);
} else if (node.target == _callMethodTarget) {
node = _lowerCallMethod(node);
}
node.transformChildren(this);
return node;
}
/// Lowers the given js_util `getProperty` call to the foreign_helper JS call
/// for any argument type. Lowers `getProperty(o, name)` to
/// `JS('Object|Null', '#.#', o, name)`.
StaticInvocation _lowerGetProperty(StaticInvocation node) {
Arguments arguments = node.arguments;
assert(arguments.types.isEmpty);
assert(arguments.positional.length == 2);
assert(arguments.named.isEmpty);
return StaticInvocation(
_jsTarget,
Arguments(
[
StringLiteral("Object|Null"),
StringLiteral("#.#"),
...arguments.positional
],
// TODO(rileyporter): Copy type from getProperty when it's generic.
types: [DynamicType()],
)..fileOffset = arguments.fileOffset)
..fileOffset = node.fileOffset;
}
/// Lowers the given js_util `setProperty` call to `_setPropertyUnchecked`
/// when the additional validation checks in `setProperty` can be elided.
///
/// Removing the checks allows further inlining by the compilers.
StaticInvocation _lowerSetProperty(StaticInvocation node) {
Arguments arguments = node.arguments;
assert(arguments.types.isEmpty);
assert(arguments.positional.length == 3);
assert(arguments.named.isEmpty);
if (!_allowedInterop(arguments.positional.last)) {
return node;
}
return StaticInvocation(_setPropertyUncheckedTarget, arguments)
..fileOffset = node.fileOffset;
}
/// Lowers the given js_util `callMethod` call to `_callMethodUncheckedN`
/// when the additional validation checks on the arguments can be elided.
///
/// Calls will be lowered when using a List literal or constant list with 0-4
/// elements for the `callMethod` arguments, or the `List.empty()` factory.
/// Removing the checks allows further inlining by the compilers.
StaticInvocation _lowerCallMethod(StaticInvocation node) {
Arguments arguments = node.arguments;
assert(arguments.types.isEmpty);
assert(arguments.positional.length == 3);
assert(arguments.named.isEmpty);
// Lower List.empty factory call.
var argumentsList = arguments.positional.last;
if (argumentsList is StaticInvocation &&
argumentsList.target == _listEmptyFactory) {
return _createNewCallMethodNode([], arguments, node.fileOffset);
}
// Lower other kinds of Lists.
var callMethodArguments;
var entryType;
if (argumentsList is ListLiteral) {
if (argumentsList.expressions.length >=
_callMethodUncheckedTargets.length) {
return node;
}
callMethodArguments = argumentsList.expressions;
entryType = argumentsList.typeArgument;
} else if (argumentsList is ConstantExpression &&
argumentsList.constant is ListConstant) {
var argumentsListConstant = argumentsList.constant as ListConstant;
if (argumentsListConstant.entries.length >=
_callMethodUncheckedTargets.length) {
return node;
}
callMethodArguments = argumentsListConstant.entries
.map((constant) => ConstantExpression(
constant, constant.getType(_staticTypeContext)))
.toList();
entryType = argumentsListConstant.typeArgument;
} else {
// Skip lowering any other type of List.
return node;
}
// Check the overall List entry type, then verify each argument if needed.
if (!_allowedInteropType(entryType)) {
for (var argument in callMethodArguments) {
if (!_allowedInterop(argument)) {
return node;
}
}
}
return _createNewCallMethodNode(
callMethodArguments, arguments, node.fileOffset);
}
/// Creates a new StaticInvocation node for `_callMethodUncheckedN` with the
/// given 0-4 arguments.
StaticInvocation _createNewCallMethodNode(
List<Expression> callMethodArguments,
Arguments arguments,
int nodeFileOffset) {
assert(callMethodArguments.length <= 4);
return StaticInvocation(
_callMethodUncheckedTargets[callMethodArguments.length],
Arguments(
[
arguments.positional[0],
arguments.positional[1],
...callMethodArguments
],
types: [],
)..fileOffset = arguments.fileOffset)
..fileOffset = nodeFileOffset;
}
/// Returns whether the given Expression is guaranteed to be allowed to
/// interop with JS.
///
/// Returns true when the node is guaranteed to be not a function:
/// - has a static DartType that is NullType or an InterfaceType that is
/// not Function or Object
/// Also returns true for allowed method calls within the JavaScript domain:
/// - dart:_foreign_helper JS
/// - dart:js `allowInterop`
/// - dart:js_util and any of the `_allowedInteropJsUtilMembers`
bool _allowedInterop(Expression node) {
// TODO(rileyporter): Detect functions that have been wrapped at some point
// with `allowInterop`
if (node is StaticInvocation) {
if (node.target == _allowInteropTarget) return true;
if (node.target == _jsTarget) return true;
if (_allowedInteropJsUtilTargets.contains(node.target)) return true;
}
return _allowedInteropType(node.getStaticType(_staticTypeContext));
}
/// Returns whether the given DartType is guaranteed to be not a function
/// and therefore allowed to interop with JS.
bool _allowedInteropType(DartType type) {
if (type is InterfaceType) {
return type.classNode != _coreTypes.functionClass &&
type.classNode != _coreTypes.objectClass;
} else {
// Only other DartType guaranteed to not be a function.
return type is NullType;
}
}
}