Skip to content

Commit cd2d647

Browse files
varomodtcopybara-github
authored andcommitted
make @requireInlining a bit more robust
PiperOrigin-RevId: 803165608
1 parent d5c2284 commit cd2d647

File tree

2 files changed

+88
-29
lines changed

2 files changed

+88
-29
lines changed

src/com/google/javascript/jscomp/InlineFunctions.java

Lines changed: 53 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,14 @@
5555
*/
5656
class InlineFunctions implements CompilerPass {
5757

58-
static final DiagnosticType FAILED_REQUIRED_INLINING =
59-
DiagnosticType.error(
60-
"JSC_FAILED_REQUIRED_INLINING",
61-
"function {1} annotated @requireInlining could not be inlined here");
58+
// NOTE: this diagnostic is only emitted in debug mode and may not cover all inlinings which
59+
// occur in practice. This allows us to test the pass independently and to obtain information
60+
// about what inlinings occurred and why.
61+
static final DiagnosticType MISSED_REQUIRED_INLINING =
62+
DiagnosticType.warning(
63+
"JSC_MISSED_REQUIRED_INLINING",
64+
"function annotated @requireInlining could not be inlined on this pass: {0}. Context:"
65+
+ " {1}");
6266

6367
// Note: This pass assumes that all functions are uniquely named variables as enforced by
6468
// the normalization pass.
@@ -195,8 +199,8 @@ public void findNamedFunctions(NodeTraversal t, Node n, Node parent) {
195199
}
196200

197201
switch (n.getToken()) {
198-
// Functions expressions in the form of:
199-
// var fooFn = function(x) { return ... }
202+
// Functions expressions in the form of:
203+
// var fooFn = function(x) { return ... }
200204
case VAR:
201205
case LET:
202206
case CONST:
@@ -209,8 +213,8 @@ public void findNamedFunctions(NodeTraversal t, Node n, Node parent) {
209213
}
210214
break;
211215

212-
// Named functions
213-
// function Foo(x) { return ... }
216+
// Named functions
217+
// function Foo(x) { return ... }
214218
case FUNCTION:
215219
Preconditions.checkState(NodeUtil.isStatementBlock(parent) || parent.isLabel());
216220
if (NodeUtil.isFunctionDeclaration(n)) {
@@ -229,8 +233,8 @@ public void findNamedFunctions(NodeTraversal t, Node n, Node parent) {
229233
*/
230234
public void findFunctionExpressions(NodeTraversal t, Node n) {
231235
switch (n.getToken()) {
232-
// Functions expressions in the form of:
233-
// (function(){})();
236+
// Functions expressions in the form of:
237+
// (function(){})();
234238
case OPTCHAIN_CALL:
235239
case CALL:
236240
Node fnNode = null;
@@ -264,9 +268,7 @@ void maybeAddFunction(Function fn, JSChunk chunk) {
264268
String name = fn.getName();
265269
FunctionState functionState = getOrCreateFunctionState(name);
266270
updateFunctionStateForInlining(fn, chunk, name, functionState);
267-
if (hasRequireInliningAnnotation(fn.getFunctionNode()) && !functionState.canInline()) {
268-
compiler.report(JSError.make(fn.getFunctionNode(), FAILED_REQUIRED_INLINING));
269-
}
271+
checkState(!(hasRequireInliningAnnotation(fn.getFunctionNode()) && !functionState.canInline()));
270272
}
271273

272274
/**
@@ -370,7 +372,7 @@ private boolean hasNoInlineAnnotation(Node fnNode) {
370372
return jsDocInfo != null && jsDocInfo.isNoInline();
371373
}
372374

373-
private boolean hasRequireInliningAnnotation(Node fnNode) {
375+
private static boolean hasRequireInliningAnnotation(Node fnNode) {
374376
JSDocInfo jsDocInfo = NodeUtil.getBestJSDocInfo(fnNode);
375377
return jsDocInfo != null && jsDocInfo.isRequireInlining();
376378
}
@@ -435,7 +437,7 @@ private static class CallVisitor extends AbstractPostOrderCallback {
435437
@Override
436438
public void visit(NodeTraversal t, Node n, Node parent) {
437439
switch (n.getToken()) {
438-
// Function calls
440+
// Function calls
439441
case OPTCHAIN_CALL:
440442
case CALL:
441443
Node child = n.getFirstChild();
@@ -608,6 +610,18 @@ private void checkNameUsage(Node n, Node parent) {
608610
// so mark the function as uninlinable.
609611
// TODO(johnlenz): Should we just remove it from fns here?
610612
functionState.disallowInlining();
613+
} else if ((parent.isAssign()
614+
&& parent.getJSDocInfo() != null
615+
&& parent.getJSDocInfo().isConstant()
616+
&& parent.getSecondChild() == n)
617+
|| (parent.getParent() != null
618+
&& parent.getParent().isConst()
619+
&& parent.getFirstChild() == n)) {
620+
// e.g. const bar = foo; exports.bar = foo;
621+
// We can't see through this reference right now, but we will be able
622+
// to once `bar` is inlined; so we shouldn't remove `foo` right now but
623+
// this is probably okay with @requireInlining.
624+
functionState.setRemove(false);
611625
} else {
612626
// e.g. var fn = bar; <== we can't inline "bar"
613627
// As this reference can't be inlined mark the function as
@@ -673,9 +687,6 @@ private void trimCandidatesUsingOnCost() {
673687
Iterator<Entry<String, FunctionState>> i;
674688
for (i = fns.entrySet().iterator(); i.hasNext(); ) {
675689
FunctionState functionState = i.next().getValue();
676-
if (hasRequireInliningAnnotation(functionState.getFn().getFunctionNode())) {
677-
continue;
678-
}
679690
if (functionState.hasReferences()) {
680691
// Only inline function if it decreases the code size.
681692
boolean lowersCost = minimizeCost(functionState);
@@ -717,13 +728,14 @@ private boolean minimizeCost(FunctionState functionState) {
717728
* @return Whether inlining the function reduces code size.
718729
*/
719730
private boolean inliningLowersCost(FunctionState functionState) {
720-
return injector.inliningLowersCost(
721-
functionState.getChunk(),
722-
functionState.getFn().getFunctionNode(),
723-
functionState.getReferences(),
724-
functionState.getNamesToAlias(),
725-
functionState.canRemove(),
726-
functionState.getReferencesThis());
731+
return functionState.requireInlining()
732+
|| injector.inliningLowersCost(
733+
functionState.getChunk(),
734+
functionState.getFn().getFunctionNode(),
735+
functionState.getReferences(),
736+
functionState.getNamesToAlias(),
737+
functionState.canRemove(),
738+
functionState.getReferencesThis());
727739
}
728740

729741
/**
@@ -769,14 +781,14 @@ private void resolveInlineConflictsForFunction(FunctionState functionState) {
769781
Node fnNode = functionState.getFn().getFunctionNode();
770782
Set<String> names = findCalledFunctions(fnNode);
771783
if (!names.isEmpty()) {
772-
// Prevent the removal of the referenced functions.
784+
// Prevent the removal of the referenced functions unless they will be immediately inlined.
773785
for (String name : names) {
774786
FunctionState fsCalled = fns.get(name);
775787
if (fsCalled != null && fsCalled.canRemove()) {
776788
fsCalled.setRemove(false);
777789
// For functions that can no longer be removed, check if they should
778790
// still be inlined.
779-
if (!minimizeCost(fsCalled)) {
791+
if (!minimizeCost(fsCalled) && !fsCalled.requireInlining()) {
780792
// It can't be inlined remove it from the list.
781793
fsCalled.disallowInlining();
782794
}
@@ -871,6 +883,9 @@ private static class FunctionState {
871883
private @Nullable JSChunk chunk = null;
872884
private @Nullable Set<String> namesToAlias = null;
873885

886+
private boolean hasRequireInliningAnnotation = false;
887+
private boolean hasRequireInliningAnnotationInitialized = false;
888+
874889
boolean hasExistingFunctionDefinition() {
875890
return (fn != null);
876891
}
@@ -931,6 +946,17 @@ public boolean canInline() {
931946
return inline;
932947
}
933948

949+
boolean requireInlining() {
950+
if (fn == null) {
951+
return false;
952+
}
953+
if (!hasRequireInliningAnnotationInitialized) {
954+
hasRequireInliningAnnotationInitialized = true;
955+
hasRequireInliningAnnotation = hasRequireInliningAnnotation(fn.getFunctionNode());
956+
}
957+
return hasRequireInliningAnnotation;
958+
}
959+
934960
public void disallowInlining() {
935961
this.inline = false;
936962

test/com/google/javascript/jscomp/InlineFunctionsTest.java

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ void maybeEnableInferConsts() {
4646
@Before
4747
public void setUp() throws Exception {
4848
super.setUp();
49+
enableDebugLogging(true);
4950
maybeEnableInferConsts();
5051
enableNormalize();
5152
// TODO(bradfordcsmith): Stop normalizing the expected output or document why it is necessary.
@@ -91,11 +92,43 @@ public void testRequireInlining() {
9192
/** @requireInlining */ function foo() {console.log('hi');console.log('bye');}
9293
foo();foo();foo();
9394
""",
94-
"{console.log('hi');console.log('bye');}".repeat(3));
95+
"""
96+
{console.log('hi');console.log('bye');}
97+
{console.log('hi');console.log('bye');}
98+
{console.log('hi');console.log('bye');}
99+
""");
100+
}
101+
102+
@Test
103+
public void testRequireInliningAliased() {
104+
// Aliasing @requireInlining functions is allowed when they are
105+
// const. The idea is that that declaration will be inlined and then
106+
// a later InlineFunctions will inline as required.
107+
testSame(
108+
"""
109+
/** @requireInlining */ function foo() {console.log('hi');console.log('bye');}
110+
const bar = foo;
111+
bar();bar();bar();
112+
""");
113+
}
114+
115+
@Test
116+
public void testRequireInliningNested() {
117+
test(
118+
"""
119+
/** @requireInlining */ function foo() {console.log('hi');console.log('bye');}
120+
/** @requireInlining */ function bar() {foo();}
121+
bar();bar();bar();
122+
""",
123+
"""
124+
{{console.log('hi');console.log('bye');}}
125+
{{console.log('hi');console.log('bye');}}
126+
{{console.log('hi');console.log('bye');}}
127+
""");
95128
}
96129

97130
@Test
98-
public void testEncourageInliningCascades() {
131+
public void testRequireInliningCascades() {
99132
disableCompareAsTree();
100133
test(
101134
"""

0 commit comments

Comments
 (0)