Optimize js_util callConstructor for 0-4 arguments.
Some usages of `callConstructor` will have unnecessary checks
removed, when checks on the arguments can be elided. The
compilers will optimize further if they can.
Example optimizations: https://paste.googleplex.com/4594240957972480
Change-Id: I0e6e7e4d1268580cbfab84599b1c9da6fc64e7c7
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/213114
Reviewed-by: Srujan Gaddam <srujzs@google.com>
Commit-Queue: Riley Porter <rileyporter@google.com>
diff --git a/pkg/_js_interop_checks/lib/src/transformations/js_util_optimizer.dart b/pkg/_js_interop_checks/lib/src/transformations/js_util_optimizer.dart
index 4159999..c5933d5 100644
--- a/pkg/_js_interop_checks/lib/src/transformations/js_util_optimizer.dart
+++ b/pkg/_js_interop_checks/lib/src/transformations/js_util_optimizer.dart
@@ -14,6 +14,8 @@
final Procedure _jsTarget;
final Procedure _callMethodTarget;
final List<Procedure> _callMethodUncheckedTargets;
+ final Procedure _callConstructorTarget;
+ final List<Procedure> _callConstructorUncheckedTargets;
final Procedure _getPropertyTarget;
final Procedure _setPropertyTarget;
final Procedure _setPropertyUncheckedTarget;
@@ -43,6 +45,12 @@
5,
(i) => _coreTypes.index.getTopLevelProcedure(
'dart:js_util', '_callMethodUnchecked$i')),
+ _callConstructorTarget = _coreTypes.index
+ .getTopLevelProcedure('dart:js_util', 'callConstructor'),
+ _callConstructorUncheckedTargets = List<Procedure>.generate(
+ 5,
+ (i) => _coreTypes.index.getTopLevelProcedure(
+ 'dart:js_util', '_callConstructorUnchecked$i')),
_getPropertyTarget = _coreTypes.index
.getTopLevelProcedure('dart:js_util', 'getProperty'),
_setPropertyTarget = _coreTypes.index
@@ -90,6 +98,8 @@
node = _lowerSetProperty(node);
} else if (node.target == _callMethodTarget) {
node = _lowerCallMethod(node);
+ } else if (node.target == _callConstructorTarget) {
+ node = _lowerCallConstructor(node);
}
node.transformChildren(this);
return node;
@@ -147,70 +157,102 @@
assert(arguments.positional.length == 3);
assert(arguments.named.isEmpty);
- // Lower List.empty factory call.
- var argumentsList = arguments.positional.last;
+ return _lowerToCallUnchecked(
+ node, _callMethodUncheckedTargets, arguments.positional.sublist(0, 2));
+ }
+
+ /// Lowers the given js_util `callConstructor` call to `_callConstructorUncheckedN`
+ /// 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 `callConstructor` arguments, or the `List.empty()` factory.
+ /// Removing the checks allows further inlining by the compilers.
+ StaticInvocation _lowerCallConstructor(StaticInvocation node) {
+ Arguments arguments = node.arguments;
+ assert(arguments.types.isEmpty);
+ assert(arguments.positional.length == 2);
+ assert(arguments.named.isEmpty);
+
+ return _lowerToCallUnchecked(
+ node, _callConstructorUncheckedTargets, [arguments.positional.first]);
+ }
+
+ /// Helper to lower the given [node] to the relevant unchecked target in the
+ /// [callUncheckedTargets] based on whether the validation checks on the
+ /// [originalArguments] can be elided.
+ ///
+ /// Calls will be lowered when using a List literal or constant list with 0-4
+ /// arguments, or the `List.empty()` factory. Removing the checks allows further
+ /// inlining by the compilers.
+ StaticInvocation _lowerToCallUnchecked(
+ StaticInvocation node,
+ List<Procedure> callUncheckedTargets,
+ List<Expression> originalArguments) {
+ var argumentsList = node.arguments.positional.last;
+ // Lower arguments in a List.empty factory call.
if (argumentsList is StaticInvocation &&
argumentsList.target == _listEmptyFactory) {
- return _createNewCallMethodNode([], arguments, node.fileOffset);
+ return _createCallUncheckedNode(callUncheckedTargets, [],
+ originalArguments, node.fileOffset, node.arguments.fileOffset);
}
- // Lower other kinds of Lists.
- var callMethodArguments;
+ // Lower arguments in other kinds of Lists.
+ var callUncheckedArguments;
var entryType;
if (argumentsList is ListLiteral) {
- if (argumentsList.expressions.length >=
- _callMethodUncheckedTargets.length) {
+ if (argumentsList.expressions.length >= callUncheckedTargets.length) {
return node;
}
- callMethodArguments = argumentsList.expressions;
+ callUncheckedArguments = 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) {
+ if (argumentsListConstant.entries.length >= callUncheckedTargets.length) {
return node;
}
- callMethodArguments = argumentsListConstant.entries
+ callUncheckedArguments = argumentsListConstant.entries
.map((constant) => ConstantExpression(
constant, constant.getType(_staticTypeContext)))
.toList();
entryType = argumentsListConstant.typeArgument;
} else {
- // Skip lowering any other type of List.
+ // Skip lowering arguments in any other type of List.
return node;
}
- // Check the overall List entry type, then verify each argument if needed.
+ // Check the arguments List type, then verify each argument if needed.
if (!_allowedInteropType(entryType)) {
- for (var argument in callMethodArguments) {
+ for (var argument in callUncheckedArguments) {
if (!_allowedInterop(argument)) {
return node;
}
}
}
- return _createNewCallMethodNode(
- callMethodArguments, arguments, node.fileOffset);
+ return _createCallUncheckedNode(
+ callUncheckedTargets,
+ callUncheckedArguments,
+ originalArguments,
+ node.fileOffset,
+ node.arguments.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);
+ /// Creates a new StaticInvocation node for the relevant unchecked target
+ /// with the given 0-4 arguments.
+ StaticInvocation _createCallUncheckedNode(
+ List<Procedure> callUncheckedTargets,
+ List<Expression> callUncheckedArguments,
+ List<Expression> originalArguments,
+ int nodeFileOffset,
+ int argumentsFileOffset) {
+ assert(callUncheckedArguments.length <= 4);
return StaticInvocation(
- _callMethodUncheckedTargets[callMethodArguments.length],
+ callUncheckedTargets[callUncheckedArguments.length],
Arguments(
- [
- arguments.positional[0],
- arguments.positional[1],
- ...callMethodArguments
- ],
+ [...originalArguments, ...callUncheckedArguments],
types: [],
- )..fileOffset = arguments.fileOffset)
+ )..fileOffset = argumentsFileOffset)
..fileOffset = nodeFileOffset;
}
diff --git a/sdk/lib/js_util/js_util.dart b/sdk/lib/js_util/js_util.dart
index ee3dd0f..bfe6cf3 100644
--- a/sdk/lib/js_util/js_util.dart
+++ b/sdk/lib/js_util/js_util.dart
@@ -195,6 +195,38 @@
// return _wrapToDart(jsObj);
}
+/// Unchecked version for 0 arguments, only used in a CFE transformation.
+@pragma('dart2js:tryInline')
+dynamic _callConstructorUnchecked0(Object constr) {
+ return JS('Object', 'new #()', constr);
+}
+
+/// Unchecked version for 1 argument, only used in a CFE transformation.
+@pragma('dart2js:tryInline')
+dynamic _callConstructorUnchecked1(Object constr, Object? arg1) {
+ return JS('Object', 'new #(#)', constr, arg1);
+}
+
+/// Unchecked version for 2 arguments, only used in a CFE transformation.
+@pragma('dart2js:tryInline')
+dynamic _callConstructorUnchecked2(Object constr, Object? arg1, Object? arg2) {
+ return JS('Object', 'new #(#, #)', constr, arg1, arg2);
+}
+
+/// Unchecked version for 3 arguments, only used in a CFE transformation.
+@pragma('dart2js:tryInline')
+dynamic _callConstructorUnchecked3(
+ Object constr, Object? arg1, Object? arg2, Object? arg3) {
+ return JS('Object', 'new #(#, #, #)', constr, arg1, arg2, arg3);
+}
+
+/// Unchecked version for 4 arguments, only used in a CFE transformation.
+@pragma('dart2js:tryInline')
+dynamic _callConstructorUnchecked4(
+ Object constr, Object? arg1, Object? arg2, Object? arg3, Object? arg4) {
+ return JS('Object', 'new #(#, #, #, #)', constr, arg1, arg2, arg3, arg4);
+}
+
/// Exception for when the promise is rejected with a `null` or `undefined`
/// value.
///
diff --git a/tests/lib/js/js_util/properties_test.dart b/tests/lib/js/js_util/properties_test.dart
index 326de20..84a09c8 100644
--- a/tests/lib/js/js_util/properties_test.dart
+++ b/tests/lib/js/js_util/properties_test.dart
@@ -85,6 +85,19 @@
external five(a, b, c, d, e);
}
+@JS()
+external get Zero;
+@JS()
+external get One;
+@JS()
+external get Two;
+@JS()
+external get Three;
+@JS()
+external get Four;
+@JS()
+external get Five;
+
main() {
eval(r"""
function Foo(a) {
@@ -157,6 +170,25 @@
CallMethodTest.prototype.five = function(a, b, c, d, e) {
return 'five';
}
+
+ function Zero() {
+ this.count = 0;
+ }
+ function One(a) {
+ this.count = 1;
+ }
+ function Two(a, b) {
+ this.count = 2;
+ }
+ function Three(a, b, c) {
+ this.count = 3;
+ }
+ function Four(a, b, c, d) {
+ this.count = 4;
+ }
+ function Five(a, b, c, d, e) {
+ this.count = 5;
+ }
""");
group('newObject', () {
@@ -543,8 +575,131 @@
group('callConstructor', () {
test('typed object', () {
- Foo f = js_util.callConstructor(JSFooType, [42]);
+ var f = js_util.callConstructor(JSFooType, [42]);
expect(f.a, equals(42));
+
+ var f2 =
+ js_util.callConstructor(js_util.getProperty(f, 'constructor'), [5]);
+ expect(f2.a, equals(5));
+ });
+
+ test('typed literal', () {
+ ExampleTypedLiteral literal = js_util.callConstructor(
+ js_util.getProperty(ExampleTypedLiteral(), 'constructor'), []);
+ expect(literal.a, equals(null));
+ });
+
+ test('callConstructor with List edge cases', () {
+ expect(
+ js_util.getProperty(
+ js_util.callConstructor(Zero, List.empty()), 'count'),
+ equals(0));
+ expect(
+ js_util.getProperty(
+ js_util.callConstructor(Zero, List<int>.empty()), 'count'),
+ equals(0));
+
+ expect(
+ js_util.getProperty(
+ js_util.callConstructor(Two, List<int>.filled(2, 0)), 'count'),
+ equals(2));
+ expect(
+ js_util.getProperty(
+ js_util.callConstructor(Three, List<int>.generate(3, (i) => i)),
+ 'count'),
+ equals(3));
+
+ Iterable<String> iterableStrings = <String>['foo', 'bar'];
+ expect(
+ js_util.getProperty(
+ js_util.callConstructor(Two, List.of(iterableStrings)), 'count'),
+ equals(2));
+
+ const l1 = [1, 2];
+ const l2 = [3, 4];
+ expect(
+ js_util.getProperty(
+ js_util.callConstructor(Four, List.from(l1)..addAll(l2)),
+ 'count'),
+ equals(4));
+ expect(
+ js_util.getProperty(js_util.callConstructor(Four, l1 + l2), 'count'),
+ equals(4));
+ expect(
+ js_util.getProperty(
+ js_util.callConstructor(Four, List.unmodifiable([1, 2, 3, 4])),
+ 'count'),
+ equals(4));
+
+ var setElements = {1, 2};
+ expect(
+ js_util.getProperty(
+ js_util.callConstructor(Two, setElements.toList()), 'count'),
+ equals(2));
+
+ var spreadList = [1, 2, 3];
+ expect(
+ js_util.getProperty(
+ js_util.callConstructor(Four, [1, ...spreadList]), 'count'),
+ equals(4));
+ });
+
+ test('edge cases for lowering to _callConstructorUncheckedN', () {
+ expect(js_util.getProperty(js_util.callConstructor(Zero, []), 'count'),
+ equals(0));
+ expect(js_util.getProperty(js_util.callConstructor(One, [1]), 'count'),
+ equals(1));
+ expect(
+ js_util.getProperty(
+ js_util.callConstructor(Four, [1, 2, 3, 4]), 'count'),
+ equals(4));
+ expect(
+ js_util.getProperty(
+ js_util.callConstructor(Five, [1, 2, 3, 4, 5]), 'count'),
+ equals(5));
+
+ // List with a type declaration, short circuits element checking
+ expect(
+ js_util.getProperty(
+ js_util.callConstructor(Two, <int>[1, 2]), 'count'),
+ equals(2));
+
+ // List as a variable instead of a List Literal or constant
+ var list = [1, 2];
+ expect(js_util.getProperty(js_util.callConstructor(Two, list), 'count'),
+ equals(2));
+
+ // Mixed types of elements to check in the given list.
+ var x = 4;
+ var str = 'cat';
+ var b = false;
+ var evens = [2, 4, 6];
+ expect(
+ js_util.getProperty(
+ js_util.callConstructor(Four, [x, str, b, evens]), 'count'),
+ equals(4));
+ var obj = Object();
+ expect(js_util.getProperty(js_util.callConstructor(One, [obj]), 'count'),
+ equals(1));
+ var nullElement = null;
+ expect(
+ js_util.getProperty(
+ js_util.callConstructor(One, [nullElement]), 'count'),
+ equals(1));
+
+ // const lists.
+ expect(
+ js_util.getProperty(js_util.callConstructor(One, const [3]), 'count'),
+ equals(1));
+ const constList = [10, 20, 30];
+ expect(
+ js_util.getProperty(
+ js_util.callConstructor(Three, constList), 'count'),
+ equals(3));
+ expect(
+ js_util.getProperty(
+ js_util.callConstructor(One, DartClass.staticConstList), 'count'),
+ equals(1));
});
});
}
diff --git a/tests/lib_2/js/js_util/properties_test.dart b/tests/lib_2/js/js_util/properties_test.dart
index 8479414..c19fe14 100644
--- a/tests/lib_2/js/js_util/properties_test.dart
+++ b/tests/lib_2/js/js_util/properties_test.dart
@@ -87,6 +87,19 @@
external five(a, b, c, d, e);
}
+@JS()
+external get Zero;
+@JS()
+external get One;
+@JS()
+external get Two;
+@JS()
+external get Three;
+@JS()
+external get Four;
+@JS()
+external get Five;
+
main() {
eval(r"""
function Foo(a) {
@@ -159,6 +172,25 @@
CallMethodTest.prototype.five = function(a, b, c, d, e) {
return 'five';
}
+
+ function Zero() {
+ this.count = 0;
+ }
+ function One(a) {
+ this.count = 1;
+ }
+ function Two(a, b) {
+ this.count = 2;
+ }
+ function Three(a, b, c) {
+ this.count = 3;
+ }
+ function Four(a, b, c, d) {
+ this.count = 4;
+ }
+ function Five(a, b, c, d, e) {
+ this.count = 5;
+ }
""");
group('newObject', () {
@@ -547,8 +579,138 @@
group('callConstructor', () {
test('typed object', () {
- Foo f = js_util.callConstructor(JSFooType, [42]);
+ var f = js_util.callConstructor(JSFooType, [42]);
expect(f.a, equals(42));
+
+ var f2 =
+ js_util.callConstructor(js_util.getProperty(f, 'constructor'), [5]);
+ expect(f2.a, equals(5));
+ });
+
+ test('typed literal', () {
+ ExampleTypedLiteral literal = js_util.callConstructor(
+ js_util.getProperty(ExampleTypedLiteral(), 'constructor'), []);
+ expect(literal.a, equals(null));
+ });
+
+ test('callConstructor with List edge cases', () {
+ expect(
+ js_util.getProperty(js_util.callConstructor(Zero, List()), 'count'),
+ equals(0));
+ expect(
+ js_util.getProperty(
+ js_util.callConstructor(Zero, List<int>()), 'count'),
+ equals(0));
+ expect(
+ js_util.getProperty(
+ js_util.callConstructor(Zero, List.empty()), 'count'),
+ equals(0));
+ expect(
+ js_util.getProperty(
+ js_util.callConstructor(Zero, List<int>.empty()), 'count'),
+ equals(0));
+
+ expect(
+ js_util.getProperty(
+ js_util.callConstructor(Two, List<int>.filled(2, 0)), 'count'),
+ equals(2));
+ expect(
+ js_util.getProperty(
+ js_util.callConstructor(Three, List<int>.generate(3, (i) => i)),
+ 'count'),
+ equals(3));
+
+ Iterable<String> iterableStrings = <String>['foo', 'bar'];
+ expect(
+ js_util.getProperty(
+ js_util.callConstructor(Two, List.of(iterableStrings)), 'count'),
+ equals(2));
+
+ const l1 = [1, 2];
+ const l2 = [3, 4];
+ expect(
+ js_util.getProperty(
+ js_util.callConstructor(Four, List.from(l1)..addAll(l2)),
+ 'count'),
+ equals(4));
+ expect(
+ js_util.getProperty(js_util.callConstructor(Four, l1 + l2), 'count'),
+ equals(4));
+ expect(
+ js_util.getProperty(
+ js_util.callConstructor(Four, List.unmodifiable([1, 2, 3, 4])),
+ 'count'),
+ equals(4));
+
+ var setElements = {1, 2};
+ expect(
+ js_util.getProperty(
+ js_util.callConstructor(Two, setElements.toList()), 'count'),
+ equals(2));
+
+ var spreadList = [1, 2, 3];
+ expect(
+ js_util.getProperty(
+ js_util.callConstructor(Four, [1, ...spreadList]), 'count'),
+ equals(4));
+ });
+
+ test('edge cases for lowering to _callConstructorUncheckedN', () {
+ expect(js_util.getProperty(js_util.callConstructor(Zero, []), 'count'),
+ equals(0));
+ expect(js_util.getProperty(js_util.callConstructor(One, [1]), 'count'),
+ equals(1));
+ expect(
+ js_util.getProperty(
+ js_util.callConstructor(Four, [1, 2, 3, 4]), 'count'),
+ equals(4));
+ expect(
+ js_util.getProperty(
+ js_util.callConstructor(Five, [1, 2, 3, 4, 5]), 'count'),
+ equals(5));
+
+ // List with a type declaration, short circuits element checking
+ expect(
+ js_util.getProperty(
+ js_util.callConstructor(Two, <int>[1, 2]), 'count'),
+ equals(2));
+
+ // List as a variable instead of a List Literal or constant
+ var list = [1, 2];
+ expect(js_util.getProperty(js_util.callConstructor(Two, list), 'count'),
+ equals(2));
+
+ // Mixed types of elements to check in the given list.
+ var x = 4;
+ var str = 'cat';
+ var b = false;
+ var evens = [2, 4, 6];
+ expect(
+ js_util.getProperty(
+ js_util.callConstructor(Four, [x, str, b, evens]), 'count'),
+ equals(4));
+ var obj = Object();
+ expect(js_util.getProperty(js_util.callConstructor(One, [obj]), 'count'),
+ equals(1));
+ var nullElement = null;
+ expect(
+ js_util.getProperty(
+ js_util.callConstructor(One, [nullElement]), 'count'),
+ equals(1));
+
+ // const lists.
+ expect(
+ js_util.getProperty(js_util.callConstructor(One, const [3]), 'count'),
+ equals(1));
+ const constList = [10, 20, 30];
+ expect(
+ js_util.getProperty(
+ js_util.callConstructor(Three, constList), 'count'),
+ equals(3));
+ expect(
+ js_util.getProperty(
+ js_util.callConstructor(One, DartClass.staticConstList), 'count'),
+ equals(1));
});
});
}