Skip to content

Commit a36e3ca

Browse files
lauraharkercopybara-github
authored andcommitted
Add beginnings of pass to transpile/optimize @closureUnaware
This CL just enables constant folding on @closureUnaware code, as a simple way to demo how the pass works. But we will be more thorough about setting CompilerOptions for @closureUnaware in a followup. PiperOrigin-RevId: 845290208
1 parent 384ddc5 commit a36e3ca

File tree

2 files changed

+311
-0
lines changed

2 files changed

+311
-0
lines changed
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/*
2+
* Copyright 2025 The Closure Compiler Authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.javascript.jscomp;
18+
19+
import static com.google.common.base.Preconditions.checkState;
20+
21+
import com.google.common.collect.LinkedHashMultimap;
22+
import com.google.javascript.jscomp.parsing.parser.FeatureSet;
23+
import com.google.javascript.rhino.Node;
24+
25+
/**
26+
* Runs a new Compiler instance over all @closureUnaware code (i.e. "shadow roots") in the AST.
27+
*
28+
* <p>This is a separate Compiler instance to avoid leaking compiler state between the shadow AST
29+
* and main AST. Otherwise, we risk doing unsafe optimizations on @closureUnaware code.
30+
*/
31+
class TranspileAndOptimizeClosureUnaware implements CompilerPass {
32+
private final AbstractCompiler original;
33+
34+
TranspileAndOptimizeClosureUnaware(AbstractCompiler original) {
35+
this.original = original;
36+
}
37+
38+
@Override
39+
public void process(Node externs, Node root) {
40+
var collector = new CollectShadowAsts();
41+
NodeTraversal.traverse(original, root, collector);
42+
if (collector.shadowAsts.isEmpty()) {
43+
return;
44+
}
45+
46+
var shadowOptions = new CompilerOptions();
47+
// TODO: b/421971366 - enable simple optimizations + debugging options + transpilation
48+
shadowOptions.setFoldConstants(true);
49+
// TODO: b/421971366 - enable configuring Mode.TRANSPILE_ONLY.
50+
NestedCompilerRunner shadowCompiler =
51+
NestedCompilerRunner.create(
52+
original, shadowOptions, NestedCompilerRunner.Mode.TRANSPILE_AND_OPTIMIZE);
53+
54+
initShadowInputs(shadowCompiler, collector.shadowAsts);
55+
shadowCompiler.compile();
56+
reattachShadowNodes(collector.shadowAsts);
57+
}
58+
59+
private record ShadowAst(Node originalRoot, Node callNode, Node script) {}
60+
61+
/** Traverses all closure unaware SCRIPTs to collect shadow ASTs. */
62+
private static final class CollectShadowAsts implements NodeTraversal.Callback {
63+
private final LinkedHashMultimap<SourceFile, ShadowAst> shadowAsts =
64+
LinkedHashMultimap.create();
65+
66+
@Override
67+
public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) {
68+
if (!n.isScript()) {
69+
return true;
70+
}
71+
return n.isClosureUnawareCode();
72+
}
73+
74+
@Override
75+
public void visit(NodeTraversal t, Node n, Node parent) {
76+
Node shadow = n.getClosureUnawareShadow();
77+
if (shadow == null) {
78+
return;
79+
}
80+
// CALL
81+
// GETPROP .call
82+
// NAME $jscomp_wrap_closure_unaware_code
83+
// NAME globalThis
84+
// or
85+
// CALL
86+
// GETPROP .call
87+
// NAME $jscomp_wrap_closure_unaware_code
88+
// NAME undefined
89+
// or
90+
// CALL
91+
// NAME $jscomp_wrap_closure_unaware_code
92+
Node callNode = n.getParent().isCall() ? n.getParent() : n.getGrandparent();
93+
checkState(callNode.isCall(), callNode);
94+
shadowAsts.put(
95+
t.getInput().getSourceFile(), new ShadowAst(shadow, callNode, shadow.getOnlyChild()));
96+
}
97+
}
98+
99+
private void initShadowInputs(
100+
NestedCompilerRunner shadowCompiler,
101+
LinkedHashMultimap<SourceFile, ShadowAst> perFileInputs) {
102+
for (SourceFile sourceFile : perFileInputs.keySet()) {
103+
int indexInFile = 0;
104+
for (ShadowAst shadowAst : perFileInputs.get(sourceFile)) {
105+
String uniqueName = sourceFile.getName() + ".shadow" + indexInFile++;
106+
shadowCompiler.addScript(shadowAst.script().detach(), uniqueName);
107+
108+
// TODO: b/421971366 - attach the correct FeatureSet, either here or possibly during
109+
// parsing.
110+
shadowAst.script().putProp(Node.FEATURE_SET, FeatureSet.BARE_MINIMUM);
111+
}
112+
}
113+
}
114+
115+
/**
116+
* Re-attaches the shadow AST scripts, that were previously detached for compilation, to their
117+
* associated ROOT nodes from the main AST.
118+
*/
119+
private void reattachShadowNodes(LinkedHashMultimap<SourceFile, ShadowAst> inputs) {
120+
for (ShadowAst shadowAst : inputs.values()) {
121+
Node script = shadowAst.script();
122+
checkState(script.hasOneChild(), script.toStringTree());
123+
shadowAst.originalRoot().addChildToFront(script.detach());
124+
// Remove traces of the shadow compilation.
125+
script.setInputId(null);
126+
script.setStaticSourceFile(null);
127+
}
128+
}
129+
}
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
/*
2+
* Copyright 2025 The Closure Compiler Authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.javascript.jscomp;
18+
19+
import static com.google.javascript.jscomp.CompilerTestCase.srcs;
20+
21+
import com.google.javascript.rhino.Node;
22+
import org.junit.Before;
23+
import org.junit.Test;
24+
import org.junit.runner.RunWith;
25+
import org.junit.runners.JUnit4;
26+
27+
@RunWith(JUnit4.class)
28+
public class TranspileAndOptimizeClosureUnawareTest extends CompilerTestCase {
29+
30+
@Override
31+
protected CompilerPass getProcessor(Compiler compiler) {
32+
return this::process;
33+
}
34+
35+
private void process(Node externs, Node root) {
36+
Compiler compiler = getLastCompiler();
37+
ManageClosureUnawareCode.wrap(compiler).process(externs, root);
38+
new TranspileAndOptimizeClosureUnaware(compiler).process(externs, root);
39+
ManageClosureUnawareCode.unwrap(compiler).process(externs, root);
40+
}
41+
42+
private int inputCount;
43+
private int outputCount;
44+
45+
@Before
46+
public void setup() {
47+
disableValidateAstChangeMarking();
48+
allowExternsChanges();
49+
this.inputCount = 0;
50+
this.outputCount = 0;
51+
}
52+
53+
String createPrettyPrinter(Node n) {
54+
return new CodePrinter.Builder(n)
55+
.setCompilerOptions(getLastCompiler().getOptions())
56+
.setPrettyPrint(true)
57+
.build();
58+
}
59+
60+
@Test
61+
public void testEmptyClosureUnawareFn_unchanged() {
62+
test(srcs(closureUnaware("")), expected(expectedClosureUnaware("")));
63+
}
64+
65+
@Test
66+
public void testFoldConstants() {
67+
test(
68+
srcs(closureUnaware("console.log(1 + 2);")),
69+
expected(expectedClosureUnaware("console.log(3);")));
70+
}
71+
72+
@Test
73+
public void testFoldConstants_multipleFiles() {
74+
test(
75+
srcs(
76+
closureUnaware("console.log(1 + 2);"),
77+
closureUnaware("console.log(11 + 22);"),
78+
closureUnaware("console.log(111 + 222);")),
79+
expected(
80+
expectedClosureUnaware("console.log(3);"),
81+
expectedClosureUnaware("console.log(33);"),
82+
expectedClosureUnaware("console.log(333);")));
83+
}
84+
85+
@Test
86+
public void testFoldConstants_multipleFiles_ignoresNonClosureUnaware() {
87+
test(
88+
srcs(
89+
closureUnaware("console.log(1 + 2);"),
90+
"""
91+
goog.module('some.other.file');
92+
console.log(11 + 22);
93+
"""),
94+
expected(
95+
expectedClosureUnaware("console.log(3);"),
96+
"""
97+
goog.module('some.other.file');
98+
console.log(11 + 22);
99+
"""));
100+
}
101+
102+
@Test
103+
public void testFoldConstants_multipleClosureUnawareBlocksInFile() {
104+
test(
105+
srcs(
106+
closureUnaware(
107+
"console.log(1 + 2);", "console.log(11 + 22);", "console.log(111 + 222);")),
108+
expected(
109+
expectedClosureUnaware("console.log(3);", "console.log(33);", "console.log(333);")));
110+
}
111+
112+
private String closureUnaware(String... closureUnaware) {
113+
String prefix =
114+
String.format(
115+
"""
116+
/** @fileoverview @closureUnaware */
117+
goog.module('test%d');
118+
""",
119+
inputCount++);
120+
StringBuilder input = new StringBuilder().append(prefix);
121+
for (String block : closureUnaware) {
122+
input.append(
123+
String.format(
124+
"""
125+
/** @closureUnaware */
126+
(function() {
127+
%s
128+
}).call(undefined);
129+
""",
130+
block));
131+
}
132+
return input.toString();
133+
}
134+
135+
@Test
136+
public void testFoldConstants_directCallInsteadOfDotCall() {
137+
test(
138+
srcs(
139+
// Don't use the "closureUnaware()" wrapper because it always uses .call - we want to
140+
// specifically test a regular direct call.
141+
"""
142+
/** @fileoverview @closureUnaware */
143+
goog.module('test');
144+
/** @closureUnaware */
145+
(function() {
146+
console.log(1 + 2);
147+
})();
148+
"""),
149+
expected(
150+
"""
151+
/** @fileoverview @closureUnaware */
152+
goog.module('test');
153+
(function() {
154+
console.log(3);
155+
})();
156+
"""));
157+
}
158+
159+
private String expectedClosureUnaware(String... closureUnaware) {
160+
String prefix =
161+
String.format(
162+
"""
163+
/** @fileoverview @closureUnaware */
164+
goog.module('test%d');
165+
""",
166+
outputCount++);
167+
StringBuilder output = new StringBuilder().append(prefix);
168+
for (String block : closureUnaware) {
169+
output.append(
170+
String.format(
171+
"""
172+
(function() {
173+
%s
174+
}).call(undefined);
175+
""",
176+
block));
177+
}
178+
return output.toString();
179+
}
180+
181+
// TODO: b/421971366 - add a greater variety of test cases.
182+
}

0 commit comments

Comments
 (0)