diff --git a/tlatools/src/tlc2/tool/Spec.java b/tlatools/src/tlc2/tool/Spec.java index 81bed024e392ba7acf726b38e29d5c56c2520e61..c671f792da93291e9356568b199d82a235e31645 100644 --- a/tlatools/src/tlc2/tool/Spec.java +++ b/tlatools/src/tlc2/tool/Spec.java @@ -1747,16 +1747,11 @@ public class Spec implements ValueConstants, ToolGlobals, Serializable return opNode; } - public final Object getVal(ExprOrOpArgNode expr, Context c, boolean cachable) + public final Object getVal(ExprOrOpArgNode expr, Context c, final boolean cachable) { if (expr instanceof ExprNode) { - LazyValue lval = new LazyValue(expr, c); - if (!cachable) - { - lval.setUncachable(); - } - return lval; + return new LazyValue(expr, c, cachable); } SymbolNode opNode = ((OpArgNode) expr).getOp(); return this.lookup(opNode, c, false); diff --git a/tlatools/src/tlc2/tool/Tool.java b/tlatools/src/tlc2/tool/Tool.java index b13813da914d5d1433cf8ad606b8391a100c3c79..f31392c0162c0f3129b042291d65d07c35f6cc3c 100644 --- a/tlatools/src/tlc2/tool/Tool.java +++ b/tlatools/src/tlc2/tool/Tool.java @@ -465,11 +465,11 @@ public class Tool if (val instanceof LazyValue) { LazyValue lv = (LazyValue)val; - if (lv.val == null || lv.val == ValUndef) { + if (lv.getValue() == null || lv.isUncachable()) { this.getInitStates(lv.expr, acts, lv.con, ps, states); return; } - val = lv.val; + val = lv.getValue(); } Object bval = val; @@ -875,10 +875,10 @@ public class Tool if (val instanceof LazyValue) { LazyValue lv = (LazyValue)val; - if (lv.val == null || lv.val == ValUndef) { + if (lv.getValue() == null || lv.isUncachable()) { return this.getNextStates(lv.expr, acts, lv.con, s0, s1, nss); } - val = lv.val; + val = lv.getValue(); } Object bval = val; @@ -1370,39 +1370,91 @@ public class Tool final LazyValue lv = (LazyValue) val; if (s1 == null) { val = this.eval(lv.expr, lv.con, s0, null, control); - } else if (lv.val == ValUndef || EvalControl.isEnabled(control)) { + } else if (lv.isUncachable() || EvalControl.isEnabled(control)) { // Never use cached LazyValues in an ENABLED expression. This is why all // this.enabled* methods pass EvalControl.Enabled (the only exclusion being the - // call on line line 2789 which passes EvalControl.Primed). This is why we can + // call on line line 2799 which passes EvalControl.Primed). This is why we can // be sure that ENALBED expressions are not affected by the caching bug tracked // in Github issue 113 (see below). val = this.eval(lv.expr, lv.con, s0, s1, control); } else { - val = lv.val; + val = lv.getValue(); if (val == null) { final Value res = this.eval(lv.expr, lv.con, s0, s1, control); // This check has been suggested by Yuan Yu on 01/15/2018: + // // If init-states are being generated, level has to be <= ConstantLevel for // caching/LazyValue to be allowed. If next-states are being generated, level - // has to be <= VariableLevel. This restriction is in place as a fix to - // Github issue 113 (https://github.com/tlaplus/tlaplus/issues/113) - + // has to be <= VariableLevel. The level indicates if the expression to be + // evaluated contains only constants, constants & variables, constants & + // variables and primed variables (thus action) or is a temporal formula. + // + // This restriction is in place to fix Github issue 113 + // (https://github.com/tlaplus/tlaplus/issues/113) - // TLC can generate invalid sets of init or next-states caused by broken // LazyValue evaluation. The related tests are AssignmentInit* and - // AssignmentNext*. Without this fix TLC essentially used a stale lv.val when it - // needed to re-evaluate res because the actual operands to eval changed. + // AssignmentNext*. Without this fix TLC essentially reuses a stale lv.val when + // it needs to re-evaluate res because the actual operands to eval changed. + // Below is Leslie's formal description of the bug: + // + // The possible initial values of some variable var are specified by a subformula + // + // F(..., var, ...) + // + // in the initial predicate, for some operator F such that expanding the + // definition of F results in a formula containing more than one occurrence of + // var , not all occurring in separate disjuncts of that formula. + // + // The possible next values of some variable var are specified by a subformula + // + // F(..., var', ...) + // + // in the next-state relation, for some operator F such that expanding the + // definition of F results in a formula containing more than one occurrence of + // var' , not all occurring in separate disjuncts of that formula. + // + // An example of the first case is an initial predicate Init defined as follows: + // + // VARIABLES x, ... + // F(var) == \/ var \in 0..99 /\ var % 2 = 0 + // \/ var = -1 + // Init == /\ F(x) + // /\ ... + // + // The error would not appear if F were defined by: + // + // F(var) == \/ var \in {i \in 0..99 : i % 2 = 0} + // \/ var = -1 + // + // or if the definition of F(x) were expanded in Init : + // + // Init == /\ \/ x \in 0..99 /\ x % 2 = 0 + // \/ x = -1 + // /\ ... + // + // A similar example holds for case 2 with the same operator F and the + // next-state formula + // + // Next == /\ F(x') + // /\ ... + // + // The workaround is to rewrite the initial predicate or next-state relation so + // it is not in the form that can cause the bug. The simplest way to do that is + // to expand (in-line) the definition of F in the definition of the initial + // predicate or next-state relation. // // Note that EvalControl.Init is only set in the scope of this.getInitStates*, // but not in the scope of methods such as this.isInModel, this.isGoodState... // which are invoked by DFIDChecker and ModelChecker#doInit and doNext. These - // invocation however don't pose a problem with regards to issue 113 though - // because they don't generate the set of initial or next states but get passed - // fully generated states. + // invocation however don't pose a problem with regards to issue 113 because + // they don't generate the set of initial or next states but get passed fully + // generated/final states. // // !EvalControl.isInit(control) means Tool is either processing the spec in // this.process* as part of initialization or that next-states are being // generated. The latter case has to restrict usage of cached LazyValue as // discussed above. - final int level = ((LevelNode) lv.expr).getLevel(); + final int level = ((LevelNode) lv.expr).getLevel(); // cast to LevelNode is safe because LV only subclass of SN. if ((EvalControl.isInit(control) && level <= LevelConstants.ConstantLevel) || (!EvalControl.isInit(control) && level <= LevelConstants.VariableLevel)) { // The performance benefits of caching values is generally debatable. The time @@ -1412,7 +1464,7 @@ public class Tool // for issue 113 reduces the cache hits from ~13 billion to ~4 billion. This was // measured with an instrumented version of TLC. // [1] general/performance/PaxosCommit/ - lv.val = res; + lv.setValue(res); } val = res; } @@ -2796,17 +2848,33 @@ public class Tool } final Value v0 = this.eval(expr, c, s0, TLCState.Empty, EvalControl.Enabled); - // We are in ENABLED and primed but why pass only primed? This appears to - // be the only place where we call eval from the ENABLED scope without passing - // EvalControl.Enabled. Thus a cached LazyValue could be used. - // + // We are in ENABLED and primed but why pass only primed? This appears to + // be the only place where we call eval from the ENABLED scope without + // additionally passing EvalControl.Enabled. Not passing Enabled allows a + // cached LazyValue could be used (see comments above on line 1384). + // // The current scope is a nested UNCHANGED in an ENABLED and evaluation is set - // to primed. - // - // However, SANY does not seem to accept a primed variable inside ENABLED - // UNCHANGED .... anyway, thus this.eval(...) is going to throw an EvalException - // regardless of EvalControl.Primed or - // EvalControl.setPrimed(EvalControl.Enabled). + // to primed. However, UNCHANGED e equals e' = e , so anything primed in e + // becomes double-primed in ENABLED UNCHANGED e. This makes it illegal TLA+ + // which is rejected by SANY's level checking. A perfectly valid spec - where + // e is not primed - but that also causes this code path to be taken is 23 below: + // + // -------- MODULE 23 --------- + // VARIABLE t + // op(var) == var + // Next == /\ (ENABLED (UNCHANGED op(t))) + // /\ (t'= t) + // Spec == (t = 0) /\ [][Next]_t + // ============================ + // + // However, spec 23 causes the call to this.eval(...) below to throw an + // EvalException either with EvalControl.Primed. The exception's message is + // "In evaluation, the identifier t is either undefined or not an operator." + // indicating that this code path is buggy. + // + // If this bug is ever fixed to make TLC accept spec 23, EvalControl.Primed + // should likely be rewritten to EvalControl.setPrimed(EvalControl.Enabled) + // to disable reusage of LazyValues on line ~1384 above. final Value v1 = this.eval(expr, c, s1, TLCState.Empty, EvalControl.Primed); if (!v0.equals(v1)) { return null; diff --git a/tlatools/src/tlc2/value/LazyValue.java b/tlatools/src/tlc2/value/LazyValue.java index 35ad31d24097c48f54843f6a0c2a16f65ec9e584..8fe604d9606693dfec1b9816413b9f18e04f9a90 100644 --- a/tlatools/src/tlc2/value/LazyValue.java +++ b/tlatools/src/tlc2/value/LazyValue.java @@ -10,8 +10,8 @@ import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; -import tlc2.tool.FingerprintException; import tla2sany.semantic.SemanticNode; +import tlc2.tool.FingerprintException; import tlc2.util.Context; import util.Assert; import util.ToolIO; @@ -44,19 +44,36 @@ public class LazyValue extends Value { public SemanticNode expr; public Context con; - public Value val; + private Value val; public LazyValue(SemanticNode expr, Context con) { + this(expr, con, true); + } + + public LazyValue(SemanticNode expr, Context con, final boolean cachable) { this.expr = expr; this.con = con; this.val = null; - if (LAZYEVAL_OFF) { - setUncachable(); + // See comment on cachable's meager performance in Tool.java on line 1408. + // See other note about a bug that surfaced with LazyValue in Tool.java on line ~1385. + if (LAZYEVAL_OFF || !cachable) { + this.val = ValUndef; } } - public final void setUncachable() { this.val = ValUndef; } + public final boolean isUncachable() { return this.val == ValUndef; } + public final void setValue(final Value aValue) { + assert !isUncachable(); + this.val = aValue; + } + + public final Value getValue() { + // cache hit on (this.val != null && !isUncachable) + // cache miss on (this.val == null) + return this.val; + } + public final byte getKind() { return LAZYVALUE; } public final int compareTo(Object obj) {