diff --git a/liquidjava-verifier/pom.xml b/liquidjava-verifier/pom.xml index 9c16fa958..84891bc2b 100644 --- a/liquidjava-verifier/pom.xml +++ b/liquidjava-verifier/pom.xml @@ -89,6 +89,10 @@ 3.2.1 true + + + check + diff --git a/liquidjava-verifier/src/main/java/liquidjava/processor/refinement_checker/TypeChecker.java b/liquidjava-verifier/src/main/java/liquidjava/processor/refinement_checker/TypeChecker.java index 5cf795f90..fb9efd2ec 100644 --- a/liquidjava-verifier/src/main/java/liquidjava/processor/refinement_checker/TypeChecker.java +++ b/liquidjava-verifier/src/main/java/liquidjava/processor/refinement_checker/TypeChecker.java @@ -415,13 +415,19 @@ public boolean checkStateSMT(Predicate prevState, Predicate expectedState, Sourc public boolean checkStateSMT(Predicate prevState, Predicate expectedState, SourcePosition p, boolean silent) throws LJError { - return verifyStateSMT(prevState, expectedState, p, silent).isOk(); + return verifyStateSMT(prevState, expectedState, p, silent, false).isOk(); } /** Checks that {@code prevState} implies {@code expectedState}, with a counterexample when it does not */ public SMTResult verifyStateSMT(Predicate prevState, Predicate expectedState, SourcePosition p, boolean silent) throws LJError { - return vcChecker.verifySMTSubtypeStates(prevState, expectedState, context.getGhostStates(), p, factory, silent); + return verifyStateSMT(prevState, expectedState, p, silent, true); + } + + private SMTResult verifyStateSMT(Predicate prevState, Predicate expectedState, SourcePosition p, boolean silent, + boolean needsCounterexample) throws LJError { + return vcChecker.verifySMTSubtypeStates(prevState, expectedState, context.getGhostStates(), p, factory, silent, + needsCounterexample); } public void throwRefinementError(SourcePosition position, SourcePosition declarationPosition, diff --git a/liquidjava-verifier/src/main/java/liquidjava/processor/refinement_checker/VCChecker.java b/liquidjava-verifier/src/main/java/liquidjava/processor/refinement_checker/VCChecker.java index 0c2f4b9d8..819e4b6a2 100644 --- a/liquidjava-verifier/src/main/java/liquidjava/processor/refinement_checker/VCChecker.java +++ b/liquidjava-verifier/src/main/java/liquidjava/processor/refinement_checker/VCChecker.java @@ -71,7 +71,7 @@ public void processSubtyping(Predicate expectedType, List list, CtEl DebugLog.smtStart(impl, expected); SMTResult result; try { - result = dischargeToSMT(expected, premises, annotationValuePos, true); + result = dischargeToSMT(expected, premises, annotationValuePos, true, true); } catch (SMTUnknownError error) { error.setDeclarationPosition(declarationPosition); DebugLog.smtError(error.getMessage()); @@ -123,13 +123,13 @@ public void processSubtyping(Predicate type, Predicate expectedType, List states, SourcePosition position, Factory factory, boolean silent) throws LJError { + return verifySMTSubtypeStates(type, expectedType, states, position, factory, silent, true); + } + + /** + * @param needsCounterexample + * whether the caller uses the counterexample when the verification fails + */ + public SMTResult verifySMTSubtypeStates(Predicate type, Predicate expectedType, List states, + SourcePosition position, Factory factory, boolean silent, boolean needsCounterexample) throws LJError { if (!silent) { DebugLog.smtVerifying(position); } @@ -185,7 +194,7 @@ public SMTResult verifySMTSubtypeStates(Predicate type, Predicate expectedType, SMTResult result; try { - result = dischargeToSMT(expected, premises, position, true); + result = dischargeToSMT(expected, premises, position, true, needsCounterexample); } catch (RuntimeException ex) { if (!silent) { DebugLog.smtError(ex.getMessage()); diff --git a/liquidjava-verifier/src/main/java/liquidjava/rj_language/opt/VCConstraintSimplification.java b/liquidjava-verifier/src/main/java/liquidjava/rj_language/opt/VCConstraintSimplification.java index 0050f6611..a7376348d 100644 --- a/liquidjava-verifier/src/main/java/liquidjava/rj_language/opt/VCConstraintSimplification.java +++ b/liquidjava-verifier/src/main/java/liquidjava/rj_language/opt/VCConstraintSimplification.java @@ -71,7 +71,7 @@ private VCImplication remove(VCImplication implication, VCImplication removed) { */ private boolean implies(Predicate stronger, Predicate weaker) { try { - SMTResult result = new SMTEvaluator().verifySubtype(stronger, weaker, Context.getInstance(), true); + SMTResult result = new SMTEvaluator().verifySubtype(stronger, weaker, Context.getInstance(), true, false); return result.isOk(); } catch (Exception e) { return false; diff --git a/liquidjava-verifier/src/main/java/liquidjava/smt/QueryCache.java b/liquidjava-verifier/src/main/java/liquidjava/smt/QueryCache.java new file mode 100644 index 000000000..46a35476b --- /dev/null +++ b/liquidjava-verifier/src/main/java/liquidjava/smt/QueryCache.java @@ -0,0 +1,245 @@ +package liquidjava.smt; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import liquidjava.processor.context.Context; +import liquidjava.processor.context.GhostFunction; +import liquidjava.processor.context.RefinedVariable; +import liquidjava.rj_language.ast.AliasInvocation; +import liquidjava.rj_language.ast.BinaryExpression; +import liquidjava.rj_language.ast.Enum; +import liquidjava.rj_language.ast.Expression; +import liquidjava.rj_language.ast.FunctionInvocation; +import liquidjava.rj_language.ast.Ite; +import liquidjava.rj_language.ast.LiteralBoolean; +import liquidjava.rj_language.ast.LiteralChar; +import liquidjava.rj_language.ast.LiteralInt; +import liquidjava.rj_language.ast.LiteralLong; +import liquidjava.rj_language.ast.LiteralReal; +import liquidjava.rj_language.ast.LiteralString; +import liquidjava.rj_language.ast.UnaryExpression; +import liquidjava.rj_language.ast.Var; +import liquidjava.rj_language.visitors.ExpressionVisitor; +import spoon.reflect.declaration.CtEnum; +import spoon.reflect.reference.CtTypeReference; + +/** + * Results of solver queries, kept across verification runs. A long-lived process such as the language server verifies + * the whole program again after every save, and asks the solver mostly the same queries each time. + *

+ * A query is identified by everything {@link TranslatorToZ3} reads to build it: the expression, node by node, the type + * and supertypes of each of its variables, and the enums, ghosts and ghost states of the context. Variables are renamed + * in order of appearance, so that a query whose variables were numbered differently by an earlier edit is still found. + * Names that denote enum constants are values, not variables, and keep their name. A counterexample names the + * variables, so a cached one is only reused when the names are the same; otherwise a caller that needs one asks the + * solver again. Unknown results and errors are never kept. Results only live as long as the process, so the verifier + * and the solver cannot change under them. + */ +final class QueryCache { + + private static final int MAX_RESULTS = 50_000; + + private static final Map RESULTS = Collections.synchronizedMap(new LinkedHashMap<>(16, 0.75f, true) { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > MAX_RESULTS; + } + }); + + /** + * @param text + * the query with its variables renamed + * @param names + * the variables of the query, in the order they were renamed + */ + record Key(String text, List names) { + } + + /** A result, whose counterexample names the variables in {@code names} */ + private record Entry(SMTResult result, List names) { + } + + private QueryCache() { + } + + static Key key(Expression expression, Context context) { + Map> types = context.getContext(); + List occurrences = new ArrayList<>(); + expression.getVariableNames(occurrences); + Set constants = enumConstants(types.values()); + List names = occurrences.stream().distinct().filter(n -> types.containsKey(n) && !constants.contains(n)) + .toList(); + + StringBuilder text = new StringBuilder(expression.accept(new Canonical(names))).append('\n'); + Map> supertypes = new LinkedHashMap<>(); + for (RefinedVariable variable : context.getAllVariablesWithSupertypes()) + if (names.contains(variable.getName())) + supertypes.computeIfAbsent(variable.getName(), k -> new ArrayList<>()).add( + variable.getSuperTypes().stream().map(QueryCache::describe).collect(Collectors.joining(", "))); + for (int i = 0; i < names.size(); i++) { + String name = names.get(i); + text.append('@').append(i).append(": ").append(describe(types.get(name))); + supertypes.getOrDefault(name, List.of()).forEach(s -> text.append(" <: ").append(s)); + text.append('\n'); + } + // every enum of the context, as their constants can be named in the query + types.values().stream().filter(CtTypeReference::isEnum).map(QueryCache::describe).distinct() + .forEach(e -> text.append("enum ").append(e).append('\n')); + for (GhostFunction ghost : context.getGhosts()) + text.append("ghost ").append(describe(ghost)).append('\n'); + for (GhostFunction state : context.getGhostStates()) + text.append("state ").append(describe(state)).append('\n'); + return new Key(text.toString(), names); + } + + /** + * The result kept for {@code key}, or nothing if there is none or it cannot be used: a counterexample is only + * reused when the variables have the same names + */ + static Optional get(Key key, boolean needsCounterexample) { + Entry entry = RESULTS.get(key.text()); + if (entry == null) + return Optional.empty(); + if (entry.result().isOk() || entry.names().equals(key.names())) + return Optional.of(entry.result()); + return needsCounterexample ? Optional.empty() : Optional.of(SMTResult.error(new Counterexample(List.of()))); + } + + static void put(Key key, SMTResult result) { + RESULTS.put(key.text(), new Entry(result, key.names())); + } + + /** + * Fails if {@code recomputed} differs from {@code cached}, with an {@link AssertionError} so that code that + * recovers from exceptions cannot hide it + */ + static void checkSame(Key key, SMTResult cached, SMTResult recomputed) { + boolean same = cached.isOk() == recomputed.isOk() && (cached.isOk() || cached.getCounterexample().isEmpty() + || cached.getCounterexample().equals(recomputed.getCounterexample())); + if (!same) + throw new AssertionError( + String.format("The solver query cache returned %s but recomputing gives %s for%n%s", + describe(cached), describe(recomputed), key.text())); + } + + /** The names the translator binds to the constants of the enums, which denote values rather than variables */ + private static Set enumConstants(Collection> types) { + Set constants = new HashSet<>(); + for (CtTypeReference type : types) + if (type.isEnum() && type.getDeclaration()instanceof CtEnum declaration) + declaration.getEnumValues().forEach( + v -> constants.add(TranslatorContextToZ3.enumConstantName(declaration, v.getSimpleName()))); + return constants; + } + + private static String describe(SMTResult result) { + return result.isOk() ? "ok" : "error with " + result.getCounterexample(); + } + + /** What the translator reads from a type: its names, and the constants of an enum */ + private static String describe(CtTypeReference type) { + if (type == null) + return "?"; + if (type.isEnum() && type.getDeclaration()instanceof CtEnum declaration) + return type.getQualifiedName() + declaration.getEnumValues().stream().map(v -> v.getSimpleName()).toList(); + return type.getQualifiedName() + " " + type; + } + + private static String describe(GhostFunction ghost) { + return ghost.getQualifiedName() + ghost.getParametersTypes().stream().map(Object::toString).toList() + " " + + ghost.getReturnType(); + } + + /** The expression node by node, with its variables renamed in order of appearance */ + private static final class Canonical implements ExpressionVisitor { + + private final List names; + + Canonical(List names) { + this.names = names; + } + + private String all(List expressions) { + return expressions.stream().map(e -> e.accept(this)).collect(Collectors.joining(" ")); + } + + @Override + public String visitAliasInvocation(AliasInvocation alias) { + return "(alias " + alias.getName() + " " + all(alias.getArgs()) + ")"; + } + + @Override + public String visitBinaryExpression(BinaryExpression exp) { + return "(" + exp.getOperator() + " " + exp.getFirstOperand().accept(this) + " " + + exp.getSecondOperand().accept(this) + ")"; + } + + @Override + public String visitFunctionInvocation(FunctionInvocation fun) { + return "(call " + fun.getName() + " " + all(fun.getArgs()) + ")"; + } + + @Override + public String visitIte(Ite ite) { + return "(ite " + ite.getCondition().accept(this) + " " + ite.getThen().accept(this) + " " + + ite.getElse().accept(this) + ")"; + } + + @Override + public String visitLiteralInt(LiteralInt lit) { + return "(int " + lit.getValue() + ")"; + } + + @Override + public String visitLiteralLong(LiteralLong lit) { + return "(long " + lit.getValue() + ")"; + } + + @Override + public String visitLiteralBoolean(LiteralBoolean lit) { + return "(bool " + lit.isBooleanTrue() + ")"; + } + + @Override + public String visitLiteralChar(LiteralChar lit) { + return "(char " + (int) lit.getValue() + ")"; + } + + @Override + public String visitLiteralReal(LiteralReal lit) { + return "(real " + Double.doubleToLongBits(lit.getValue()) + ")"; + } + + @Override + public String visitLiteralString(LiteralString lit) { + return "(string " + lit.toString().length() + ":" + lit + ")"; + } + + @Override + public String visitUnaryExpression(UnaryExpression exp) { + return "(" + exp.getOp() + " " + exp.getExpression().accept(this) + ")"; + } + + @Override + public String visitEnum(Enum en) { + Expression literal = en.getResolvedLiteral(); + return "(const " + en.getTypeName() + " " + en.getConstName() + " " + + (literal == null ? "-" : literal.accept(this)) + ")"; + } + + @Override + public String visitVar(Var var) { + int index = names.indexOf(var.getName()); + return index < 0 ? "(free " + var.getName().length() + ":" + var.getName() + ")" : "@" + index; + } + } +} diff --git a/liquidjava-verifier/src/main/java/liquidjava/smt/SMTEvaluator.java b/liquidjava-verifier/src/main/java/liquidjava/smt/SMTEvaluator.java index 30f8ac242..6fc196326 100644 --- a/liquidjava-verifier/src/main/java/liquidjava/smt/SMTEvaluator.java +++ b/liquidjava-verifier/src/main/java/liquidjava/smt/SMTEvaluator.java @@ -1,17 +1,18 @@ package liquidjava.smt; -import com.martiansoftware.jsap.SyntaxException; import com.microsoft.z3.Expr; -import com.microsoft.z3.Model; import com.microsoft.z3.Solver; import com.microsoft.z3.Status; import com.microsoft.z3.Z3Exception; +import java.util.Optional; + import liquidjava.diagnostics.DebugLog; import liquidjava.diagnostics.errors.SMTUnknownError; import liquidjava.processor.context.Context; import liquidjava.rj_language.Predicate; import liquidjava.rj_language.ast.Expression; +import liquidjava.utils.CacheMode; public class SMTEvaluator { @@ -31,64 +32,91 @@ public SMTResult verifySubtype(Predicate subRef, Predicate supRef, Context conte public SMTResult verifySubtype(Predicate subRef, Predicate supRef, Context context, boolean silent) throws Exception { + return verifySubtype(subRef, supRef, context, silent, true); + } + + /** + * @param needsCounterexample + * whether the caller uses the counterexample of a failing check, which a result kept from an earlier run + * only has when the variables have the same names + */ + public SMTResult verifySubtype(Predicate subRef, Predicate supRef, Context context, boolean silent, + boolean needsCounterexample) throws Exception { if (!silent) { DebugLog.smtStart(subRef, supRef); } Predicate toVerify = Predicate.createConjunction(subRef, supRef.negate()); try { - Expression exp = toVerify.getExpression(); - try (TranslatorToZ3 tz3 = new TranslatorToZ3(context)) { - ExpressionToZ3Visitor visitor = new ExpressionToZ3Visitor(tz3); - Expr e = exp.accept(visitor); - Solver solver = tz3.makeSimpleSolverForExpression(e); - Status result = solver.check(); - if (result.equals(Status.UNKNOWN)) { - solver = tz3.makeSolverForExpression(e); - result = solver.check(); - } - - // subRef is not a subtype of supRef - if (result.equals(Status.SATISFIABLE)) { - Model model = solver.getModel(); - Counterexample counterexample = tz3.getCounterexample(model); - if (!silent) { - DebugLog.smtSat(counterexample); - } - return SMTResult.error(counterexample); - } - if (result.equals(Status.UNKNOWN)) { - SMTUnknownError error = new SMTUnknownError(solver.getReasonUnknown()); - if (!silent) { - DebugLog.smtError(error.getMessage()); - } - throw error; - } - if (!silent) { - DebugLog.smtUnsat(); - } - } - } catch (SyntaxException e) { - System.out.println("Could not parse: " + toVerify); - e.printStackTrace(); + return check(toVerify.getExpression(), context, silent, needsCounterexample); } catch (Z3Exception e) { throw new Z3Exception(e.getLocalizedMessage()); } - return SMTResult.ok(); } public boolean isUnsatisfiable(Predicate predicate, Context context) throws Exception { try { - Expression exp = predicate.getExpression(); - try (TranslatorToZ3 tz3 = new TranslatorToZ3(context)) { - ExpressionToZ3Visitor visitor = new ExpressionToZ3Visitor(tz3); - Expr e = exp.accept(visitor); - Status result = tz3.makeSimpleSolverForExpression(e).check(); - if (result.equals(Status.UNKNOWN)) - result = tz3.makeSolverForExpression(e).check(); - return result.equals(Status.UNSATISFIABLE); - } + return check(predicate.getExpression(), context, true, false).isOk(); + } catch (SMTUnknownError e) { + return false; } catch (Z3Exception e) { throw new Z3Exception(e.getLocalizedMessage()); } } + + /** + * Whether {@code exp} is unsatisfiable, with a counterexample when it is not, reusing the result of an earlier run + */ + private SMTResult check(Expression exp, Context context, boolean silent, boolean needsCounterexample) + throws Exception { + CacheMode mode = CacheMode.current(); + if (mode == CacheMode.OFF) + return solve(exp, context, silent); + QueryCache.Key key = QueryCache.key(exp, context); + Optional cached = QueryCache.get(key, needsCounterexample); + if (cached.isPresent() && mode == CacheMode.ON) { + if (!silent) + log(cached.get()); + return cached.get(); + } + SMTResult result = solve(exp, context, silent); + if (cached.isPresent()) + QueryCache.checkSame(key, cached.get(), result); + else + QueryCache.put(key, result); + return result; + } + + private SMTResult solve(Expression exp, Context context, boolean silent) throws Exception { + try (TranslatorToZ3 tz3 = new TranslatorToZ3(context)) { + ExpressionToZ3Visitor visitor = new ExpressionToZ3Visitor(tz3); + Expr e = exp.accept(visitor); + Solver solver = tz3.makeSimpleSolverForExpression(e); + Status result = solver.check(); + if (result.equals(Status.UNKNOWN)) { + solver = tz3.makeSolverForExpression(e); + result = solver.check(); + } + + if (result.equals(Status.UNKNOWN)) { + SMTUnknownError error = new SMTUnknownError(solver.getReasonUnknown()); + if (!silent) { + DebugLog.smtError(error.getMessage()); + } + throw error; + } + // subRef is not a subtype of supRef + SMTResult verified = result.equals(Status.SATISFIABLE) + ? SMTResult.error(tz3.getCounterexample(solver.getModel())) : SMTResult.ok(); + if (!silent) + log(verified); + return verified; + } + } + + private static void log(SMTResult result) { + if (result.isOk()) + DebugLog.smtUnsat(); + else + DebugLog.smtSat(result.getCounterexample()); + } } diff --git a/liquidjava-verifier/src/main/java/liquidjava/smt/TranslatorContextToZ3.java b/liquidjava-verifier/src/main/java/liquidjava/smt/TranslatorContextToZ3.java index 235cb170e..9e58bd905 100644 --- a/liquidjava-verifier/src/main/java/liquidjava/smt/TranslatorContextToZ3.java +++ b/liquidjava-verifier/src/main/java/liquidjava/smt/TranslatorContextToZ3.java @@ -49,11 +49,16 @@ private static EnumSort translateEnum(Context z3, Map> varTra EnumSort enumSort = z3.mkEnumSort(k, enumValueNames); Expr[] consts = enumSort.getConsts(); for (int i = 0; i < enumValueNames.length; i++) - varTranslation.put(enumDecl.getSimpleName() + "." + enumValueNames[i], consts[i]); + varTranslation.put(enumConstantName(enumDecl, enumValueNames[i]), consts[i]); return enumSort; }); } + /** The name the constant {@code value} of an enum is translated from, which denotes that constant */ + static String enumConstantName(CtEnum enumDecl, String value) { + return enumDecl.getSimpleName() + "." + value; + } + public static void storeVariablesSubtypes(Context z3, List variables, Map>> varSuperTypes) { for (RefinedVariable v : variables) { diff --git a/liquidjava-verifier/src/main/java/liquidjava/utils/CacheMode.java b/liquidjava-verifier/src/main/java/liquidjava/utils/CacheMode.java new file mode 100644 index 000000000..779e647cc --- /dev/null +++ b/liquidjava-verifier/src/main/java/liquidjava/utils/CacheMode.java @@ -0,0 +1,18 @@ +package liquidjava.utils; + +import java.util.Locale; + +/** + * Whether the verifier reuses solver results from earlier verification runs, set with the system property + * {@value #PROPERTY}: {@code on} (the default), {@code off}, or {@code check}, which recomputes every result found in + * the cache and fails if it differs, to test that the cache never changes what the verifier reports + */ +public enum CacheMode { + ON, OFF, CHECK; + + public static final String PROPERTY = "liquidjava.cache"; + + public static CacheMode current() { + return valueOf(System.getProperty(PROPERTY, "on").toUpperCase(Locale.ROOT)); + } +} diff --git a/liquidjava-verifier/src/test/java/liquidjava/api/tests/TestQueryCache.java b/liquidjava-verifier/src/test/java/liquidjava/api/tests/TestQueryCache.java new file mode 100644 index 000000000..3c5f19a20 --- /dev/null +++ b/liquidjava-verifier/src/test/java/liquidjava/api/tests/TestQueryCache.java @@ -0,0 +1,191 @@ +package liquidjava.api.tests; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import liquidjava.api.CommandLineLauncher; +import liquidjava.diagnostics.Diagnostics; +import liquidjava.diagnostics.LJDiagnostic; +import liquidjava.diagnostics.errors.NotFoundError; +import liquidjava.diagnostics.errors.SyntaxError; +import liquidjava.utils.CacheMode; + +/** + * The cache that keeps solver results across verification runs must never change what the verifier reports. Each test + * verifies a program, edits one thing the solver queries depend on without changing their text, and verifies it again, + * as the language server does after a save: the result must be the same as without the cache, and must change. + */ +class TestQueryCache { + + private static final String DOOR = """ + import liquidjava.specification.StateRefinement; + import liquidjava.specification.StateSet; + + @StateSet({"created", "open", "closed"}) + public class Test { + + @StateRefinement(to = "created(this)") + public Test() {} + + @StateRefinement(from = "created(this)", to = "%s(this)") + public void open() {} + + @StateRefinement(from = "open(this)") + public void read() {} + + @StateRefinement(from = "%s", to = "closed(this)") + public void close() {} + + public static void main(String[] args) { + Test d = new Test(); + d.open(); + d.read(); + } + } + """; + + private static final String NUMBER = """ + import liquidjava.specification.Refinement; + + public class Test { + public static void atLeastOne(@Refinement("v >= 1") %1$s v) {} + + public static void positive(@Refinement("v > 0") %1$s v) { + atLeastOne(v); + } + } + """; + + private static final String COLOR = """ + import liquidjava.specification.Refinement; + + public class Test { + enum Color { %s } + + public static void green(@Refinement("c == Color.Green") Color c) {} + + public static void notRed(@Refinement("c != Color.Red") Color c) { + green(c); + } + } + """; + + private static final String BOX = """ + import liquidjava.specification.Ghost; + import liquidjava.specification.StateRefinement; + + @Ghost("%s size") + public class Test { + + @StateRefinement(to = "size(this) > 0") + public Test() {} + + @StateRefinement(from = "size(this) >= 1") + public void take() {} + + public static void main(String[] args) { + Test b = new Test(); + b.take(); + } + } + """; + + private static final String MODE = """ + import liquidjava.specification.Refinement; + + public class Test { + enum Mode { Photo, Video } + + public static void photo(@Refinement("m == Mode.Photo") Mode m) {} + + public static void main(String[] args) { + photo(Mode.Photo); + photo(Mode.Video); + } + } + """; + + @TempDir + Path dir; + + @Test + void enumConstantsAreValuesNotVariables() throws IOException { + // the two calls only differ in the enum constant: the second must not reuse the result of the first + List cached = verify(MODE, CacheMode.ON); + assertEquals(verify(MODE, CacheMode.OFF), cached); + assertEquals(1, Diagnostics.getInstance().getErrors().size(), "photo(Mode.Video) must be reported"); + } + + @Test + void editedTransition() throws IOException { + assertSameAsUncachedAfterEdit(String.format(DOOR, "open", "open(this)"), + String.format(DOOR, "closed", "closed(this)")); + } + + @Test + void stateRefinementThatCanNeverHold() throws IOException { + assertSameAsUncachedAfterEdit(String.format(DOOR, "open", "open(this)"), + String.format(DOOR, "open", "open(this) && closed(this)")); + } + + @Test + void typeOfAVariable() throws IOException { + // integers greater than 0 are at least 1, but long values are reals to the solver + assertSameAsUncachedAfterEdit(String.format(NUMBER, "int"), String.format(NUMBER, "long")); + } + + @Test + void constantsOfAnEnum() throws IOException { + assertSameAsUncachedAfterEdit(String.format(COLOR, "Red, Green"), String.format(COLOR, "Red, Green, Blue")); + } + + @Test + void typeOfAGhost() throws IOException { + // sizes greater than 0 are at least 1 for an int ghost, but not for a double one + assertSameAsUncachedAfterEdit(String.format(BOX, "int"), String.format(BOX, "double")); + } + + /** Verifies {@code before}, then {@code after} with the cache, which must report what it reports without it */ + private void assertSameAsUncachedAfterEdit(String before, String after) throws IOException { + List reportedBefore = verify(before, CacheMode.ON); + assertWellFormed(before); + List cached = verify(after, CacheMode.ON); + List uncached = verify(after, CacheMode.OFF); + assertWellFormed(after); + assertEquals(uncached, cached); + assertNotEquals(reportedBefore, uncached, "the edit must change what the verifier reports"); + } + + /** The program must reach the solver, or the test would not exercise the cache */ + private static void assertWellFormed(String source) { + assertTrue( + Diagnostics.getInstance().getErrors().stream() + .noneMatch(e -> e instanceof SyntaxError || e instanceof NotFoundError), + () -> "not well formed:\n" + source); + } + + private List verify(String source, CacheMode mode) throws IOException { + Files.writeString(dir.resolve("Test.java"), source); + String previous = System.getProperty(CacheMode.PROPERTY); + System.setProperty(CacheMode.PROPERTY, mode.name()); + try { + CommandLineLauncher.launch(dir.toString()); + } finally { + if (previous == null) + System.clearProperty(CacheMode.PROPERTY); + else + System.setProperty(CacheMode.PROPERTY, previous); + } + Diagnostics diagnostics = Diagnostics.getInstance(); + return Stream.concat(diagnostics.getErrors().stream(), diagnostics.getWarnings().stream()) + .map(LJDiagnostic::toString).toList(); + } +} diff --git a/liquidjava-verifier/src/test/java/liquidjava/smt/QueryCacheTest.java b/liquidjava-verifier/src/test/java/liquidjava/smt/QueryCacheTest.java new file mode 100644 index 000000000..c61d299e9 --- /dev/null +++ b/liquidjava-verifier/src/test/java/liquidjava/smt/QueryCacheTest.java @@ -0,0 +1,72 @@ +package liquidjava.smt; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import java.util.Set; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import liquidjava.processor.context.Context; +import liquidjava.processor.context.Variable; +import liquidjava.rj_language.Predicate; +import spoon.Launcher; +import spoon.reflect.factory.Factory; +import spoon.reflect.reference.CtTypeReference; + +class QueryCacheTest { + + private final Context context = Context.getInstance(); + private final Factory factory = new Launcher().getFactory(); + + @AfterEach + void resetContext() { + context.reinitializeAllContext(); + } + + @Test + void variablesAreRenamedInOrderOfAppearance() { + addVariable("a", int.class); + addVariable("b", int.class); + addVariable("c", int.class); + addVariable("d", int.class); + QueryCache.Key ab = key("a > b"); + QueryCache.Key cd = key("c > d"); + assertEquals(ab.text(), cd.text()); + assertEquals(List.of("a", "b"), ab.names()); + assertEquals(List.of("c", "d"), cd.names()); + QueryCache.Key ba = key("b > a"); + assertEquals(ab.text(), ba.text()); + assertEquals(List.of("b", "a"), ba.names()); + assertNotEquals(ab.text(), key("a > a").text()); + } + + @Test + void typesOfTheVariablesAreInTheKey() { + addVariable("a", int.class); + addVariable("b", long.class); + assertNotEquals(key("a > 0").text(), key("b > 0").text()); + } + + @Test + void supertypesOfTheVariablesAreInTheKey() { + CtTypeReference type = factory.Type().createReference(java.io.IOException.class); + Variable plain = new Variable("plain", type, new Predicate()); + Variable withSupertypes = new Variable("withSupertypes", type, new Predicate()); + withSupertypes.addSuperTypes(factory.Type().createReference(Runnable.class), Set.of()); + context.addVarToContext(plain); + context.addVarToContext(withSupertypes); + assertNotEquals(key("plain == plain").text(), key("withSupertypes == withSupertypes").text()); + } + + private void addVariable(String name, Class type) { + context.addVarToContext(name, factory.Type().createReference(type), new Predicate(), + factory.Code().createCodeSnippetStatement("")); + } + + private QueryCache.Key key(String predicate) { + return QueryCache.key( + new Predicate(predicate, factory.Code().createCodeSnippetStatement(""), "").getExpression(), context); + } +}