-
Notifications
You must be signed in to change notification settings - Fork 1.1k
/
J2clConstantHoisterPass.java
164 lines (138 loc) · 6.17 KB
/
J2clConstantHoisterPass.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
/*
* 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.checkState;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Multimap;
import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
import com.google.javascript.rhino.Node;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
/**
* An optimization pass for J2CL-generated code to hoist some constant assignments out clinit method
* to declaration phase so they could be used by other optimization passes for static evaliation.
*/
public class J2clConstantHoisterPass implements CompilerPass {
private final AbstractCompiler compiler;
J2clConstantHoisterPass(AbstractCompiler compiler) {
this.compiler = compiler;
}
@Override
public void process(Node externs, Node root) {
if (!J2clSourceFileChecker.shouldRunJ2clPasses(compiler)) {
return;
}
final Multimap<String, Node> fieldAssignments = ArrayListMultimap.create();
final Set<Node> hoistableFunctions = new HashSet<>();
NodeTraversal.traverseEs6(compiler, root, new AbstractPostOrderCallback() {
@Override
public void visit(NodeTraversal t, Node node, Node parent) {
if (parent != null && NodeUtil.isLValue(node)) {
fieldAssignments.put(node.getQualifiedName(), parent);
}
if (isHoistableFunction(t, node)) {
hoistableFunctions.add(node);
}
}
});
for (Collection<Node> assignments : fieldAssignments.asMap().values()) {
maybeHoistClassField(assignments, hoistableFunctions);
}
}
/**
* Returns whether the specified rValue is a function which does not receive any variables from
* its containing scope, and is thus 'hoistable'.
*/
private static boolean isHoistableFunction(NodeTraversal t, Node node) {
// TODO(michaelthomas): This could be improved slightly by not assuming that any variable in the
// outer scope is used in the function.
return node.isFunction() && t.getScope().getVarCount() == 0;
}
private void maybeHoistClassField(
Collection<Node> assignments, Collection<Node> hoistableFunctions) {
// The field is only assigned twice:
if (assignments.size() != 2) {
return;
}
Node firstAssignment = Iterables.get(assignments, 0);
Node secondAssignment = Iterables.get(assignments, 1);
// One of them is for field initialization in declaration phase:
if (!isClassFieldInitialization(secondAssignment)) {
return;
}
// The other one is the clinit initialization:
if (!isClinitFieldAssignment(firstAssignment)) {
return;
}
// And it is assigned to a literal value; hence could be used in static eval and safe to move:
Node firstAssignmentRhs = firstAssignment.getSecondChild();
if (!NodeUtil.isLiteralValue(firstAssignmentRhs, true /* includeFunctions */)
|| (firstAssignmentRhs.isFunction() && !hoistableFunctions.contains(firstAssignmentRhs))) {
return;
}
// And the assignment are in the same script:
if (NodeUtil.getEnclosingScript(firstAssignment)
!= NodeUtil.getEnclosingScript(secondAssignment)) {
return;
}
// At this point the only case some could observe the declaration value is the when you have
// cycle between clinits and the field is accessed before initialization; which is almost always
// a bug and GWT never assumed this state is observable in its optimization, yet nobody
// complained. So it is safe to upgrade it to a constant.
hoistConstantLikeField(firstAssignment, secondAssignment);
}
private void hoistConstantLikeField(Node clinitAssignment, Node declarationAssignment) {
Node clinitAssignedValue = clinitAssignment.getSecondChild();
Node declarationInClass = declarationAssignment.getFirstChild();
Node declarationAssignedValue = declarationInClass.getFirstChild();
Node clinitChangeScope = NodeUtil.getEnclosingChangeScopeRoot(clinitAssignment);
// Remove the clinit initialization
NodeUtil.removeChild(clinitAssignment.getParent(), clinitAssignment);
// Replace the assignment in declaration with the value from clinit
clinitAssignedValue.detach();
declarationInClass.replaceChild(declarationAssignedValue, clinitAssignedValue);
declarationInClass.putBooleanProp(Node.IS_CONSTANT_VAR, true);
// Sanity check
checkState(NodeUtil.isLiteralValue(declarationAssignedValue, false /* includeFunctions */));
compiler.reportChangeToChangeScope(clinitChangeScope);
compiler.reportChangeToEnclosingScope(declarationAssignment);
}
private static boolean isClassFieldInitialization(Node node) {
return node.getParent().isScript()
&& node.isVar()
&& node.getFirstFirstChild() != null
&& NodeUtil.isLiteralValue(node.getFirstFirstChild(), false /* includeFunctions */);
}
private static boolean isClinitFieldAssignment(Node node) {
return node.getParent().isExprResult()
&& node.getGrandparent().isNormalBlock()
&& isClinitMethod(node.getGrandparent().getParent());
}
// TODO(goktug): Create a utility to share this logic and start using getQualifiedOriginalName.
private static boolean isClinitMethod(Node fnNode) {
if (!fnNode.isFunction()) {
return false;
}
String fnName = NodeUtil.getName(fnNode);
return fnName != null && isClinitMethodName(fnName);
}
private static boolean isClinitMethodName(String methodName) {
return methodName != null
&& (methodName.endsWith("$$0clinit") || methodName.endsWith(".$clinit"));
}
}