-
Notifications
You must be signed in to change notification settings - Fork 1.1k
/
J2clClinitPrunerPass.java
354 lines (304 loc) · 12.2 KB
/
J2clClinitPrunerPass.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
/*
* Copyright 2016 The Closure Compiler Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.javascript.jscomp;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.base.Strings;
import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
import com.google.javascript.jscomp.NodeTraversal.Callback;
import com.google.javascript.jscomp.NodeTraversal.ChangeScopeRootCallback;
import com.google.javascript.rhino.Node;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.annotation.Nullable;
/**
* An optimization pass to prune J2CL clinits.
*/
public class J2clClinitPrunerPass implements CompilerPass {
private final AbstractCompiler compiler;
private boolean madeChange = false;
private final List<Node> changedScopeNodes;
J2clClinitPrunerPass(AbstractCompiler compiler, List<Node> changedScopeNodes) {
this.compiler = compiler;
this.changedScopeNodes = changedScopeNodes;
}
@Override
public void process(Node externs, Node root) {
if (!J2clSourceFileChecker.shouldRunJ2clPasses(compiler)) {
return;
}
RedundantClinitPruner redundantClinitPruner = new RedundantClinitPruner();
NodeTraversal.traverseEs6ScopeRoots(
compiler,
root,
getNonNestedParentScopeNodes(),
redundantClinitPruner,
redundantClinitPruner, // FunctionCallback
true);
NodeTraversal.traverseEs6ScopeRoots(
compiler, root, changedScopeNodes, new LookAheadRedundantClinitPruner(), false);
NodeTraversal.traverseEs6ScopeRoots(
compiler, root, changedScopeNodes, new EmptyClinitPruner(), false);
if (madeChange) {
// This invocation is ~70% of ALL the cost of j2clOptBundlePass :(
new PureFunctionIdentifier.Driver(compiler, null).process(externs, root);
}
}
private List<Node> getNonNestedParentScopeNodes() {
return changedScopeNodes == null
? null
: NodeUtil.removeNestedChangeScopeNodes(
NodeUtil.getParentChangeScopeNodes(changedScopeNodes));
}
/** Removes redundant clinit calls inside method body if it is guaranteed to be called earlier. */
private final class RedundantClinitPruner implements Callback, ChangeScopeRootCallback {
@Override
public void enterChangeScopeRoot(AbstractCompiler compiler, Node root) {
// Reset the clinit call tracking when starting over on a new scope.
clinitsCalledAtBranch = new HierarchicalSet<>(null);
stateStack.clear();
}
private final Deque<HierarchicalSet<String>> stateStack = new ArrayDeque<>();
private HierarchicalSet<String> clinitsCalledAtBranch = new HierarchicalSet<>(null);
@Override
public boolean shouldTraverse(NodeTraversal t, Node node, Node parent) {
if (NodeUtil.isFunctionDeclaration(node)) {
// Unlike function expressions, we don't know when the function in a function declaration
// will be executed so lets avoid inheriting anything from the current branch.
stateStack.addLast(clinitsCalledAtBranch);
clinitsCalledAtBranch = new HierarchicalSet<>(null);
}
if (isNewControlBranch(parent)) {
clinitsCalledAtBranch = new HierarchicalSet<>(clinitsCalledAtBranch);
if (isClinitMethod(parent)) {
// Adds itself as any of your children can assume clinit is already called.
clinitsCalledAtBranch.add(NodeUtil.getName(parent));
}
}
return true;
}
@Override
public void visit(NodeTraversal t, Node node, Node parent) {
tryRemovingClinit(node, parent);
if (isNewControlBranch(parent)) {
clinitsCalledAtBranch = clinitsCalledAtBranch.parent;
}
if (parent != null && NodeUtil.isFunctionDeclaration(node)) {
// Unlike function expressions, we don't know when the function in a function declaration
// will be executed so lets avoid inheriting anything from the current branch.
clinitsCalledAtBranch = stateStack.removeLast();
}
}
private void tryRemovingClinit(Node node, Node parent) {
String clinitName = node.isCall() ? getClinitMethodName(node.getFirstChild()) : null;
if (clinitName == null) {
return;
}
if (clinitsCalledAtBranch.add(clinitName)) {
// This is the first time we are seeing this clinit so cannot remove it.
return;
}
// Replacing with undefined is a simple way of removing without introducing invalid AST.
parent.replaceChild(node, NodeUtil.newUndefinedNode(node));
compiler.reportChangeToEnclosingScope(parent);
madeChange = true;
}
private boolean isNewControlBranch(Node n) {
return n != null
&& (NodeUtil.isControlStructure(n)
|| n.isHook()
|| n.isAnd()
|| n.isOr()
|| n.isFunction());
}
}
// TODO(michaelthomas): Prune clinit calls in functions, if previous functions or the immediate
// next function guarantees clinit call. With that we won't need this pass.
/**
* Prunes clinit calls which immediately precede calls to a static function which calls the same
* clinit. e.g. "Foo.clinit(); return new Foo()" -> "return new Foo()"
*/
private final class LookAheadRedundantClinitPruner extends AbstractPostOrderCallback {
@Override
public void visit(NodeTraversal t, Node node, Node parent) {
if (!node.isExprResult()) {
return;
}
// Find clinit calls.
String clinitName =
node.getFirstChild().isCall() ? getClinitMethodName(node.getFirstFirstChild()) : null;
if (clinitName == null) {
return;
}
// Check for calls to a static method immediately following the clinit call.
Node callOrNewNode = getCallOrNewNode(node.getNext());
if (callOrNewNode == null || !callOrNewNode.getFirstChild().isName()) {
return;
}
// Check that the call isn't a recursive call to the same function.
Node enclosingFunction = NodeUtil.getEnclosingFunction(node);
if (enclosingFunction == null || callOrNewNode.getFirstChild().getString()
.equals(NodeUtil.getNearestFunctionName(enclosingFunction))) {
return;
}
// Find the definition of the function being called.
Var var = t.getScope().getVar(callOrNewNode.getFirstChild().getString());
if (var == null || var.getInitialValue() == null || !var.getInitialValue().isFunction()) {
return;
}
// Check that the clinit is safe to prune.
Node staticFnNode = var.getInitialValue();
if (callsClinit(staticFnNode, clinitName) && hasSafeArguments(t, callOrNewNode)) {
parent.removeChild(node);
compiler.reportChangeToEnclosingScope(parent);
madeChange = true;
}
}
/**
* Returns whether the arguments to the specified call/new node are "safe". i.e. they are only
* parameters to the enclosing function or literal values (and not e.g. a static field reference
* which might need the clinit we are trying to remove).
*/
private boolean hasSafeArguments(NodeTraversal t, Node callOrNewNode) {
Node child = callOrNewNode.getSecondChild();
while (child != null) {
if (!NodeUtil.isLiteralValue(child, false /* includeFunctions */)
&& !isParameter(t, child)) {
return false;
}
child = child.getNext();
}
return true;
}
/** Returns whether the specified node is defined as a parameter to its enclosing function. */
private boolean isParameter(NodeTraversal t, Node n) {
if (!n.isName()) {
return false;
}
Var var = t.getScope().getVar(n.getString());
return var.getParentNode().isParamList();
}
/**
* Returns the call node associated with the specified node if one exists, otherwise returns
* null.
*/
private Node getCallOrNewNode(Node n) {
if (n == null) {
return null;
}
switch (n.getToken()) {
case EXPR_RESULT:
case RETURN:
return getCallOrNewNode(n.getFirstChild());
case CALL:
case NEW:
return n;
case CONST:
case LET:
case VAR:
return n.hasOneChild() ? getCallOrNewNode(n.getFirstFirstChild()) : null;
default:
return null;
}
}
/** Returns whether the specified function contains a call to the specified clinit. */
private boolean callsClinit(Node fnNode, String clinitName) {
checkNotNull(clinitName);
// TODO(michaelthomas): Consider checking all children, but watch out for return statements
// that could short-circuit the clinit.
Node child = fnNode.getLastChild().getFirstChild();
return child != null
&& child.isExprResult()
&& child.getFirstChild().isCall()
&& clinitName.equals(getClinitMethodName(child.getFirstFirstChild()));
}
}
/**
* A traversal callback that removes the body of empty clinits.
*/
private final class EmptyClinitPruner extends AbstractPostOrderCallback {
@Override
public void visit(NodeTraversal t, Node node, Node parent) {
if (!isClinitMethod(node)) {
return;
}
trySubstituteEmptyFunction(node);
}
/**
* Clears the body of any functions that are equivalent to empty functions.
*/
private void trySubstituteEmptyFunction(Node fnNode) {
String fnQualifiedName = NodeUtil.getName(fnNode);
// Ignore anonymous/constructor functions.
if (Strings.isNullOrEmpty(fnQualifiedName)) {
return;
}
Node body = fnNode.getLastChild();
if (!body.hasChildren()) {
return;
}
// Ensure that the first expression in the body is setting itself to the empty function and
// there are no other expressions.
Node firstExpr = body.getFirstChild();
if (!isAssignToEmptyFn(firstExpr, fnQualifiedName) || firstExpr.getNext() != null) {
return;
}
body.removeChild(firstExpr);
NodeUtil.markFunctionsDeleted(firstExpr, compiler);
compiler.reportChangeToEnclosingScope(body);
madeChange = true;
}
private boolean isAssignToEmptyFn(Node node, String enclosingFnName) {
if (!NodeUtil.isExprAssign(node)) {
return false;
}
Node lhs = node.getFirstFirstChild();
Node rhs = node.getFirstChild().getLastChild();
return NodeUtil.isEmptyFunctionExpression(rhs) && lhs.matchesQualifiedName(enclosingFnName);
}
}
private static boolean isClinitMethod(Node node) {
return node.isFunction() && isClinitMethodName(NodeUtil.getName(node));
}
private static String getClinitMethodName(Node fnNode) {
String fnName = fnNode.getQualifiedName();
return isClinitMethodName(fnName) ? fnName : null;
}
private static boolean isClinitMethodName(String fnName) {
// The '.$clinit' case) only happens when collapseProperties is off.
return fnName != null && (fnName.endsWith("$$0clinit") || fnName.endsWith(".$clinit"));
}
/**
* A minimalist implelementation of a hierarchical Set where an item might exist in current set or
* any of its parents.
*/
private static class HierarchicalSet<T> {
private Set<T> currentSet = new HashSet<>();
@Nullable private final HierarchicalSet<T> parent;
public HierarchicalSet(@Nullable HierarchicalSet<T> parent) {
this.parent = parent;
}
public boolean add(T o) {
return !parentsContains(o) && currentSet.add(o);
}
/** Returns true either my parent or any of its parents contains the item. */
private boolean parentsContains(T o) {
return parent != null && (parent.currentSet.contains(o) || parent.parentsContains(o));
}
}
}