|
| 1 | +package liquidjava.smt; |
| 2 | + |
| 3 | +import java.util.ArrayList; |
| 4 | +import java.util.Collection; |
| 5 | +import java.util.Collections; |
| 6 | +import java.util.HashSet; |
| 7 | +import java.util.LinkedHashMap; |
| 8 | +import java.util.List; |
| 9 | +import java.util.Map; |
| 10 | +import java.util.Optional; |
| 11 | +import java.util.Set; |
| 12 | +import java.util.stream.Collectors; |
| 13 | + |
| 14 | +import liquidjava.processor.context.Context; |
| 15 | +import liquidjava.processor.context.GhostFunction; |
| 16 | +import liquidjava.processor.context.RefinedVariable; |
| 17 | +import liquidjava.rj_language.ast.AliasInvocation; |
| 18 | +import liquidjava.rj_language.ast.BinaryExpression; |
| 19 | +import liquidjava.rj_language.ast.Enum; |
| 20 | +import liquidjava.rj_language.ast.Expression; |
| 21 | +import liquidjava.rj_language.ast.FunctionInvocation; |
| 22 | +import liquidjava.rj_language.ast.Ite; |
| 23 | +import liquidjava.rj_language.ast.LiteralBoolean; |
| 24 | +import liquidjava.rj_language.ast.LiteralChar; |
| 25 | +import liquidjava.rj_language.ast.LiteralInt; |
| 26 | +import liquidjava.rj_language.ast.LiteralLong; |
| 27 | +import liquidjava.rj_language.ast.LiteralReal; |
| 28 | +import liquidjava.rj_language.ast.LiteralString; |
| 29 | +import liquidjava.rj_language.ast.UnaryExpression; |
| 30 | +import liquidjava.rj_language.ast.Var; |
| 31 | +import liquidjava.rj_language.visitors.ExpressionVisitor; |
| 32 | +import spoon.reflect.declaration.CtEnum; |
| 33 | +import spoon.reflect.reference.CtTypeReference; |
| 34 | + |
| 35 | +/** |
| 36 | + * Results of solver queries, kept across verification runs. A long-lived process such as the language server verifies |
| 37 | + * the whole program again after every save, and asks the solver mostly the same queries each time. |
| 38 | + * <p> |
| 39 | + * A query is identified by everything {@link TranslatorToZ3} reads to build it: the expression, node by node, the type |
| 40 | + * and supertypes of each of its variables, and the enums, ghosts and ghost states of the context. Variables are renamed |
| 41 | + * in order of appearance, so that a query whose variables were numbered differently by an earlier edit is still found. |
| 42 | + * Names that denote enum constants are values, not variables, and keep their name. A counterexample names the |
| 43 | + * variables, so a cached one is only reused when the names are the same; otherwise a caller that needs one asks the |
| 44 | + * solver again. Unknown results and errors are never kept. Results only live as long as the process, so the verifier |
| 45 | + * and the solver cannot change under them. |
| 46 | + */ |
| 47 | +final class QueryCache { |
| 48 | + |
| 49 | + private static final int MAX_RESULTS = 50_000; |
| 50 | + |
| 51 | + private static final Map<String, Entry> RESULTS = Collections.synchronizedMap(new LinkedHashMap<>(16, 0.75f, true) { |
| 52 | + @Override |
| 53 | + protected boolean removeEldestEntry(Map.Entry<String, Entry> eldest) { |
| 54 | + return size() > MAX_RESULTS; |
| 55 | + } |
| 56 | + }); |
| 57 | + |
| 58 | + /** |
| 59 | + * @param text |
| 60 | + * the query with its variables renamed |
| 61 | + * @param names |
| 62 | + * the variables of the query, in the order they were renamed |
| 63 | + */ |
| 64 | + record Key(String text, List<String> names) { |
| 65 | + } |
| 66 | + |
| 67 | + /** A result, whose counterexample names the variables in {@code names} */ |
| 68 | + private record Entry(SMTResult result, List<String> names) { |
| 69 | + } |
| 70 | + |
| 71 | + private QueryCache() { |
| 72 | + } |
| 73 | + |
| 74 | + static Key key(Expression expression, Context context) { |
| 75 | + Map<String, CtTypeReference<?>> types = context.getContext(); |
| 76 | + List<String> occurrences = new ArrayList<>(); |
| 77 | + expression.getVariableNames(occurrences); |
| 78 | + Set<String> constants = enumConstants(types.values()); |
| 79 | + List<String> names = occurrences.stream().distinct().filter(n -> types.containsKey(n) && !constants.contains(n)) |
| 80 | + .toList(); |
| 81 | + |
| 82 | + StringBuilder text = new StringBuilder(expression.accept(new Canonical(names))).append('\n'); |
| 83 | + Map<String, List<String>> supertypes = new LinkedHashMap<>(); |
| 84 | + for (RefinedVariable variable : context.getAllVariablesWithSupertypes()) |
| 85 | + if (names.contains(variable.getName())) |
| 86 | + supertypes.computeIfAbsent(variable.getName(), k -> new ArrayList<>()).add( |
| 87 | + variable.getSuperTypes().stream().map(QueryCache::describe).collect(Collectors.joining(", "))); |
| 88 | + for (int i = 0; i < names.size(); i++) { |
| 89 | + String name = names.get(i); |
| 90 | + text.append('@').append(i).append(": ").append(describe(types.get(name))); |
| 91 | + supertypes.getOrDefault(name, List.of()).forEach(s -> text.append(" <: ").append(s)); |
| 92 | + text.append('\n'); |
| 93 | + } |
| 94 | + // every enum of the context, as their constants can be named in the query |
| 95 | + types.values().stream().filter(CtTypeReference::isEnum).map(QueryCache::describe).distinct() |
| 96 | + .forEach(e -> text.append("enum ").append(e).append('\n')); |
| 97 | + for (GhostFunction ghost : context.getGhosts()) |
| 98 | + text.append("ghost ").append(describe(ghost)).append('\n'); |
| 99 | + for (GhostFunction state : context.getGhostStates()) |
| 100 | + text.append("state ").append(describe(state)).append('\n'); |
| 101 | + return new Key(text.toString(), names); |
| 102 | + } |
| 103 | + |
| 104 | + /** |
| 105 | + * The result kept for {@code key}, or nothing if there is none or it cannot be used: a counterexample is only |
| 106 | + * reused when the variables have the same names |
| 107 | + */ |
| 108 | + static Optional<SMTResult> get(Key key, boolean needsCounterexample) { |
| 109 | + Entry entry = RESULTS.get(key.text()); |
| 110 | + if (entry == null) |
| 111 | + return Optional.empty(); |
| 112 | + if (entry.result().isOk() || entry.names().equals(key.names())) |
| 113 | + return Optional.of(entry.result()); |
| 114 | + return needsCounterexample ? Optional.empty() : Optional.of(SMTResult.error(new Counterexample(List.of()))); |
| 115 | + } |
| 116 | + |
| 117 | + static void put(Key key, SMTResult result) { |
| 118 | + RESULTS.put(key.text(), new Entry(result, key.names())); |
| 119 | + } |
| 120 | + |
| 121 | + /** |
| 122 | + * Fails if {@code recomputed} differs from {@code cached}, with an {@link AssertionError} so that code that |
| 123 | + * recovers from exceptions cannot hide it |
| 124 | + */ |
| 125 | + static void checkSame(Key key, SMTResult cached, SMTResult recomputed) { |
| 126 | + boolean same = cached.isOk() == recomputed.isOk() && (cached.isOk() || cached.getCounterexample().isEmpty() |
| 127 | + || cached.getCounterexample().equals(recomputed.getCounterexample())); |
| 128 | + if (!same) |
| 129 | + throw new AssertionError( |
| 130 | + String.format("The solver query cache returned %s but recomputing gives %s for%n%s", |
| 131 | + describe(cached), describe(recomputed), key.text())); |
| 132 | + } |
| 133 | + |
| 134 | + /** The names the translator binds to the constants of the enums, which denote values rather than variables */ |
| 135 | + private static Set<String> enumConstants(Collection<CtTypeReference<?>> types) { |
| 136 | + Set<String> constants = new HashSet<>(); |
| 137 | + for (CtTypeReference<?> type : types) |
| 138 | + if (type.isEnum() && type.getDeclaration()instanceof CtEnum<?> declaration) |
| 139 | + declaration.getEnumValues().forEach( |
| 140 | + v -> constants.add(TranslatorContextToZ3.enumConstantName(declaration, v.getSimpleName()))); |
| 141 | + return constants; |
| 142 | + } |
| 143 | + |
| 144 | + private static String describe(SMTResult result) { |
| 145 | + return result.isOk() ? "ok" : "error with " + result.getCounterexample(); |
| 146 | + } |
| 147 | + |
| 148 | + /** What the translator reads from a type: its names, and the constants of an enum */ |
| 149 | + private static String describe(CtTypeReference<?> type) { |
| 150 | + if (type == null) |
| 151 | + return "?"; |
| 152 | + if (type.isEnum() && type.getDeclaration()instanceof CtEnum<?> declaration) |
| 153 | + return type.getQualifiedName() + declaration.getEnumValues().stream().map(v -> v.getSimpleName()).toList(); |
| 154 | + return type.getQualifiedName() + " " + type; |
| 155 | + } |
| 156 | + |
| 157 | + private static String describe(GhostFunction ghost) { |
| 158 | + return ghost.getQualifiedName() + ghost.getParametersTypes().stream().map(Object::toString).toList() + " " |
| 159 | + + ghost.getReturnType(); |
| 160 | + } |
| 161 | + |
| 162 | + /** The expression node by node, with its variables renamed in order of appearance */ |
| 163 | + private static final class Canonical implements ExpressionVisitor<String> { |
| 164 | + |
| 165 | + private final List<String> names; |
| 166 | + |
| 167 | + Canonical(List<String> names) { |
| 168 | + this.names = names; |
| 169 | + } |
| 170 | + |
| 171 | + private String all(List<Expression> expressions) { |
| 172 | + return expressions.stream().map(e -> e.accept(this)).collect(Collectors.joining(" ")); |
| 173 | + } |
| 174 | + |
| 175 | + @Override |
| 176 | + public String visitAliasInvocation(AliasInvocation alias) { |
| 177 | + return "(alias " + alias.getName() + " " + all(alias.getArgs()) + ")"; |
| 178 | + } |
| 179 | + |
| 180 | + @Override |
| 181 | + public String visitBinaryExpression(BinaryExpression exp) { |
| 182 | + return "(" + exp.getOperator() + " " + exp.getFirstOperand().accept(this) + " " |
| 183 | + + exp.getSecondOperand().accept(this) + ")"; |
| 184 | + } |
| 185 | + |
| 186 | + @Override |
| 187 | + public String visitFunctionInvocation(FunctionInvocation fun) { |
| 188 | + return "(call " + fun.getName() + " " + all(fun.getArgs()) + ")"; |
| 189 | + } |
| 190 | + |
| 191 | + @Override |
| 192 | + public String visitIte(Ite ite) { |
| 193 | + return "(ite " + ite.getCondition().accept(this) + " " + ite.getThen().accept(this) + " " |
| 194 | + + ite.getElse().accept(this) + ")"; |
| 195 | + } |
| 196 | + |
| 197 | + @Override |
| 198 | + public String visitLiteralInt(LiteralInt lit) { |
| 199 | + return "(int " + lit.getValue() + ")"; |
| 200 | + } |
| 201 | + |
| 202 | + @Override |
| 203 | + public String visitLiteralLong(LiteralLong lit) { |
| 204 | + return "(long " + lit.getValue() + ")"; |
| 205 | + } |
| 206 | + |
| 207 | + @Override |
| 208 | + public String visitLiteralBoolean(LiteralBoolean lit) { |
| 209 | + return "(bool " + lit.isBooleanTrue() + ")"; |
| 210 | + } |
| 211 | + |
| 212 | + @Override |
| 213 | + public String visitLiteralChar(LiteralChar lit) { |
| 214 | + return "(char " + (int) lit.getValue() + ")"; |
| 215 | + } |
| 216 | + |
| 217 | + @Override |
| 218 | + public String visitLiteralReal(LiteralReal lit) { |
| 219 | + return "(real " + Double.doubleToLongBits(lit.getValue()) + ")"; |
| 220 | + } |
| 221 | + |
| 222 | + @Override |
| 223 | + public String visitLiteralString(LiteralString lit) { |
| 224 | + return "(string " + lit.toString().length() + ":" + lit + ")"; |
| 225 | + } |
| 226 | + |
| 227 | + @Override |
| 228 | + public String visitUnaryExpression(UnaryExpression exp) { |
| 229 | + return "(" + exp.getOp() + " " + exp.getExpression().accept(this) + ")"; |
| 230 | + } |
| 231 | + |
| 232 | + @Override |
| 233 | + public String visitEnum(Enum en) { |
| 234 | + Expression literal = en.getResolvedLiteral(); |
| 235 | + return "(const " + en.getTypeName() + " " + en.getConstName() + " " |
| 236 | + + (literal == null ? "-" : literal.accept(this)) + ")"; |
| 237 | + } |
| 238 | + |
| 239 | + @Override |
| 240 | + public String visitVar(Var var) { |
| 241 | + int index = names.indexOf(var.getName()); |
| 242 | + return index < 0 ? "(free " + var.getName().length() + ":" + var.getName() + ")" : "@" + index; |
| 243 | + } |
| 244 | + } |
| 245 | +} |
0 commit comments