16
16
17
17
package com .google .javascript .jscomp ;
18
18
19
+ import static com .google .common .base .Preconditions .checkState ;
20
+
21
+ import com .google .common .collect .ArrayListMultimap ;
22
+ import com .google .common .collect .ListMultimap ;
19
23
import com .google .javascript .jscomp .diagnostic .LogFile ;
20
24
import com .google .javascript .rhino .IR ;
21
25
import com .google .javascript .rhino .Node ;
26
+ import java .util .ArrayDeque ;
27
+ import java .util .ArrayList ;
28
+ import java .util .Collections ;
29
+ import java .util .Deque ;
22
30
import java .util .HashMap ;
31
+ import java .util .List ;
23
32
import java .util .Map ;
24
33
import java .util .Set ;
34
+ import org .jspecify .nullness .Nullable ;
25
35
26
36
/**
27
37
* Marks all functions of specified chunks for eager parsing by adding the node property
38
48
* for eager compile for each specified chunk.
39
49
*/
40
50
public final class ParenthesizeFunctionsInChunks implements CompilerPass {
41
- final AbstractCompiler compiler ;
42
- final Set <String > parenthesizeFunctionsInChunks ;
43
-
44
- // Map for recording diagnostic information about what was marked for eager compilation.
45
- final Map <String , Long > chunkToEagerCompileFnCount = new HashMap <>();
51
+ private final AbstractCompiler compiler ;
52
+ private final Set <String > parenthesizeFunctionsInChunks ;
46
53
54
+ /**
55
+ * @param compiler An abstract compiler.
56
+ * @param parenthesizeFunctionsInChunks The set of chunk names in which to parenthesize top level
57
+ * functions.
58
+ */
47
59
public ParenthesizeFunctionsInChunks (
48
60
AbstractCompiler compiler , Set <String > parenthesizeFunctionsInChunks ) {
49
61
this .compiler = compiler ;
@@ -52,52 +64,155 @@ public ParenthesizeFunctionsInChunks(
52
64
53
65
@ Override
54
66
public void process (Node externs , Node root ) {
55
- NodeTraversal .traverse (compiler , root , new Traversal ());
56
- for (String key : chunkToEagerCompileFnCount .keySet ()) {
57
- try (LogFile log = compiler .createOrReopenLog (getClass (), "eager_compile_chunks.log" )) {
58
- log .log ("%s: %d fn's marked for eager compile" , key , chunkToEagerCompileFnCount .get (key ));
67
+ Traversal traversal = new Traversal (parenthesizeFunctionsInChunks );
68
+ NodeTraversal .traverse (compiler , root , traversal );
69
+
70
+ Map <String , Long > chunkToEagerCompileFnCounts = traversal .getChunkToEagerCompileFnCounts ();
71
+
72
+ try (LogFile log = compiler .createOrReopenLog (getClass (), "eager_compile_chunks.log" )) {
73
+ for (Map .Entry <String , Long > entry : chunkToEagerCompileFnCounts .entrySet ()) {
74
+ log .log ("%s: %d fn's marked for eager compile" , entry .getKey (), entry .getValue ());
59
75
}
60
76
}
61
77
}
62
78
63
- private class Traversal implements NodeTraversal .Callback {
79
+ private static class Traversal implements NodeTraversal .Callback {
80
+ private final Set <String > parenthesizeFunctionsInChunks ;
81
+
82
+ // The stack of nested block scopes for the node we're currently visiting.
83
+ private final Deque <Node > nestedBlockScopes = new ArrayDeque <>();
84
+
85
+ // A multimap relating a scope node to any children nodes which should be hosted into its scope.
86
+ private final ListMultimap <Node , Node > hoistNodesToScope = ArrayListMultimap .create ();
87
+
88
+ // Map for recording diagnostic information about what was marked for eager compilation.
89
+ private final Map <String , Long > chunkToEagerCompileFnCounts = new HashMap <>();
90
+
91
+ public Traversal (Set <String > parenthesizeFunctionsInChunks ) {
92
+ this .parenthesizeFunctionsInChunks = parenthesizeFunctionsInChunks ;
93
+ }
94
+
95
+ @ Override
96
+ public boolean shouldTraverse (NodeTraversal t , Node n , Node parent ) {
97
+ if (!shouldParenthesizeTree (t , n )) {
98
+ return false ;
99
+ }
100
+
101
+ if (parent != null && parent .isFunction ()) {
102
+ return false ; // Don't visit the contents of any functions.
103
+ }
104
+
105
+ if (NodeUtil .isStatementBlock (n )) {
106
+ nestedBlockScopes .push (n ); // Enter block scope.
107
+ }
108
+
109
+ return true ;
110
+ }
111
+
112
+ @ Override
113
+ public void visit (NodeTraversal t , Node n , Node parent ) {
114
+ if (isInnerMostBlockScope (n )) {
115
+ hoistChildrenToTopOfScope (n );
116
+ nestedBlockScopes .pop (); // Exit block scope.
117
+ } else if (NodeUtil .isFunctionExpression (n )) {
118
+ n .setMarkForParenthesize (true );
119
+ incrementEagerlyCompiledFunctionCount (t );
120
+ } else if (NodeUtil .isFunctionDeclaration (n )) {
121
+ n .setMarkForParenthesize (true );
122
+ addChildForHoistToScope (functionDeclarationToFunctionExpression (t , n ));
123
+ incrementEagerlyCompiledFunctionCount (t );
124
+ } else {
125
+ // Ex: Method function definitions.
126
+ // Do nothing.
127
+ }
128
+ }
129
+
130
+ public Map <String , Long > getChunkToEagerCompileFnCounts () {
131
+ checkState (
132
+ nestedBlockScopes .isEmpty (), "Expected empty scope stack. Got: %s" , nestedBlockScopes );
133
+ checkState (
134
+ hoistNodesToScope .isEmpty (), "Expected empty hoist map. Got: %s" , hoistNodesToScope );
135
+ return chunkToEagerCompileFnCounts ;
136
+ }
137
+
138
+ /**
139
+ * Whether we should parenthesize functions in the given tree of the traversal. If there is no
140
+ * chunk, then we are compiling a single-chunk output, so we will just parenthesize all
141
+ * top-level functions.
142
+ */
143
+ private boolean shouldParenthesizeTree (NodeTraversal t , Node n ) {
144
+ if (!n .isScript ()) {
145
+ return true ;
146
+ }
64
147
65
- Traversal () {}
148
+ String chunkName = getChunkName (t );
149
+ return chunkName == null || parenthesizeFunctionsInChunks .contains (chunkName );
150
+ }
151
+
152
+ private void incrementEagerlyCompiledFunctionCount (NodeTraversal t ) {
153
+ chunkToEagerCompileFnCounts .merge (getChunkName (t ), 1L , (oldValue , value ) -> oldValue + 1 );
154
+ }
66
155
67
- private void rewriteAsFunctionExpression (Node n ) {
156
+ /** Gets the chunk name of the current traveral or null if it doesn't belong to a chunk. */
157
+ private @ Nullable String getChunkName (NodeTraversal t ) {
158
+ JSChunk chunk = t .getChunk ();
159
+ return (chunk != null ? chunk .getName () : null );
160
+ }
161
+
162
+ /** Whether this node is the inner-most block scope. */
163
+ private boolean isInnerMostBlockScope (Node n ) {
164
+ return !nestedBlockScopes .isEmpty () && nestedBlockScopes .peek () == n ;
165
+ }
166
+
167
+ /**
168
+ * Converts the given function declaration into a function expression suitable for wrapping in
169
+ * parenthesis. The function expression is assigned to a `var` declaration so that it is
170
+ * semantically hoisted to the inner-most function scope like with function declarations.
171
+ */
172
+ private Node functionDeclarationToFunctionExpression (NodeTraversal t , Node n ) {
173
+ AbstractCompiler compiler = t .getCompiler ();
68
174
Node nameNode = n .getFirstChild ();
69
175
Node name = IR .name (nameNode .getString ()).srcref (nameNode );
70
176
Node var = IR .var (name ).srcref (n );
71
- // Add the VAR, remove the FUNCTION
72
- n .replaceWith (var );
73
- // readd the function, but remove the function name, to guard against
177
+ // read the function, but remove the function name, to guard against
74
178
// functions that re-assign to themselves, which will end up causing a
75
179
// recursive loop.
76
- n .getFirstChild ().setString ("" );
180
+ nameNode .setString ("" );
181
+ compiler .reportChangeToEnclosingScope (n .getLastChild ());
182
+ // Add the VAR, remove the FUNCTION
183
+ n .replaceWith (var );
184
+ compiler .reportChangeToEnclosingScope (var );
77
185
name .addChildToFront (n );
186
+ return var ;
78
187
}
79
188
80
- @ Override
81
- public boolean shouldTraverse (NodeTraversal t , Node n , Node parent ) {
82
- if (n .isScript () || n .isRoot () || n .isModuleBody ()) {
83
- return true ;
84
- }
85
- if (n .isFunction ()) {
86
- String chunkName = t .getChunk ().getName ();
87
- chunkToEagerCompileFnCount .putIfAbsent (chunkName , 0L );
88
- if (parenthesizeFunctionsInChunks .contains (chunkName )) {
89
- n .putBooleanProp (Node .MARK_FOR_PARENTHESIZE , true );
90
- if (!NodeUtil .isFunctionExpression (n )) {
91
- rewriteAsFunctionExpression (n );
92
- }
93
- chunkToEagerCompileFnCount .put (chunkName , chunkToEagerCompileFnCount .get (chunkName ) + 1 );
94
- }
95
- return false ;
189
+ /**
190
+ * Marks a node to be moved to the top of its parent *block* scope. This is useful for hoisting
191
+ * function expression assignments to emulate how function declarations are assigned.
192
+ *
193
+ * <pre>
194
+ * Function declarations have the following semantics:
195
+ * 1. Hoists the function variable declaration to the top of the inner-most *function* scope.
196
+ * 2. Hoists the function assignment to the top of the inner-most *block* scope.
197
+ * </pre>
198
+ */
199
+ private void addChildForHoistToScope (Node node ) {
200
+ if (nestedBlockScopes .isEmpty ()) { // This node is already at the root scope.
201
+ return ;
96
202
}
97
- return true ;
203
+
204
+ Node innerMostBlockScope = nestedBlockScopes .peek ();
205
+ hoistNodesToScope .put (innerMostBlockScope , node .detach ());
98
206
}
99
207
100
- @ Override
101
- public void visit (NodeTraversal t , Node n , Node parent ) {}
208
+ /** Hoists any marked nodes to the beginning of this scope. */
209
+ private void hoistChildrenToTopOfScope (Node scope ) {
210
+ List <Node > nodes = new ArrayList <>(hoistNodesToScope .removeAll (scope ));
211
+ Collections .reverse (nodes ); // Maintain node order when pushing as first sibling.
212
+
213
+ for (Node node : nodes ) {
214
+ scope .addChildToFront (node );
215
+ }
216
+ }
102
217
}
103
218
}
0 commit comments