Skip to content

Commit

Permalink
Optimization for empty J2CL class initializers.
Browse files Browse the repository at this point in the history
In particular, this optimization detects equivalently empty function blocks and if a match is found then the function block just has all its children cleared.

The exact criteria for this is that the method's first statement must set itself to the empty function and any subsequent statements must only be recursive calls.
-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=117961112
  • Loading branch information
kevinoconnor7 authored and blickly committed Mar 23, 2016
1 parent c71fd80 commit bc96344
Show file tree
Hide file tree
Showing 4 changed files with 253 additions and 3 deletions.
19 changes: 16 additions & 3 deletions src/com/google/javascript/jscomp/DefaultPassConfig.java
Expand Up @@ -886,6 +886,10 @@ private List<PassFactory> getCodeRemovingPasses() {
passes.add(removeUnusedClassProperties);
}

if (options.j2clPass) {
passes.add(j2clEmptyClinitPrunerPass);
}

assertAllLoopablePasses(passes);
return passes;
}
Expand Down Expand Up @@ -2592,11 +2596,11 @@ protected HotSwapCompilerPass create(AbstractCompiler compiler) {
};

/** Rewrites J2CL constructs to be more optimizable. */
private final PassFactory j2clPass =
new PassFactory("j2clPass", true) {
private final PassFactory j2clEmptyClinitPrunerPass =
new PassFactory("j2clEmptyClinitPrunerPass", false) {
@Override
protected CompilerPass create(AbstractCompiler compiler) {
return new J2clPass(compiler);
return new J2clEmptyClinitPrunerPass(compiler);
}
};

Expand All @@ -2609,6 +2613,15 @@ protected CompilerPass create(AbstractCompiler compiler) {
}
};

/** Rewrites J2CL constructs to be more optimizable. */
private final PassFactory j2clPass =
new PassFactory("j2clPass", true) {
@Override
protected CompilerPass create(AbstractCompiler compiler) {
return new J2clPass(compiler);
}
};

private final PassFactory checkConformance =
new PassFactory("checkConformance", true) {
@Override
Expand Down
138 changes: 138 additions & 0 deletions src/com/google/javascript/jscomp/J2clEmptyClinitPrunerPass.java
@@ -0,0 +1,138 @@
/*
* 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 com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
import com.google.javascript.rhino.Node;

/**
* An optimziation pass for J2CL-generated code to help prune class initializers that would be seen
* as having side-effects.
*/
public class J2clEmptyClinitPrunerPass implements CompilerPass {

private final AbstractCompiler compiler;

J2clEmptyClinitPrunerPass(AbstractCompiler compiler) {
this.compiler = compiler;
}

@Override
public void process(Node externs, Node root) {
NodeTraversal.traverseEs6(compiler, root, new EmptyClinitPruner());
}

/**
* A traversal callback that sets equivalently empty clinits to the empty function.
*
* <p>A clinit is considered empty if the following criteria is met:
*
* <ol>
* <li>The first statement must set itself to be the empty function
* <li>All other statements in the body of the function must be recursive calls to itself.
* </ol>
*/
private static final class EmptyClinitPruner extends AbstractPostOrderCallback {

@Override
public void visit(NodeTraversal t, Node node, Node parent) {
if (!isClinitMethod(node)) {
return;
}

trySubstituteEmptyFunction(node, t.getCompiler());
}

private static boolean isClinitMethod(Node fnNode) {
if (!fnNode.isFunction()) {
return false;
}

String fnName = NodeUtil.getName(fnNode);

return fnName != null && (fnName.endsWith("$$0clinit") || fnName.endsWith(".$clinit"));
}

/**
* Clears the body of any functions are are equivalent to empty functions.
*/
private Node trySubstituteEmptyFunction(Node fnNode, AbstractCompiler compiler) {
Preconditions.checkArgument(fnNode.isFunction());

String fnQualifiedName = NodeUtil.getName(fnNode);

// Ignore anonymous/constructor functions.
if (Strings.isNullOrEmpty(fnQualifiedName)) {
return fnNode;
}

Node body = fnNode.getLastChild();
if (!body.hasChildren()) {
return fnNode;
}

// Ensure that the first expression in the body is setting itself to the empty function and
// that all other statements are only calls to itself.
Node firstExpr = body.getFirstChild();
if (firstExpr.isExprResult()
&& isAssignToEmptyFn(firstExpr.getFirstChild(), fnQualifiedName)
&& siblingsOnlyCallTarget(firstExpr.getNext(), fnQualifiedName)) {
body.removeChildren();
compiler.reportCodeChange();
}

return fnNode;
}

private static boolean isAssignToEmptyFn(Node assignNode, String enclosingFnName) {
if (!assignNode.isAssign()) {
return false;
}

Node lhs = assignNode.getFirstChild();
Node rhs = assignNode.getLastChild();

// If the RHS is not an empty function then this isn't of interest.
if (!NodeUtil.isEmptyFunctionExpression(rhs)) {
return false;
}

// Ensure that we are actually mutating the given function.
return lhs.getQualifiedName() != null && lhs.matchesQualifiedName(enclosingFnName);
}

private static boolean siblingsOnlyCallTarget(Node expr, String targetName) {
while (expr != null) {
if (!expr.isExprResult()
|| !expr.getFirstChild().isCall()
|| !isCallToNode(expr.getFirstChild(), targetName)) {
return false;
}
expr = expr.getNext();
}
return true;
}

private static boolean isCallToNode(Node callNode, String targetName) {
Preconditions.checkArgument(callNode.isCall());

return callNode.getFirstChild() != null
&& callNode.getFirstChild().matchesQualifiedName(targetName);
}
}
}
31 changes: 31 additions & 0 deletions test/com/google/javascript/jscomp/IntegrationTest.java
Expand Up @@ -2139,6 +2139,37 @@ public void testFoldLocals7() {
test(options, code, optimized);
}

public void testFoldJ2clClinits() {
CompilerOptions options = createCompilerOptions();

options.setJ2clPass(true);

String code =
LINE_JOINER.join(
"function InternalWidget(){}",
"InternalWidget.$clinit = function () {",
" InternalWidget.$clinit = function() {};",
" InternalWidget.$clinit();",
"};",
"InternalWidget.$clinit();");

String optimized =
LINE_JOINER.join(
"function InternalWidget(){}",
"InternalWidget.$clinit = function () {};",
"InternalWidget.$clinit();");

test(options, code, optimized);

options.setFoldConstants(true);
options.setComputeFunctionSideEffects(true);
options.setCollapseProperties(true);
options.setRemoveUnusedVars(true);
options.setInlineFunctions(true);

test(options, code, "");
}

public void testVarDeclarationsIntoFor() {
CompilerOptions options = createCompilerOptions();

Expand Down
@@ -0,0 +1,68 @@
/*
* 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;

public class J2clEmptyClinitPrunerPassTest extends CompilerTestCase {

@Override
protected CompilerPass getProcessor(Compiler compiler) {
return new J2clEmptyClinitPrunerPass(compiler);
}

public void testFoldClinit() {
test(
LINE_JOINER.join(
"var someClass = {};",
"someClass.$clinit = function() {",
" someClass.$clinit = function() {};",
"};"),
LINE_JOINER.join("var someClass = {};", "someClass.$clinit = function() {};"));
test(
LINE_JOINER.join(
"var someClass = {};",
"someClass.$clinit = function() {",
" someClass.$clinit = function() {};",
" someClass.$clinit();",
" someClass.$clinit();",
" someClass.$clinit();",
"};"),
LINE_JOINER.join("var someClass = {};", "someClass.$clinit = function() {};"));
}

public void testFoldClinit_invalidCandidates() {
testSame(
LINE_JOINER.join(
"var someClass = /** @constructor */ function() {};",
"someClass.foo = function() {};",
"someClass.$clinit = function() {",
" someClass.$clinit = function() {};",
" someClass.foo();",
"};"));
testSame(
LINE_JOINER.join(
"var someClass = {}, otherClass = {};",
"someClass.$clinit = function() {",
" otherClass.$clinit = function() {};",
" someClass.$clinit();",
"};"));
testSame(
LINE_JOINER.join(
"var someClass = {};",
"someClass.$notClinit = function() {",
" someClass.$notClinit = function() {};",
"};"));
}
}

0 comments on commit bc96344

Please sign in to comment.