diff --git a/src/com/google/javascript/jscomp/AbstractPeepholeOptimization.java b/src/com/google/javascript/jscomp/AbstractPeepholeOptimization.java index 2d8c3ea82ed..1eee6c14664 100644 --- a/src/com/google/javascript/jscomp/AbstractPeepholeOptimization.java +++ b/src/com/google/javascript/jscomp/AbstractPeepholeOptimization.java @@ -23,9 +23,13 @@ import com.google.javascript.jscomp.base.Tri; import com.google.javascript.jscomp.parsing.parser.FeatureSet; import com.google.javascript.jscomp.parsing.parser.FeatureSet.Feature; +import com.google.javascript.rhino.IR; import com.google.javascript.rhino.Node; +import com.google.javascript.rhino.Token; import java.math.BigInteger; +import java.util.ArrayDeque; import java.util.LinkedHashSet; +import org.jspecify.annotations.Nullable; /** * An abstract class whose implementations run peephole optimizations: @@ -213,6 +217,239 @@ protected boolean nodeTypeMayHaveSideEffects(Node n) { return astAnalyzer.nodeTypeMayHaveSideEffects(n); } + /** + * Replaces an {@code expression} with a provided {@code value}, which must have been obtained by + * evaluating the expression. The present function ensures that the side effects of the {@code + * expression} is preserved after the replacement. + * + *
This is achieved by substituting the original expression with the comma node (expr, value) + * and then simplify the first child using {@link trySimplifyUnusedResult()}. + * + *
If the provided expression is a call such as `fn(innerExpr)`, we require that fn has no side + * effects. This is because if fn had side effects, no simplifications can occur and one ends up + * with (fn(innerExpr), value), which is less optimized than the initial expression. + * + * @return The replacement expression, which can be of the form `(..., value)` or just `value`. + */ + protected final Node replaceExpressionWithEvalResult(Node expr, Node value) { + checkState(expr.hasParent(), expr); + checkState(!value.hasParent(), value); + if (expr.isCall() || expr.isOptChainCall()) { + checkState(expr.isNoSideEffectsCall(), expr); + } + Node comma = new Node(Token.COMMA, value.srcref(expr)); + expr.replaceWith(comma); + comma.addChildToFront(expr); // comma = (expr, value) + expr = trySimplifyUnusedResult(expr); + if (expr == null || !mayHaveSideEffects(expr)) { + value.detach(); + comma.replaceWith(value); + if (expr != null) { + markFunctionsDeleted(expr); + } + expr = value; // expr = value + } else { + expr = comma; // expr = (..., value) + } + reportChangeToEnclosingScope(expr); + return expr; + } + + /** + * Replaces {@code expression} with an expression that contains only side-effects of the original. + * + *
This replacement is made under the assumption that the result of {@code expression} is
+ * unused and therefore it is correct to eliminate non-side-effectful nodes.
+ *
+ * @return The replacement expression, or {@code null} if there were no side-effects to preserve.
+ */
+ protected final @Nullable Node trySimplifyUnusedResult(Node expression) {
+ ArrayDeque When a node is determined to have side-effects its descendants are not explored. This method
+ * assumes the entire subtree of such a node must be preserved. As a corollary, the contents of
+ * {@code sideEffectRoots} are a forest.
+ *
+ * This operation generally does not mutate {@code tree}; however, exceptions are made for
+ * expressions that alter control-flow. Such expression will be pruned of their side-effectless
+ * branches. Even in this case, {@code tree} is never detached.
+ *
+ * @param sideEffectRoots The roots of subtrees determined to have side-effects, in execution
+ * order.
+ * @return {@code true} iff there is no code to be removed from within {@code tree}; it is already
+ * at a fixed point for code removal.
+ */
+ private boolean trySimplifyUnusedResultInternal(Node tree, ArrayDeque A fixed-point representation is one in which no further nodes should be changed or removed
+ * when removing unused code. This method assumes that the expression tree in question is unused,
+ * so only side-effects are relevant.
+ */
+ private static boolean hasFixedPointParent(Node expr) {
+ // Most kinds of nodes shouldn't be branches in the fixed-point tree of an unused
+ // expression. Those listed below are the only valid kinds.
+ return switch (expr.getParent().getToken()) {
+ case AND, COMMA, HOOK, OR, COALESCE -> true;
+ case ARRAYLIT, OBJECTLIT ->
+ // Make a special allowance for SPREADs so they remain in a legal context. Parent types
+ // other than ARRAYLIT and OBJECTLIT are not fixed-point because they are the tersest
+ // legal
+ // parents and are known to be side-effect free.
+ expr.isSpread();
+ default ->
+ // Statments are always fixed-point parents. All other expressions are not.
+ NodeUtil.isStatement(expr.getParent());
+ };
+ }
+
/**
* Returns whether the output language is ECMAScript 5 or later. Workarounds for quirks in
* browsers that do not support ES5 can be ignored when this is true.
diff --git a/src/com/google/javascript/jscomp/PeepholeFoldConstants.java b/src/com/google/javascript/jscomp/PeepholeFoldConstants.java
index f6a8086a47b..4f320a173ac 100644
--- a/src/com/google/javascript/jscomp/PeepholeFoldConstants.java
+++ b/src/com/google/javascript/jscomp/PeepholeFoldConstants.java
@@ -28,6 +28,8 @@
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.javascript.jscomp.NodeUtil.ValueType;
import com.google.javascript.jscomp.base.Tri;
+import com.google.javascript.jscomp.colors.Color;
+import com.google.javascript.jscomp.colors.ColorId;
import com.google.javascript.jscomp.colors.StandardColors;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.Node;
@@ -48,7 +50,6 @@ class PeepholeFoldConstants extends AbstractPeepholeOptimization {
DiagnosticType.warning("JSC_FRACTIONAL_BITWISE_OPERAND", "Fractional bitwise operand: {0}");
private static final double MAX_FOLD_NUMBER = Math.pow(2, 53);
-
private final boolean late;
private final boolean shouldUseTypes;
@@ -239,46 +240,77 @@ private void tryConvertToNumber(Node n) {
reportChangeToEnclosingScope(replacement);
}
- /**
- * Folds 'typeof(foo)' if foo is a literal, e.g. typeof("bar") --> "string" typeof(6) --> "number"
- */
- private Node tryFoldTypeof(Node originalTypeofNode) {
- checkArgument(originalTypeofNode.isTypeOf());
-
- Node argumentNode = originalTypeofNode.getFirstChild();
- if (argumentNode == null || !NodeUtil.isLiteralValue(argumentNode, true)) {
- return originalTypeofNode;
+ private @Nullable String tryEvalTypeof(Node typeofExpr) {
+ Node operand = typeofExpr.getOnlyChild();
+ switch (operand.getToken()) {
+ case FUNCTION, CLASS -> {
+ return "function";
+ }
+ default -> {}
}
+ return switch (NodeUtil.getKnownValueType(operand)) {
+ case STRING -> "string";
+ case NUMBER -> "number";
+ case BIGINT -> "bigint";
+ case BOOLEAN -> "boolean";
+ case NULL, OBJECT -> "object";
+ case VOID -> "undefined";
+ default -> shouldUseTypes ? tryEvalTypeofFromColor(operand.getColor()) : null;
+ };
+ }
- String typeNameString = null;
-
- switch (argumentNode.getToken()) {
- case FUNCTION -> typeNameString = "function";
- case STRINGLIT -> typeNameString = "string";
- case NUMBER -> typeNameString = "number";
- case TRUE, FALSE -> typeNameString = "boolean";
- case NULL, OBJECTLIT, ARRAYLIT -> typeNameString = "object";
- case VOID -> typeNameString = "undefined";
- case NAME -> {
- // We assume here that programs don't change the value of the
- // keyword undefined to something other than the value undefined.
- if (argumentNode.getString().equals("undefined")) {
- typeNameString = "undefined";
+ private static @Nullable String tryEvalTypeofFromColor(@Nullable Color color) {
+ if (color == null) {
+ return null;
+ }
+ if (color.isUnion()) {
+ String unionResult = null;
+ for (Color element : color.getUnionElements()) {
+ String elementResult = tryEvalTypeofFromColor(element);
+ if (elementResult == null) {
+ return null;
+ }
+ if (unionResult == null) {
+ unionResult = elementResult;
+ } else if (!unionResult.equals(elementResult)) {
+ return null;
}
}
- default -> {}
+ return unionResult;
+ } else if (color.isConstructor()) {
+ return "function";
+ }
+ ColorId colorId = color.getId();
+ if (color.isPrimitive()) {
+ if (colorId.equals(StandardColors.STRING.getId())) {
+ return "string";
+ } else if (colorId.equals(StandardColors.NUMBER.getId())) {
+ return "number";
+ } else if (colorId.equals(StandardColors.BIGINT.getId())) {
+ return "bigint";
+ } else if (colorId.equals(StandardColors.BOOLEAN.getId())) {
+ return "boolean";
+ } else if (colorId.equals(StandardColors.SYMBOL.getId())) {
+ return "symbol";
+ } else {
+ return null;
+ }
}
+ return (colorId.equals(StandardColors.TOP_OBJECT.getId())
+ || colorId.equals(StandardColors.UNKNOWN.getId()))
+ ? null
+ : "object";
+ }
- if (typeNameString != null) {
- Node newNode = IR.string(typeNameString);
- reportChangeToEnclosingScope(originalTypeofNode);
- originalTypeofNode.replaceWith(newNode);
- markFunctionsDeleted(originalTypeofNode);
+ /** Folds 'typeof foo' when the runtime result of {@code foo} is known. */
+ private Node tryFoldTypeof(Node typeOfNode) {
+ checkArgument(typeOfNode.isTypeOf());
- return newNode;
+ String typeName = tryEvalTypeof(typeOfNode);
+ if (typeName != null) {
+ typeOfNode = replaceExpressionWithEvalResult(typeOfNode, IR.string(typeName));
}
-
- return originalTypeofNode;
+ return typeOfNode;
}
private Node tryFoldUnaryOperator(Node n) {
@@ -1758,8 +1790,9 @@ private Node tryFoldSpread(Node spread) {
parent.addChildrenAfter(child.removeChildren(), spread);
spread.detach();
reportChangeToEnclosingScope(parent);
+ return parent;
}
- return parent;
+ return spread;
}
/**
diff --git a/src/com/google/javascript/jscomp/PeepholeRemoveDeadCode.java b/src/com/google/javascript/jscomp/PeepholeRemoveDeadCode.java
index 1083d7fb73b..cc8a13ad334 100644
--- a/src/com/google/javascript/jscomp/PeepholeRemoveDeadCode.java
+++ b/src/com/google/javascript/jscomp/PeepholeRemoveDeadCode.java
@@ -26,7 +26,6 @@
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;
-import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
import org.jspecify.annotations.Nullable;
@@ -310,201 +309,6 @@ private Node tryFoldExpr(Node subtree) {
return subtree;
}
- /**
- * Replaces {@code expression} with an expression that contains only side-effects of the original.
- *
- * This replacement is made under the assumption that the result of {@code expression} is
- * unused and therefore it is correct to eliminate non-side-effectful nodes.
- *
- * @return The replacement expression, or {@code null} if there were no side-effects to preserve.
- */
- private @Nullable Node trySimplifyUnusedResult(Node expression) {
- ArrayDeque When a node is determined to have side-effects its descendants are not explored. This method
- * assumes the entire subtree of such a node must be preserved. As a corollary, the contents of
- * {@code sideEffectRoots} are a forest.
- *
- * This operation generally does not mutate {@code tree}; however, exceptions are made for
- * expressions that alter control-flow. Such expression will be pruned of their side-effectless
- * branches. Even in this case, {@code tree} is never detached.
- *
- * @param sideEffectRoots The roots of subtrees determined to have side-effects, in execution
- * order.
- * @return {@code true} iff there is no code to be removed from within {@code tree}; it is already
- * at a fixed point for code removal.
- */
- private boolean trySimplifyUnusedResultInternal(Node tree, ArrayDeque A fixed-point representation is one in which no further nodes should be changed or removed
- * when removing unused code. This method assumes that the expression tree in question is unused,
- * so only side-effects are relevant.
- */
- private static boolean hasFixedPointParent(Node expr) {
- // Most kinds of nodes shouldn't be branches in the fixed-point tree of an unused
- // expression. Those listed below are the only valid kinds.
- return switch (expr.getParent().getToken()) {
- case AND, COMMA, HOOK, OR, COALESCE -> true;
- case ARRAYLIT, OBJECTLIT ->
- // Make a special allowance for SPREADs so they remain in a legal context. Parent types
- // other than ARRAYLIT and OBJECTLIT are not fixed-point because they are the tersest
- // legal
- // parents and are known to be side-effect free.
- expr.isSpread();
- default ->
- // Statments are always fixed-point parents. All other expressions are not.
- NodeUtil.isStatement(expr.getParent());
- };
- }
-
/** A predicate for matching anything except function nodes. */
private static class MatchUnnamedBreak implements Predicate Example:
+ *
+ * If the return value can be determined with certainty, returns Tri.TRUE or Tri.FALSE;
+ * otherwise returns Tri.UNKNOWN.
+ *
+ * While JSType's can lead to more refined evaluation, since they are not available at
+ * peepholes in most common pass configs, we rely on colors only.
+ */
+ private Tri tryEvalArrayIsArray(Node expr) {
+ Node arg = expr.getSecondChild();
+ if (arg == null) {
+ return Tri.FALSE;
+ }
+ if (arg.isSpread()) {
+ return Tri.UNKNOWN;
+ }
+ if (arg.isArrayLit()) {
+ return Tri.TRUE;
+ }
+ Color color = useTypes ? arg.getColor() : null;
+ if (color == null) {
+ return Tri.UNKNOWN;
+ }
+ if (DEFINITELY_ARRAY_COLOR_IDS.contains(color.getId())) {
+ return Tri.TRUE;
+ }
+ if (color.isUnion()) {
+ boolean allArrays = true;
+ boolean allPrimitives = true;
+ for (Color element : color.getUnionElements()) {
+ allArrays &= DEFINITELY_ARRAY_COLOR_IDS.contains(element.getId());
+ allPrimitives &= element.isPrimitive();
+ if (!allArrays && !allPrimitives) {
+ return Tri.UNKNOWN;
+ }
+ }
+ return Tri.forBoolean(allArrays);
+ }
+ return color.isPrimitive() ? Tri.FALSE : Tri.UNKNOWN;
}
private Node tryFoldKnownNumberMethods(Node subtree, Node callTarget) {
diff --git a/src/com/google/javascript/jscomp/testing/TestExternsBuilder.java b/src/com/google/javascript/jscomp/testing/TestExternsBuilder.java
index beec6e0e088..2884a5dec3f 100644
--- a/src/com/google/javascript/jscomp/testing/TestExternsBuilder.java
+++ b/src/com/google/javascript/jscomp/testing/TestExternsBuilder.java
@@ -518,8 +518,9 @@ function ReadonlyArray(var_args) {}
* @param {...*} var_args
* @return {!Array>}
* @this {*}
+ * @nosideeffects
*/
- ReadonlyArray.prototype.concat;
+ ReadonlyArray.prototype.concat = function(var_args) {};
/**
* @param {?number=} begin Zero-based index at which to begin extraction.
* @param {?number=} end Zero-based index at which to end extraction. slice
@@ -610,6 +611,7 @@ function Array(var_args) {}
* @param {...*} var_args
* @return {!Array>}
* @this {*}
+ * @nosideeffects
*/
Array.prototype.concat = function(var_args) {};
/**
diff --git a/test/com/google/javascript/jscomp/PeepholeFoldConstantsTest.java b/test/com/google/javascript/jscomp/PeepholeFoldConstantsTest.java
index c67ab886ec1..e3980bdeb1f 100644
--- a/test/com/google/javascript/jscomp/PeepholeFoldConstantsTest.java
+++ b/test/com/google/javascript/jscomp/PeepholeFoldConstantsTest.java
@@ -1545,21 +1545,58 @@ public void testFoldStringLength() {
@Test
public void testFoldTypeof() {
- test("x = typeof 1", "x = \"number\"");
- test("x = typeof 'foo'", "x = \"string\"");
- test("x = typeof true", "x = \"boolean\"");
- test("x = typeof false", "x = \"boolean\"");
- test("x = typeof null", "x = \"object\"");
- test("x = typeof undefined", "x = \"undefined\"");
- test("x = typeof void 0", "x = \"undefined\"");
- test("x = typeof []", "x = \"object\"");
- test("x = typeof [1]", "x = \"object\"");
- test("x = typeof [1,[]]", "x = \"object\"");
- test("x = typeof {}", "x = \"object\"");
+ test("x = typeof 1", "x = 'number'");
+ test("x = typeof 'foo'", "x = 'string'");
+ test("x = typeof true", "x = 'boolean'");
+ test("x = typeof false", "x = 'boolean'");
+ test("x = typeof null", "x = 'object'");
+ test("x = typeof undefined", "x = 'undefined'");
+ test("x = typeof void 0", "x = 'undefined'");
+ test("x = typeof []", "x = 'object'");
+ test("x = typeof [1]", "x = 'object'");
+ test("x = typeof [1,[]]", "x = 'object'");
+ test("x = typeof {}", "x = 'object'");
test("x = typeof function() {}", "x = 'function'");
- testSame("x = typeof[1,[foo()]]");
- testSame("x = typeof{bathwater:baby()}");
+ test("x = typeof[1,[foo()]]", "x = (foo(), 'object')");
+ test("x = typeof{bathwater:baby()}", "x = (baby(), 'object')");
+
+ test("x = typeof(a = 1)", "x = (a = 1, 'number')");
+ test("x = typeof(foo(), null)", "x = (foo(), 'object')");
+ test("x = typeof(foo() ? 'a' : 'b')", "x = (foo(), 'string')");
+ test("x = typeof void bar()", "x = (bar(), 'undefined')");
+ }
+
+ @Test
+ public void testFoldTypeofFromColor() {
+ enableTypeCheck();
+ replaceTypesWithColors();
+ disableCompareJsDoc();
+
+ test(
+ "/** @constructor */ function Foo() {} function f(/** !Foo */ x) { return typeof x; }",
+ "/** @constructor */ function Foo() {} function f(/** !Foo */ x) { return 'object'; }");
+ test(
+ "/** @constructor */ function Foo() {} function f() { return typeof Foo; }",
+ "/** @constructor */ function Foo() {} function f() { return 'function'; }");
+ test(
+ "/** @constructor */ function Foo() {} function f() { return typeof new Foo(); }",
+ "/** @constructor */ function Foo() {} function f() { return new Foo(), 'object'; }");
+ test(
+ "/** @constructor */ function Foo() {} function f(/** typeof Foo */ x) { return typeof x;"
+ + " }",
+ "/** @constructor */ function Foo() {} function f(/** typeof Foo */ x) { return 'function';"
+ + " }");
+ test(
+ "function f(/** string */ x) { return typeof (x || 'fallback'); }",
+ "function f(/** string */ x) { return 'string'; }");
+ test(
+ "/** @constructor */ function Foo() {}"
+ + " function f(/** (!Foo|undefined) */ x, /** !Foo */ y) { return typeof (x ?? y); }",
+ "/** @constructor */ function Foo() {}"
+ + " function f(/** (!Foo|undefined) */ x, /** !Foo */ y) { return 'object'; }");
+ testSame("function f(x) { return x; }; typeof f;");
+ testSame("function f(/** function(): number */ x) { return typeof x; }");
}
@Test
diff --git a/test/com/google/javascript/jscomp/PeepholeReplaceKnownMethodsTest.java b/test/com/google/javascript/jscomp/PeepholeReplaceKnownMethodsTest.java
index 58287deb180..3d79b4ca078 100644
--- a/test/com/google/javascript/jscomp/PeepholeReplaceKnownMethodsTest.java
+++ b/test/com/google/javascript/jscomp/PeepholeReplaceKnownMethodsTest.java
@@ -764,13 +764,13 @@ public void testReplaceWithCharAt() {
foldSame(
"""
/** @constructor */ function A() {};
- A.prototype.substring = function(begin$jscomp$1, end$jscomp$1) {};
+ A.prototype.substring = function(begin$jscomp$2, end$jscomp$2) {};
function f(/** !A */ a) { a.substring(0, 1); }
""");
foldSame(
"""
/** @constructor */ function A() {};
- A.prototype.slice = function(begin$jscomp$1, end$jscomp$1) {};
+ A.prototype.slice = function(begin$jscomp$2, end$jscomp$2) {};
function f(/** !A */ a) { a.slice(0, 1); }
""");
@@ -780,6 +780,108 @@ public void testReplaceWithCharAt() {
foldSameStringTyped("''.substring(i, i + 1)");
}
+ @Test
+ public void testFoldArrayIsArray() {
+ enableTypeCheck();
+ replaceTypesWithColors();
+ disableCompareJsDoc();
+
+ foldArrayIsArrayTyped("!Array", "true");
+ foldArrayIsArrayTyped("!Array{@code
+ * Number.parseFloat("0.1") -> 0.1
+ * "(".charCodeAt(0) -> 40
+ * ['a', 'b', 'c'].join('') -> 'abc'
+ * Array.isArray([1, 2]) -> true
+ * Array.isArray(x.sort()) -> x.sort(), true
+ * }
+ */
class PeepholeReplaceKnownMethods extends AbstractPeepholeOptimization {
+ private static final ImmutableSet