/* * Copyright 2010 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.collect.ImmutableSet; import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback; import com.google.javascript.jscomp.SideEffectsAnalysis.LocationAbstractionMode; import com.google.javascript.rhino.Node; /** * Tests for {@link SideEffectsAnalysis}. * * @author dcc@google.com (Devin Coughlin) * */ public class SideEffectsAnalysisTest extends CompilerTestCase { private static final String SHARED_EXTERNS = "var arguments = [];"; LocationAbstractionMode currentLocationAbstractionIdentifier; SideEffectsAnalysis currentAnalysis = null; Compiler currentCompiler = null; Node currentJsRoot = null; @Override protected CompilerPass getProcessor(final Compiler compiler) { currentCompiler = compiler; currentAnalysis = new SideEffectsAnalysis(compiler, currentLocationAbstractionIdentifier); return new CompilerPass() { @Override public void process(Node externs, Node root) { if (currentLocationAbstractionIdentifier == LocationAbstractionMode.VISIBILITY_BASED) { // Run var when using the visibility abstraction // because it is unsound if it fails. final VarCheck varCheck = new VarCheck(compiler); varCheck.process(externs, root); } currentAnalysis.process(externs, root); } }; } @Override public void setUp() throws Exception { super.setUp(); currentAnalysis = null; currentCompiler = null; } public void testDegenerateSafeMoves() { // Env is empty assertSafeMoveDegenerate("src: 1; env: ; dest: 3;"); // Src and env pure assertSafeMoveDegenerate("src: 1; env: 2; dest: 3;"); // Only refs assertSafeMoveDegenerate("src: 1; env: x; dest: 3;"); assertSafeMoveDegenerate("src: x; env: 1; dest: 3;"); // Only mods assertSafeMoveDegenerate("src: 1; env: x++; dest: 3;"); assertSafeMoveDegenerate("src: x++; env: 1; dest: 3;"); } public void testVisibilitySafeMoves() { // Env is empty assertSafeMoveVisibility("src: 1; env: ; dest: 3;"); // Src and env pure assertSafeMoveVisibility("src: 1; env: 2; dest: 3;"); // Only refs assertSafeMoveVisibility("var x; src: 1; env: x; dest: 3;"); assertSafeMoveVisibility("var x; src: x; env: 1; dest: 3;"); // Only mods assertSafeMoveVisibility("var x; src: 1; env: x++; dest: 3;"); assertSafeMoveVisibility("var x; src: x++; env: 1; dest: 3;"); // Source references global, env changes local assertSafeMoveVisibility( "var x;" + "function f(){" + "var y;" + "src: x;" + "env: y++;" + "dest: 3;" + "}"); // Source changes global, env refs local assertSafeMoveVisibility( "var x;" + "function f(){" + "var y;" + "src: x++;" + "env: y;" + "dest: 3;" + "}"); // Source references global, env changes local with shadowing assertSafeMoveVisibility( "var x;" + "var y;" + "function f(){" + "var y;" + "src: x;" + "env: y++;" + "dest: 3;" + "}"); // Source changes global, env refs local with shadowing assertSafeMoveVisibility( "var x;" + "var y;" + "function f(){" + "var y;" + "src: x++;" + "env: y;" + "dest: 3;" + "}"); // Source references captured local, env changes local assertSafeMoveVisibility( "function f(){" + "var x;" + "var y;" + "src: x;" + "env: y++;" + "dest: 3;" + "function inner() {" + "x" + "}" + "}"); // Source changes captured local, env refs local assertSafeMoveVisibility( "function f(){" + "var x;" + "var y;" + "src: x++;" + "env: y;" + "dest: 3;" + "function inner() {" + "x" + "}" + "}"); // Source references heap, env changes local assertSafeMoveVisibility( "var x = {};" + "function f(){" + "var y;" + "src: x.a;" + "env: y++;" + "dest: 3;" + "}"); // Source changes heap, env refs local assertSafeMoveVisibility( "var x = {};" + "function f(){" + "var y;" + "src: x.a++;" + "env: y;" + "dest: 3;" + "}"); // MOD in function expressions shouldn't count assertSafeMoveVisibility( "var x = {};" + "src: x.a;" + "env: (function() {" + "x.a++;" + "});" + "dest: 3;"); // REF in function expressions shouldn't count assertSafeMoveVisibility( "var x = {};" + "src: x.a++;" + "env: (function() {" + "x.a;" + "});" + "dest: 3;"); } public void testDegenerateUnsafeMoves() { // Unsafe to move increment across read assertUnsafeMoveDegenerate("src: x++; env: foo(y); dest: 3;"); // Unsafe to move read across increment assertUnsafeMoveDegenerate("src: foo(y); env: x++; dest: 3;"); // Unsafe to move write across write assertUnsafeMoveDegenerate("src: x = 7; env: y = 3; dest:3;"); } public void testVisibilityUnsafeMoves() { // Unsafe to move increment across read for global variables assertUnsafeMoveVisibility("var x,y; src: x++; env: y; dest: 3;"); // Unsafe to move increment across read for local variables assertUnsafeMoveVisibility("function f() {" + "var x,y; src: x++; env: y; dest: 3;" + "}"); // Unsafe to move increment across read for captured local variables assertUnsafeMoveVisibility( "function f() {" + "var x,y; src: x++; env: y; dest: 3;" + "function inner() {" + "x; y;" + "}" + "}"); // Unsafe to move increment across read for heap locations assertUnsafeMoveVisibility("var x,y; src: x.a++; env: y.b; dest: 3;"); // Unsafe to move read across increment of for global variables assertUnsafeMoveVisibility("var x,y; src: y; env: x++; dest: 3;"); // Unsafe to move read across increment for local variables assertUnsafeMoveVisibility("function f() {" + "var x,y; src: x; env: y++; dest: 3;" + "}"); // Unsafe to move read across increment for captured local variables assertUnsafeMoveVisibility( "function f() {" + "var x,y; src: x; env: y++; dest: 3;" + "function inner() {" + "x; y;" + "}" + "}"); // Unsafe to move read across increment for heap locations assertUnsafeMoveVisibility("var x,y; src: x.a; env: y.b++; dest: 3;"); // Unsafe to move write across write for globals assertUnsafeMoveVisibility("var x,y; src: x = 7; env: y = 3; dest: 3;"); // Unsafe to move write across write for local variables assertUnsafeMoveVisibility("function f() {" + "var x,y; src: x = 7; env: y = 3; dest: 3;" + "}"); // Unsafe to move write across write for captured local variables assertUnsafeMoveVisibility( "function f() {" + "var x,y; src: x = 7; env: y = 3; dest: 3;" + "function inner() {" + "x; y;" + "}" + "}"); // Unsafe to move write across write for heap locations assertUnsafeMoveVisibility("var x,y; src: x.a = 7; env: y.b = 3; dest: 3;"); } public void testVisibilityMoveCalls() { // Interprocedural side effect analysis isn't implemented yet, so any calls // should make movement unsafe, since we don't know what those calls are // doing. // TODO(dcc): implement interprocedural side effect analysis. // Source makes call, env refs global assertUnsafeMoveVisibility( "var x = {};" + "var g = function(){};" + "function f(){" + "var y;" + "src: g();" + "env: x;" + "dest: 3;" + "}"); // Source makes refs global, env makes call assertUnsafeMoveVisibility( "var x = {};" + "var g = function(){};" + "function f(){" + "var y;" + "src: x;" + "env: g();" + "dest: 3;" + "}"); } public void testVisibilityMergesParametersWithHeap() { // For now, we expect the visibility based location abstraction // to merge parameter variable locations with heap locations because // parameters can be references and modified via the arguments object. // Source changes heap, env refs parameter assertUnsafeMoveVisibility( "var x = {};" + "function f(y){" + "src: x[0]++;" + "env: y;" + "dest: 3;" + "}"); // Source refs heap, env changes parameters assertUnsafeMoveVisibility( "var x = {};" + "function f(y){" + "src: x[0];" + "env: y++;" + "dest: 3;" + "}"); // Source changes arguments explicitly, env refs parameter assertUnsafeMoveVisibility( "var x = {};" + "function f(y){" + "src: arguments[0]++;" + "env: y;" + "dest: 3;" + "}"); // Source refs arguments explicitly, env changes parameter assertUnsafeMoveVisibility( "var x = {};" + "function f(y){" + "src: arguments[0];" + "env: y++;" + "dest: 3;" + "}"); } public void testMovedSideEffectsMustHaveSameControlFlow() { // Safe to move within IF block assertSafeMoveVisibility( "var a;" + "function f() {" + "var l;" + "if (l) {" + "src: a++;" + "env: 3;" + "dest: 3;" + "}" + "}" ); // Unsafe to move between two IF blocks assertUnsafeMoveVisibility( "var a;" + "function f() {" + "var l;" + "if (l) {" + "src: a++;" + "env: 3;" + "}" + "if (l) {" + "dest: 3;" + "}" + "}" ); // Unsafe to move between then/else of same IF block assertUnsafeMoveVisibility( "var a;" + "function f() {" + "var l;" + "if (l) {" + "src: a++;" + "env: 3;" + "} else {" + "dest: 3;" + "}" + "}" ); // Safe to move within WHILE block assertSafeMoveVisibility( "var a;" + "function f() {" + "var l;" + "while (l) {" + "src: a++;" + "env: 3;" + "dest: 3;" + "}" + "}" ); // Unsafe to move within WHILE block with BREAK assertUnsafeMoveVisibility( "var a;" + "function f() {" + "var l;" + "while (l) {" + "src: a++;" + "env: l;" + "break;" + "dest: 3;" + "}" + "}" ); // Unsafe to move within WHILE block with continue assertUnsafeMoveVisibility( "var a;" + "function f() {" + "var l;" + "while (l) {" + "src: a++;" + "env: 3;" + "continue;" + "dest: 3;" + "}" + "}" ); // Unsafe to move within WHILE block with continue assertUnsafeMoveVisibility( "var a;" + "function f() {" + "var l;" + "while (l) {" + "src: a++;" + "env: 3;" + "return;" + "dest: 3;" + "}" + "}" ); // Safe to move within DO assertSafeMoveVisibility( "var a;" + "function f() {" + "var l;" + "do {" + "src: a++;" + "env: 3;" + "dest: 3;" + "} while(l)" + "}" ); // Unsafe to move outside DO assertUnsafeMoveVisibility( "var a;" + "function f() {" + "var l;" + "do {" + "src: a++;" + "env: 3;" + "} while(l)" + "dest: 3;" + "}" ); // It should be safe to move within CASE // but we disallow for now because analyzing // CASE fall-through and BREAKs is complicated. assertUnsafeMoveVisibility( "var a;" + "function f() {" + "var l;" + "switch(l) {" + "case 17:" + "src: a++;" + "env: 3;" + "dest: 3;" + "break;" + "}" + "}" ); // Unsafe to move between CASEs assertUnsafeMoveVisibility( "var a;" + "function f() {" + "var l;" + "switch(l) {" + "case 17:" + "src: a++;" + "env: 3;" + "break;" + "case 18:" + "dest: 3;" + "break;" + "}" + "}" ); // Unsafe to move between FUNCTIONs assertUnsafeMoveVisibility( "var a;" + "function f() {" + "src: a++;" + "env: 3;" + "}" + "function g() {" + "dest: 3;" + "}" ); } private SideEffectsAnalysis.AbstractMotionEnvironment environment( Node ...nodes) { return new SideEffectsAnalysis.RawMotionEnvironment( ImmutableSet.copyOf(nodes)); } private void assertMove(LocationAbstractionMode abstraction, String src, boolean expected) { SideEffectsAnalysis analysis = compileAndRun(src, abstraction); Node sourceNode = findLabeledStatement("src"); Node environmentNode = findLabeledStatement("env"); Node destinationNode = findLabeledStatement("dest"); boolean result = analysis.safeToMoveBefore(sourceNode, environment(environmentNode), destinationNode); if (expected) { assertTrue(result); } else { assertFalse(result); } } private void assertSafeMoveDegenerate(String src) { assertMove(LocationAbstractionMode.DEGENERATE, src, true); } private void assertUnsafeMoveDegenerate(String src) { assertMove(LocationAbstractionMode.DEGENERATE, src, false); } private void assertSafeMoveVisibility(String src) { assertMove(LocationAbstractionMode.VISIBILITY_BASED, src, true); } private void assertUnsafeMoveVisibility(String src) { assertMove(LocationAbstractionMode.VISIBILITY_BASED, src, false); } private SideEffectsAnalysis compileAndRun(String js, LocationAbstractionMode locationAbstractionIdentifier) { currentLocationAbstractionIdentifier = locationAbstractionIdentifier; testSame(SHARED_EXTERNS, js, null); currentJsRoot = currentCompiler.jsRoot; return currentAnalysis; } // Shamelessly stolen from NameReferenceGraphConstructionTest private Node findLabeledStatement(String label) { LabeledStatementSearcher s = new LabeledStatementSearcher(label); new NodeTraversal(currentCompiler, s).traverse(currentCompiler.jsRoot); assertNotNull("Label " + label + " should be in the source code", s.found); return s.found; } /** * Quick traversal to find a given labeled statement in the AST. * * Given "foo", finds the statement a = x in * foo: a = x; */ private class LabeledStatementSearcher extends AbstractPostOrderCallback { Node found = null; final String target; LabeledStatementSearcher(String target) { this.target = target; } @Override public void visit(NodeTraversal t, Node n, Node parent) { if (n.isLabel() && target.equals(n.getFirstChild().getString())) { found = n.getLastChild(); } } } }