Skip to content

Commit 57cb431

Browse files
concavelenzbrad4d
authored andcommitted
Per-scope change detection validation in the unit tests is broken (the older
all or nothing validation works correctly). Several issues: * There was a logic mismatch with regard to which Nodes should be checked and SCRIPT nodes were being ignored. * The validation was only run if the PhaseOptimizer set but CompilerTestCase does use PhaseOptimizer by default (some all but a few don't) * The validation was comparing the state before the setup passes ran (es6 transpilation, normalization, etc) so a unit test might pass even if it didn't mark a pass as changed. * The validate logic, in unit tests, was not checking scopes not marked as changed but only validated that marked nodes had been changed (the latter is less likely to be incorrect). * The validation logic was ignoring Node annotations when comparing node for changes which causes some false negative results. In addition to the validation logic fixes, this simplifies the Compiler/PhaseOptimizer interaction with regard to change tracking and makes way for future changes. The validation is now turned off by default. The next change will turn it on by default with appropriate fixes. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=152757595
1 parent d1392ed commit 57cb431

File tree

6 files changed

+138
-112
lines changed

6 files changed

+138
-112
lines changed

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

+6
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,12 @@ LifeCycleStage getLifeCycleStage() {
265265
/** Let the PhaseOptimizer know which scope a pass is currently analyzing */
266266
abstract void setScope(Node n);
267267

268+
/** A monotonically increasing value to identify a change */
269+
abstract int getChangeStamp();
270+
271+
/** Called to indicate that the current change stamp has been used */
272+
abstract void incrementChangeStamp();
273+
268274
/** Returns the root of the source tree, ignoring externs */
269275
abstract Node getJsRoot();
270276

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

+66-25
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,19 @@ public SourceFile apply(String filename) {
262262

263263
private static final Joiner pathJoiner = Joiner.on(File.separator);
264264

265+
// TODO(johnlenz): remove "currentScope".
266+
// Used as a shortcut for change tracking. This is the current scope being
267+
// visited by the "current" NodeTraversal. This can't be thread safe so
268+
// we should move it into the NodeTraversal and require explicit changed
269+
// nodes elsewhere so we aren't blocked from doing this elsewhere.
270+
private Node currentScope = null;
271+
272+
// Starts at 0, increases as "interesting" things happen.
273+
// Nothing happens at time START_TIME, the first pass starts at time 1.
274+
// The correctness of scope-change tracking relies on Node/getIntProp
275+
// returning 0 if the custom attribute on a node hasn't been set.
276+
private int changeStamp = 1;
277+
265278
/**
266279
* Creates a Compiler that reports errors and warnings to its logger.
267280
*/
@@ -2293,13 +2306,6 @@ void removeChangeHandler(CodeChangeHandler handler) {
22932306
codeChangeHandlers.remove(handler);
22942307
}
22952308

2296-
@Override
2297-
void setScope(Node n) {
2298-
if (phaseOptimizer != null) {
2299-
phaseOptimizer.setScope(n);
2300-
}
2301-
}
2302-
23032309
Node getExternsRoot() {
23042310
return externsRoot;
23052311
}
@@ -2309,6 +2315,47 @@ Node getJsRoot() {
23092315
return jsRoot;
23102316
}
23112317

2318+
/**
2319+
* Some tests don't want to call the compiler "wholesale," they may not want
2320+
* to call check and/or optimize. With this method, tests can execute custom
2321+
* optimization loops.
2322+
*/
2323+
@VisibleForTesting
2324+
void setPhaseOptimizer(PhaseOptimizer po) {
2325+
this.phaseOptimizer = po;
2326+
}
2327+
2328+
@Override
2329+
public int getChangeStamp() {
2330+
return changeStamp;
2331+
}
2332+
2333+
@Override
2334+
public void incrementChangeStamp() {
2335+
changeStamp++;
2336+
}
2337+
2338+
@Override
2339+
void setScope(Node n) {
2340+
currentScope = (n.isFunction() || n.isScript()) ? n : getEnclosingChangeScope(n);
2341+
}
2342+
2343+
private Node getEnclosingChangeScope(Node n) {
2344+
while (n.getParent() != null) {
2345+
n = n.getParent();
2346+
if (n.isFunction() || n.isScript()) {
2347+
return n;
2348+
}
2349+
}
2350+
return n;
2351+
}
2352+
2353+
private void recordChange(Node n) {
2354+
n.setChangeTime(changeStamp);
2355+
// Every code change happens at a different time
2356+
changeStamp++;
2357+
}
2358+
23122359
@Override
23132360
boolean hasScopeChanged(Node n) {
23142361
if (phaseOptimizer == null) {
@@ -2318,29 +2365,23 @@ boolean hasScopeChanged(Node n) {
23182365
}
23192366

23202367
@Override
2321-
void reportChangeToEnclosingScope(Node n) {
2322-
if (phaseOptimizer != null) {
2323-
phaseOptimizer.reportChangeToEnclosingScope(n);
2324-
phaseOptimizer.startCrossScopeReporting();
2325-
reportCodeChange();
2326-
phaseOptimizer.endCrossScopeReporting();
2327-
} else {
2328-
reportCodeChange();
2368+
public void reportCodeChange() {
2369+
// TODO(johnlenz): if this is called with a null scope we need to invalidate everything
2370+
// but this isn't done, so we need to make this illegal or record this as having
2371+
// invalidated everything.
2372+
if (currentScope != null) {
2373+
recordChange(currentScope);
2374+
notifyChangeHandlers();
23292375
}
23302376
}
23312377

2332-
/**
2333-
* Some tests don't want to call the compiler "wholesale," they may not want
2334-
* to call check and/or optimize. With this method, tests can execute custom
2335-
* optimization loops.
2336-
*/
2337-
@VisibleForTesting
2338-
void setPhaseOptimizer(PhaseOptimizer po) {
2339-
this.phaseOptimizer = po;
2378+
@Override
2379+
void reportChangeToEnclosingScope(Node n) {
2380+
recordChange(getEnclosingChangeScope(n));
2381+
notifyChangeHandlers();
23402382
}
23412383

2342-
@Override
2343-
public void reportCodeChange() {
2384+
private void notifyChangeHandlers() {
23442385
for (CodeChangeHandler handler : codeChangeHandlers) {
23452386
handler.reportChange();
23462387
}

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

+16-10
Original file line numberDiff line numberDiff line change
@@ -4720,7 +4720,8 @@ public static Map<Node, Node> mapMainToClone(Node main, Node clone) {
47204720
}
47214721

47224722
private static void mtocHelper(Map<Node, Node> map, Node main, Node clone) {
4723-
if (main.isFunction()) {
4723+
// TODO(johnlenz): determine if MODULE_BODY is useful here.
4724+
if (main.isFunction() || main.isScript()) {
47244725
map.put(main, clone);
47254726
}
47264727
Node mchild = main.getFirstChild();
@@ -4733,8 +4734,10 @@ private static void mtocHelper(Map<Node, Node> map, Node main, Node clone) {
47334734
}
47344735

47354736
/** Checks that the scope roots marked as changed have indeed changed */
4736-
public static void verifyScopeChanges(Map<Node, Node> map, Node main,
4737-
boolean verifyUnchangedNodes) {
4737+
public static void verifyScopeChanges(
4738+
String passName, Map<Node, Node> map, Node main) {
4739+
final String passNameMsg = passName.isEmpty() ? "" : passName + ": ";
4740+
47384741
// compiler is passed only to call compiler.toSource during debugging to see
47394742
// mismatches in scopes
47404743

@@ -4744,25 +4747,28 @@ public static void verifyScopeChanges(Map<Node, Node> map, Node main,
47444747
// If verifyUnchangedNodes is true, we are comparing the ASTs before & after
47454748
// a pass. Check all scope roots.
47464749
final Map<Node, Node> mtoc = map;
4747-
final boolean checkUnchanged = verifyUnchangedNodes;
47484750
Node clone = mtoc.get(main);
47494751
if (main.getChangeTime() > clone.getChangeTime()) {
47504752
Preconditions.checkState(!isEquivalentToExcludingFunctions(main, clone));
4751-
} else if (checkUnchanged) {
4753+
} else {
47524754
Preconditions.checkState(isEquivalentToExcludingFunctions(main, clone));
47534755
}
47544756
visitPreOrder(main,
47554757
new Visitor() {
47564758
@Override
47574759
public void visit(Node n) {
4758-
if (n.isFunction() && mtoc.containsKey(n)) {
4760+
if ((n.isScript() || n.isFunction()) && mtoc.containsKey(n)) {
47594761
Node clone = mtoc.get(n);
47604762
if (n.getChangeTime() > clone.getChangeTime()) {
47614763
Preconditions.checkState(
4762-
!isEquivalentToExcludingFunctions(n, clone));
4763-
} else if (checkUnchanged) {
4764+
!isEquivalentToExcludingFunctions(n, clone),
4765+
"%sunchanged scope marked as changed",
4766+
passNameMsg);
4767+
} else {
47644768
Preconditions.checkState(
4765-
isEquivalentToExcludingFunctions(n, clone));
4769+
isEquivalentToExcludingFunctions(n, clone),
4770+
"%schange scope not marked as changed",
4771+
passNameMsg);
47664772
}
47674773
}
47684774
}
@@ -4803,7 +4809,7 @@ private static boolean isEquivalentToExcludingFunctions(
48034809
if (thisNode == null || thatNode == null) {
48044810
return thisNode == null && thatNode == null;
48054811
}
4806-
if (!thisNode.isEquivalentToShallow(thatNode)) {
4812+
if (!thisNode.isEquivalentWithSideEffectsToShallow(thatNode)) {
48074813
return false;
48084814
}
48094815
if (thisNode.getChildCount() != thatNode.getChildCount()) {

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

+19-70
Original file line numberDiff line numberDiff line change
@@ -56,19 +56,11 @@ class PhaseOptimizer implements CompilerPass {
5656
private NamedPass currentPass;
5757
// For each pass, remember the time at the end of the pass's last run.
5858
private Map<NamedPass, Integer> lastRuns;
59-
private Node currentScope;
60-
// Starts at 0, increases as "interesting" things happen.
61-
// Nothing happens at time START_TIME, the first pass starts at time 1.
62-
// The correctness of scope-change tracking relies on Node/getIntProp
63-
// returning 0 if the custom attribute on a node hasn't been set.
64-
private int timestamp;
6559
// The time of the last change made to the program by any pass.
6660
private int lastChange;
6761
private static final int START_TIME = 0;
6862
private final Node jsRoot;
69-
// Compiler/reportChangeToScope must call reportCodeChange to update all
70-
// change handlers. This flag prevents double update in ScopedChangeHandler.
71-
private boolean crossScopeReporting;
63+
7264
private final boolean useSizeHeuristicToStopOptimizationLoop;
7365

7466
// Used for sanity checks between loopable passes
@@ -130,8 +122,7 @@ enum State {
130122
this.passes = new ArrayList<>();
131123
this.progressRange = range;
132124
this.inLoop = false;
133-
this.crossScopeReporting = false;
134-
this.timestamp = this.lastChange = START_TIME;
125+
this.lastChange = START_TIME;
135126
this.useSizeHeuristicToStopOptimizationLoop =
136127
comp.getOptions().useSizeHeuristicToStopOptimizationLoop;
137128
}
@@ -198,6 +189,7 @@ void setSanityCheck(PassFactory sanityCheck) {
198189
}
199190

200191
private void setSanityCheckState() {
192+
// TODO(johnlenz): change this to always validate. See b/37164291
201193
if (inLoop) {
202194
lastAst = jsRoot.cloneTree();
203195
mtoc = NodeUtil.mapMainToClone(jsRoot, lastAst);
@@ -242,8 +234,8 @@ public void process(Node externs, Node root) {
242234

243235
private void maybePrintAstHashcodes(String passName, Node root) {
244236
if (printAstHashcodes) {
245-
String hashCodeMsg = "AST hashCode after " + passName + ": " +
246-
compiler.toSource(root).hashCode();
237+
String hashCodeMsg = "AST hashCode after " + passName + ": "
238+
+ compiler.toSource(root).hashCode();
247239
System.err.println(hashCodeMsg);
248240
compiler.addToDebugLog(hashCodeMsg);
249241
}
@@ -252,7 +244,7 @@ private void maybePrintAstHashcodes(String passName, Node root) {
252244
/**
253245
* Runs the sanity check if it is available.
254246
*/
255-
private void maybeSanityCheck(Node externs, Node root) {
247+
private void maybeSanityCheck(String passName, Node externs, Node root) {
256248
if (sanityCheck != null) {
257249
sanityCheck.create(compiler).process(externs, root);
258250
// The cross-module passes are loopable and ran together, but do not
@@ -261,7 +253,7 @@ private void maybeSanityCheck(Node externs, Node root) {
261253
if (inLoop
262254
&& !currentPass.name.equals(Compiler.CROSS_MODULE_CODE_MOTION_NAME)
263255
&& !currentPass.name.equals(Compiler.CROSS_MODULE_METHOD_MOTION_NAME)) {
264-
NodeUtil.verifyScopeChanges(mtoc, jsRoot, true);
256+
NodeUtil.verifyScopeChanges(passName, mtoc, jsRoot);
265257
}
266258
}
267259
}
@@ -320,7 +312,7 @@ public void process(Node externs, Node root) {
320312
tracker.recordPassStop(name, traceRuntime);
321313
}
322314
maybePrintAstHashcodes(name, root);
323-
maybeSanityCheck(externs, root);
315+
maybeSanityCheck(name, externs, root);
324316
} catch (IllegalStateException e) {
325317
// TODO(johnlenz): Remove this once the normalization checks report
326318
// errors instead of exceptions.
@@ -334,14 +326,6 @@ public String toString() {
334326
}
335327
}
336328

337-
void setScope(Node n) {
338-
// NodeTraversal causes setScope calls outside loops; ignore them.
339-
if (inLoop) {
340-
// Find the top-level node in the scope.
341-
currentScope = n.isFunction() ? n : getEnclosingScope(n);
342-
}
343-
}
344-
345329
boolean hasScopeChanged(Node n) {
346330
// Outside loops we don't track changed scopes, so we visit them all.
347331
if (!inLoop) {
@@ -353,65 +337,26 @@ boolean hasScopeChanged(Node n) {
353337
|| n.getChangeTime() > timeOfLastRun;
354338
}
355339

356-
private Node getEnclosingScope(Node n) {
357-
while (n.getParent() != null) {
358-
n = n.getParent();
359-
if (n.isFunction() || n.isScript()) {
360-
return n;
361-
}
362-
}
363-
return n;
364-
}
365-
366-
void reportChangeToEnclosingScope(Node n) {
367-
lastChange = timestamp;
368-
getEnclosingScope(n).setChangeTime(timestamp);
369-
// Every code change happens at a different time
370-
timestamp++;
371-
}
372-
373-
/**
374-
* Records that the currently-running pass may report cross-scope changes.
375-
* When this happens, we don't want to falsely report the current scope as
376-
* changed when reportChangeToScope is called from Compiler.
377-
*/
378-
void startCrossScopeReporting() {
379-
crossScopeReporting = true;
380-
}
381-
382-
/** The currently-running pass won't report cross-scope changes. */
383-
void endCrossScopeReporting() {
384-
crossScopeReporting = false;
385-
}
386-
387340
/**
388341
* A change handler that marks scopes as changed when reportChange is called.
389342
*/
390343
private class ScopedChangeHandler implements CodeChangeHandler {
391344
private int lastCodeChangeQuery;
392345

393346
ScopedChangeHandler() {
394-
this.lastCodeChangeQuery = timestamp;
347+
this.lastCodeChangeQuery = compiler.getChangeStamp();
395348
}
396349

397350
@Override
398351
public void reportChange() {
399-
if (crossScopeReporting) {
400-
// This call was caused by Compiler/reportChangeToEnclosingScope,
401-
// do nothing.
402-
return;
403-
}
404-
lastChange = timestamp;
405-
currentScope.setChangeTime(timestamp);
406-
// Every code change happens at a different time
407-
timestamp++;
352+
lastChange = compiler.getChangeStamp();
408353
}
409354

410355
private boolean hasCodeChangedSinceLastCall() {
411356
boolean result = lastChange > lastCodeChangeQuery;
412-
lastCodeChangeQuery = timestamp;
357+
lastCodeChangeQuery = compiler.getChangeStamp();
413358
// The next call to the method will happen at a different time
414-
timestamp++;
359+
compiler.incrementChangeStamp();
415360
return result;
416361
}
417362
}
@@ -449,7 +394,11 @@ public void process(Node externs, Node root) {
449394
// Set up function-change tracking
450395
scopeHandler = new ScopedChangeHandler();
451396
compiler.addChangeHandler(scopeHandler);
452-
setScope(root);
397+
398+
// TODO(johnlenz): It is unclear why "setScope" is called here. Try to remove
399+
// this.
400+
compiler.setScope(root);
401+
453402
// lastRuns is initialized before each loop. This way, when a pass is run
454403
// in the 2nd loop for the 1st time, it looks at all scopes.
455404
lastRuns = new HashMap<>();
@@ -487,11 +436,11 @@ public void process(Node externs, Node root) {
487436
&& !runInPrevIter.contains(pass))
488437
|| (state == State.RUN_PASSES_THAT_CHANGED_STH_IN_PREV_ITER
489438
&& madeChanges.contains(pass))) {
490-
timestamp++;
439+
compiler.incrementChangeStamp();
491440
currentPass = pass;
492441
pass.process(externs, root);
493442
runInPrevIter.add(pass);
494-
lastRuns.put(pass, timestamp);
443+
lastRuns.put(pass, compiler.getChangeStamp());
495444
if (hasHaltingErrors()) {
496445
return;
497446
} else if (scopeHandler.hasCodeChangedSinceLastCall()) {

0 commit comments

Comments
 (0)