Skip to content

Commit

Permalink
Allow expression decomposition in a few places where we previously we…
Browse files Browse the repository at this point in the history
…ren't allowing.

These decompositions are only allowed if a new flag, --allow_method_call_decomposing, is passed, because they produce code that will not run on some browsers.

This means expressions like x.someMethod(yield 1); can now be transpiled.

See https://github.com/google/closure-compiler/wiki/FAQ#i-get-an-undecomposable-expression-error-for-my-yield-or-await-expression-what-do-i-do (which will need to be updated after this is submitted)

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=175870681
  • Loading branch information
tbreisacher committed Nov 15, 2017
1 parent 78816ee commit 1de0f4b
Show file tree
Hide file tree
Showing 10 changed files with 324 additions and 61 deletions.
7 changes: 7 additions & 0 deletions src/com/google/javascript/jscomp/CommandLineRunner.java
Expand Up @@ -758,6 +758,11 @@ private static class Flags {
usage = "Rewrite ES6 library calls to use polyfills provided by the compiler's runtime.") usage = "Rewrite ES6 library calls to use polyfills provided by the compiler's runtime.")
private boolean rewritePolyfills = true; private boolean rewritePolyfills = true;


@Option(name = "--allow_method_call_decomposing",
handler = BooleanOptionHandler.class,
usage = "Allow decomposing x.y(); to: var tmp = x.y; tmp.call(x); Unsafe on IE 8 and 9")
private boolean allowMethodCallDecomposing = false;

@Option( @Option(
name = "--print_source_after_each_pass", name = "--print_source_after_each_pass",
handler = BooleanOptionHandler.class, handler = BooleanOptionHandler.class,
Expand Down Expand Up @@ -1728,6 +1733,8 @@ protected CompilerOptions createOptions() {
options.rewritePolyfills = options.rewritePolyfills =
flags.rewritePolyfills && options.getLanguageIn().toFeatureSet().contains(FeatureSet.ES6); flags.rewritePolyfills && options.getLanguageIn().toFeatureSet().contains(FeatureSet.ES6);


options.setAllowMethodCallDecomposing(flags.allowMethodCallDecomposing);

if (!flags.translationsFile.isEmpty()) { if (!flags.translationsFile.isEmpty()) {
try { try {
options.messageBundle = new XtbMessageBundle( options.messageBundle = new XtbMessageBundle(
Expand Down
14 changes: 14 additions & 0 deletions src/com/google/javascript/jscomp/CompilerOptions.java
Expand Up @@ -979,6 +979,20 @@ public void setTrustedStrings(boolean yes) {
trustedStrings = yes; trustedStrings = yes;
} }


private boolean allowMethodCallDecomposing;

boolean allowMethodCallDecomposing() {
return allowMethodCallDecomposing;
}

/**
* Setting this to true indicates that it's safe to rewrite x.y() as: fn = x.y; fn.call(x);
* This should be usually be false if supporting IE 8 or IE 9 is necessary.
*/
public void setAllowMethodCallDecomposing(boolean value) {
this.allowMethodCallDecomposing = value;
}

// Should only be used when debugging compiler bugs. // Should only be used when debugging compiler bugs.
boolean printSourceAfterEachPass; boolean printSourceAfterEachPass;


Expand Down
3 changes: 2 additions & 1 deletion src/com/google/javascript/jscomp/Es6ExtractClasses.java
Expand Up @@ -65,7 +65,8 @@ public final class Es6ExtractClasses
compiler, compiler,
compiler.getUniqueNameIdSupplier(), compiler.getUniqueNameIdSupplier(),
consts, consts,
Scope.createGlobalScope(new Node(Token.SCRIPT))); Scope.createGlobalScope(new Node(Token.SCRIPT)),
compiler.getOptions().allowMethodCallDecomposing());
} }


@Override @Override
Expand Down
3 changes: 2 additions & 1 deletion src/com/google/javascript/jscomp/Es6RewriteGenerators.java
Expand Up @@ -1116,7 +1116,8 @@ private final class DecomposeYields extends NodeTraversal.AbstractPreOrderCallba
compiler, compiler,
compiler.getUniqueNameIdSupplier(), compiler.getUniqueNameIdSupplier(),
consts, consts,
Scope.createGlobalScope(new Node(Token.SCRIPT))); Scope.createGlobalScope(new Node(Token.SCRIPT)),
compiler.getOptions().allowMethodCallDecomposing());
} }


@Override @Override
Expand Down
58 changes: 23 additions & 35 deletions src/com/google/javascript/jscomp/ExpressionDecomposer.java
Expand Up @@ -59,18 +59,26 @@ enum DecompositionType {
private final Set<String> knownConstants; private final Set<String> knownConstants;
private final Scope scope; private final Scope scope;


/**
* Whether to allow decomposing foo.bar to "var fn = foo.bar; fn.call(foo);"
* Should be false if targetting IE8 or IE9.
*/
private final boolean allowMethodCallDecomposing;

ExpressionDecomposer( ExpressionDecomposer(
AbstractCompiler compiler, AbstractCompiler compiler,
Supplier<String> safeNameIdSupplier, Supplier<String> safeNameIdSupplier,
Set<String> constNames, Set<String> constNames,
Scope scope) { Scope scope,
boolean allowMethodCallDecomposing) {
checkNotNull(compiler); checkNotNull(compiler);
checkNotNull(safeNameIdSupplier); checkNotNull(safeNameIdSupplier);
checkNotNull(constNames); checkNotNull(constNames);
this.compiler = compiler; this.compiler = compiler;
this.safeNameIdSupplier = safeNameIdSupplier; this.safeNameIdSupplier = safeNameIdSupplier;
this.knownConstants = constNames; this.knownConstants = constNames;
this.scope = scope; this.scope = scope;
this.allowMethodCallDecomposing = allowMethodCallDecomposing;
} }


// An arbitrary limit to prevent catch infinite recursion. // An arbitrary limit to prevent catch infinite recursion.
Expand All @@ -89,7 +97,7 @@ void maybeExposeExpression(Node expression) {
i++; i++;
if (i > MAX_ITERATIONS) { if (i > MAX_ITERATIONS) {
throw new IllegalStateException( throw new IllegalStateException(
"DecomposeExpression depth exceeded on :\n" + expression.toStringTree()); "DecomposeExpression depth exceeded on:\n" + expression.toStringTree());
} }
} }
} }
Expand Down Expand Up @@ -196,18 +204,14 @@ private void exposeExpression(Node expressionRoot, Node subExpression) {
decomposeSubExpressions(left.getFirstChild(), null, state); decomposeSubExpressions(left.getFirstChild(), null, state);
} }
} }
} else if (parentType == Token.CALL } else if (parentType == Token.CALL && NodeUtil.isGet(parent.getFirstChild())) {
&& NodeUtil.isGet(parent.getFirstChild())) {
Node functionExpression = parent.getFirstChild(); Node functionExpression = parent.getFirstChild();
decomposeSubExpressions(functionExpression.getNext(), child, state); decomposeSubExpressions(functionExpression.getNext(), child, state);
// Now handle the call expression // Now handle the call expression
if (isExpressionTreeUnsafe(functionExpression, state.sideEffects) if (isExpressionTreeUnsafe(functionExpression, state.sideEffects)
&& functionExpression.getFirstChild() != grandchild) { && functionExpression.getFirstChild() != grandchild) {
// TODO(johnlenz): In Internet Explorer, non-JavaScript objects such checkState(allowMethodCallDecomposing, "Object method calls can not be decomposed.");
// as DOM objects can not be decomposed. // Either there were preexisting side-effects, or this node has side-effects.
checkState(allowObjectCallDecomposing(), "Object method calls can not be decomposed.");
// Either there were preexisting side-effects, or this node has
// side-effects.
state.sideEffects = true; state.sideEffects = true;


// Rewrite the call so "this" is preserved. // Rewrite the call so "this" is preserved.
Expand Down Expand Up @@ -239,18 +243,6 @@ private void exposeExpression(Node expressionRoot, Node subExpression) {
} }
} }


private static boolean allowObjectCallDecomposing() {
return false;
}

/**
* @return Whether the node may represent an external method.
*/
private static boolean maybeExternMethod(Node node) {
// TODO(johnlenz): Provide some mechanism for determining this.
return true;
}

/** /**
* @return "expression" or the node closest to "expression", that does not * @return "expression" or the node closest to "expression", that does not
* have a conditional ancestor. * have a conditional ancestor.
Expand Down Expand Up @@ -519,20 +511,19 @@ private Node extractExpression(Node expr, Node injectionPoint) {
* @return The replacement node. * @return The replacement node.
*/ */
private Node rewriteCallExpression(Node call, DecompositionState state) { private Node rewriteCallExpression(Node call, DecompositionState state) {
checkArgument(call.isCall()); checkArgument(call.isCall(), call);
Node first = call.getFirstChild(); Node first = call.getFirstChild();
checkArgument(NodeUtil.isGet(first)); checkArgument(NodeUtil.isGet(first), first);


// Extracts the expression representing the function to call. For example: // Extracts the expression representing the function to call. For example:
// "a['b'].c" from "a['b'].c()" // "a['b'].c" from "a['b'].c()"
Node getVarNode = extractExpression( Node getVarNode = extractExpression(first, state.extractBeforeStatement);
first, state.extractBeforeStatement);
state.extractBeforeStatement = getVarNode; state.extractBeforeStatement = getVarNode;


// Extracts the object reference to be used as "this". For example: // Extracts the object reference to be used as "this". For example:
// "a['b']" from "a['b'].c" // "a['b']" from "a['b'].c"
Node getExprNode = getVarNode.getFirstFirstChild(); Node getExprNode = getVarNode.getFirstFirstChild();
checkArgument(NodeUtil.isGet(getExprNode)); checkArgument(NodeUtil.isGet(getExprNode), getExprNode);
Node thisVarNode = extractExpression( Node thisVarNode = extractExpression(
getExprNode.getFirstChild(), state.extractBeforeStatement); getExprNode.getFirstChild(), state.extractBeforeStatement);
state.extractBeforeStatement = thisVarNode; state.extractBeforeStatement = thisVarNode;
Expand All @@ -551,9 +542,8 @@ private Node rewriteCallExpression(Node call, DecompositionState state) {
// ... // ...
Node newCall = IR.call( Node newCall = IR.call(
IR.getprop( IR.getprop(
functionNameNode.cloneNode(), functionNameNode.cloneNode(), IR.string("call")),
IR.string("call")), thisNameNode.cloneNode()).useSourceInfoIfMissingFromForTree(call);
thisNameNode.cloneNode()).srcref(call);


// Throw away the call name // Throw away the call name
call.removeFirstChild(); call.removeFirstChild();
Expand All @@ -562,9 +552,7 @@ private Node rewriteCallExpression(Node call, DecompositionState state) {
newCall.addChildrenToBack(call.removeChildren()); newCall.addChildrenToBack(call.removeChildren());
} }


// Replace the call. call.replaceWith(newCall);
Node callParent = call.getParent();
callParent.replaceChild(call, newCall);


return newCall; return newCall;
} }
Expand Down Expand Up @@ -824,10 +812,10 @@ private DecompositionType isSubexpressionMovable(
// //
Node first = parent.getFirstChild(); Node first = parent.getFirstChild();
if (requiresDecomposition && parent.isCall() && NodeUtil.isGet(first)) { if (requiresDecomposition && parent.isCall() && NodeUtil.isGet(first)) {
if (maybeExternMethod(first)) { if (allowMethodCallDecomposing) {
return DecompositionType.UNDECOMPOSABLE;
} else {
return DecompositionType.DECOMPOSABLE; return DecompositionType.DECOMPOSABLE;
} else {
return DecompositionType.UNDECOMPOSABLE;
} }
} }
} }
Expand Down
12 changes: 10 additions & 2 deletions src/com/google/javascript/jscomp/FunctionInjector.java
Expand Up @@ -449,8 +449,12 @@ private CallSiteType classifyCallSite(Reference ref) {
} else { } else {
Node expressionRoot = ExpressionDecomposer.findExpressionRoot(callNode); Node expressionRoot = ExpressionDecomposer.findExpressionRoot(callNode);
if (expressionRoot != null) { if (expressionRoot != null) {
// TODO(tbreisacher): Change this to compiler.getOptions().allowMethodCallDecomposing().
// Doing so currently causes a "DecomposeExpression depth exceeded" error.
boolean allowMethodCallDecomposing = false;

ExpressionDecomposer decomposer = new ExpressionDecomposer( ExpressionDecomposer decomposer = new ExpressionDecomposer(
compiler, safeNameIdSupplier, knownConstants, ref.scope); compiler, safeNameIdSupplier, knownConstants, ref.scope, allowMethodCallDecomposing);
DecompositionType type = decomposer.canExposeExpression( DecompositionType type = decomposer.canExposeExpression(
callNode); callNode);
if (type == DecompositionType.MOVABLE) { if (type == DecompositionType.MOVABLE) {
Expand All @@ -467,8 +471,12 @@ private CallSiteType classifyCallSite(Reference ref) {
} }


private ExpressionDecomposer getDecomposer(Scope scope) { private ExpressionDecomposer getDecomposer(Scope scope) {
// TODO(tbreisacher): Change this to compiler.getOptions().allowMethodCallDecomposing().
// Doing so currently causes a "DecomposeExpression depth exceeded" error.
boolean allowMethodCallDecomposing = false;

return new ExpressionDecomposer( return new ExpressionDecomposer(
compiler, safeNameIdSupplier, knownConstants, scope); compiler, safeNameIdSupplier, knownConstants, scope, allowMethodCallDecomposing);
} }


/** /**
Expand Down
34 changes: 32 additions & 2 deletions test/com/google/javascript/jscomp/Es6RewriteGeneratorsTest.java
Expand Up @@ -21,10 +21,12 @@


/** Unit tests for {@link Es6RewriteGenerators}. */ /** Unit tests for {@link Es6RewriteGenerators}. */
public final class Es6RewriteGeneratorsTest extends CompilerTestCase { public final class Es6RewriteGeneratorsTest extends CompilerTestCase {
private boolean allowMethodCallDecomposing;


@Override @Override
protected void setUp() throws Exception { protected void setUp() throws Exception {
super.setUp(); super.setUp();
allowMethodCallDecomposing = false;
setAcceptedLanguage(LanguageMode.ECMASCRIPT_2015); setAcceptedLanguage(LanguageMode.ECMASCRIPT_2015);
enableRunTypeCheckAfterProcessing(); enableRunTypeCheckAfterProcessing();
} }
Expand All @@ -33,6 +35,7 @@ protected void setUp() throws Exception {
protected CompilerOptions getOptions() { protected CompilerOptions getOptions() {
CompilerOptions options = super.getOptions(); CompilerOptions options = super.getOptions();
options.setLanguageOut(LanguageMode.ECMASCRIPT3); options.setLanguageOut(LanguageMode.ECMASCRIPT3);
options.setAllowMethodCallDecomposing(allowMethodCallDecomposing);
return options; return options;
} }


Expand Down Expand Up @@ -406,8 +409,35 @@ public void testWhileLoopsGenerator() {
} }


public void testUndecomposableExpression() { public void testUndecomposableExpression() {
testError("function *f() { obj.bar(yield 5); }", testError("function *f() { obj.bar(yield 5); }", Es6ToEs3Util.CANNOT_CONVERT);
Es6ToEs3Util.CANNOT_CONVERT); }

public void testDecomposableExpression() {
allowMethodCallDecomposing = true;
rewriteGeneratorBodyWithVars(
"obj.bar(yield 5);",
LINE_JOINER.join(
"var $jscomp$generator$next$arg2;",
"var JSCompiler_temp_const$jscomp$0;",
"var JSCompiler_temp_const$jscomp$1;"),
LINE_JOINER.join(
"case 0:",
" JSCompiler_temp_const$jscomp$1 = obj;",
" JSCompiler_temp_const$jscomp$0 = JSCompiler_temp_const$jscomp$1.bar;",
" $jscomp$generator$state = 1;",
" return { value: 5, done: false };",
"case 1:",
" if (!($jscomp$generator$action$arg ==1 )) {",
" $jscomp$generator$state = 2;",
" break;",
" }",
" $jscomp$generator$state = -1;",
" throw $jscomp$generator$throw$arg;",
"case 2:",
" $jscomp$generator$next$arg2 = $jscomp$generator$next$arg;",
" JSCompiler_temp_const$jscomp$0.call(",
" JSCompiler_temp_const$jscomp$1, $jscomp$generator$next$arg2);",
" $jscomp$generator$state = -1;"));
} }


public void testGeneratorCannotConvertYet() { public void testGeneratorCannotConvertYet() {
Expand Down

0 comments on commit 1de0f4b

Please sign in to comment.