Skip to content

Commit 089dc0a

Browse files
committed
Add Counterexamples to Refinement Errors
1 parent 1b290cb commit 089dc0a

File tree

9 files changed

+99
-34
lines changed

9 files changed

+99
-34
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package liquidjava.diagnostics;
2+
3+
import java.util.List;
4+
5+
public record Counterexample(List<String> assignments) {
6+
}

liquidjava-verifier/src/main/java/liquidjava/diagnostics/errors/RefinementError.java

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package liquidjava.diagnostics.errors;
22

3+
import liquidjava.diagnostics.Counterexample;
34
import liquidjava.diagnostics.TranslationTable;
4-
import liquidjava.rj_language.ast.Expression;
55
import liquidjava.rj_language.opt.derivation_node.ValDerivationNode;
66
import spoon.reflect.cu.SourcePosition;
77

@@ -14,13 +14,30 @@ public class RefinementError extends LJError {
1414

1515
private final ValDerivationNode expected;
1616
private final ValDerivationNode found;
17+
private final Counterexample counterexample;
1718

1819
public RefinementError(SourcePosition position, ValDerivationNode expected, ValDerivationNode found,
19-
TranslationTable translationTable) {
20+
TranslationTable translationTable, Counterexample counterexample) {
2021
super("Refinement Error", String.format("%s is not a subtype of %s", found.getValue(), expected.getValue()),
2122
position, translationTable);
2223
this.expected = expected;
2324
this.found = found;
25+
this.counterexample = counterexample;
26+
}
27+
28+
@Override
29+
public String getDetails() {
30+
if (counterexample == null || counterexample.assignments().isEmpty())
31+
return "";
32+
33+
// filter fresh variables and join assignements with &&
34+
String counterexampleExp = counterexample.assignments().stream().filter(a -> !a.startsWith("#fresh_"))
35+
.reduce((a, b) -> a + " && " + b).orElse("");
36+
37+
// check if counterexample is trivial (same as the found value)
38+
if (counterexampleExp.equals(found.getValue().toSimplifiedString()))
39+
return "";
40+
return "Counterexample: " + counterexampleExp;
2441
}
2542

2643
public ValDerivationNode getExpected() {

liquidjava-verifier/src/main/java/liquidjava/processor/refinement_checker/TypeChecker.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import java.util.List;
66
import java.util.Optional;
77

8+
import liquidjava.diagnostics.Counterexample;
89
import liquidjava.diagnostics.errors.*;
910
import liquidjava.processor.context.AliasWrapper;
1011
import liquidjava.processor.context.Context;
@@ -292,8 +293,10 @@ public void checkStateSMT(Predicate prevState, Predicate expectedState, CtElemen
292293
vcChecker.processSubtyping(prevState, expectedState, context.getGhostState(), target, factory);
293294
}
294295

295-
public boolean checksStateSMT(Predicate prevState, Predicate expectedState, SourcePosition p) throws LJError {
296-
return vcChecker.canProcessSubtyping(prevState, expectedState, context.getGhostState(), p, factory);
296+
public boolean checkStateSMT(Predicate prevState, Predicate expectedState, SourcePosition p) throws LJError {
297+
Counterexample counterexample = vcChecker.canProcessSubtyping(prevState, expectedState, context.getGhostState(),
298+
p, factory);
299+
return counterexample == null;
297300
}
298301

299302
public void throwRefinementError(SourcePosition position, Predicate expectedType, Predicate foundType)

liquidjava-verifier/src/main/java/liquidjava/processor/refinement_checker/VCChecker.java

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import java.util.stream.Collectors;
99

1010
import liquidjava.diagnostics.errors.*;
11+
import liquidjava.diagnostics.Counterexample;
1112
import liquidjava.diagnostics.TranslationTable;
1213
import liquidjava.processor.VCImplication;
1314
import liquidjava.processor.context.*;
@@ -50,10 +51,10 @@ public void processSubtyping(Predicate expectedType, List<GhostState> list, CtEl
5051
e.setPosition(element.getPosition());
5152
throw e;
5253
}
53-
boolean isSubtype = smtChecks(expected, premises, element.getPosition());
54-
if (!isSubtype)
54+
Counterexample counterexample = smtChecks(expected, premises, element.getPosition());
55+
if (counterexample != null)
5556
throw new RefinementError(element.getPosition(), expectedType.simplify(), premisesBeforeChange.simplify(),
56-
map);
57+
map, counterexample);
5758
}
5859

5960
/**
@@ -69,9 +70,9 @@ public void processSubtyping(Predicate expectedType, List<GhostState> list, CtEl
6970
*/
7071
public void processSubtyping(Predicate type, Predicate expectedType, List<GhostState> list, CtElement element,
7172
Factory f) throws LJError {
72-
boolean b = canProcessSubtyping(type, expectedType, list, element.getPosition(), f);
73-
if (!b)
74-
throwRefinementError(element.getPosition(), expectedType, type);
73+
Counterexample counterexample = canProcessSubtyping(type, expectedType, list, element.getPosition(), f);
74+
if (counterexample != null)
75+
throwRefinementError(element.getPosition(), expectedType, type, counterexample);
7576
}
7677

7778
/**
@@ -81,16 +82,16 @@ public void processSubtyping(Predicate type, Predicate expectedType, List<GhostS
8182
* @param found
8283
* @param position
8384
*
84-
* @return true if expected type is subtype of found type, false otherwise
85+
* @return counterexample if expected type is not subtype of found type, otherwise null
8586
*
8687
* @throws LJError
8788
*/
88-
public boolean smtChecks(Predicate expected, Predicate found, SourcePosition position) throws LJError {
89+
public Counterexample smtChecks(Predicate expected, Predicate found, SourcePosition position) throws LJError {
8990
try {
9091
new SMTEvaluator().verifySubtype(found, expected, context);
91-
return true;
92+
return null;
9293
} catch (TypeCheckError e) {
93-
return false;
94+
return e.getCounterexample();
9495
} catch (LJError e) {
9596
e.setPosition(position);
9697
throw e;
@@ -99,13 +100,13 @@ public boolean smtChecks(Predicate expected, Predicate found, SourcePosition pos
99100
}
100101
}
101102

102-
public boolean canProcessSubtyping(Predicate type, Predicate expectedType, List<GhostState> list,
103+
public Counterexample canProcessSubtyping(Predicate type, Predicate expectedType, List<GhostState> list,
103104
SourcePosition position, Factory f) throws LJError {
104105
List<RefinedVariable> lrv = new ArrayList<>(), mainVars = new ArrayList<>();
105106
gatherVariables(expectedType, lrv, mainVars);
106107
gatherVariables(type, lrv, mainVars);
107108
if (expectedType.isBooleanTrue() && type.isBooleanTrue())
108-
return true;
109+
return null;
109110

110111
TranslationTable map = new TranslationTable();
111112
String[] s = { Keys.WILDCARD, Keys.THIS };
@@ -258,12 +259,17 @@ void removePathVariableThatIncludes(String otherVar) {
258259

259260
protected void throwRefinementError(SourcePosition position, Predicate expected, Predicate found)
260261
throws RefinementError {
262+
throwRefinementError(position, expected, found, null);
263+
}
264+
265+
protected void throwRefinementError(SourcePosition position, Predicate expected, Predicate found,
266+
Counterexample counterexample) throws RefinementError {
261267
List<RefinedVariable> lrv = new ArrayList<>(), mainVars = new ArrayList<>();
262268
gatherVariables(expected, lrv, mainVars);
263269
gatherVariables(found, lrv, mainVars);
264270
TranslationTable map = new TranslationTable();
265271
Predicate premises = joinPredicates(expected, mainVars, lrv, map).toConjunctions();
266-
throw new RefinementError(position, expected.simplify(), premises.simplify(), map);
272+
throw new RefinementError(position, expected.simplify(), premises.simplify(), map, counterexample);
267273
}
268274

269275
protected void throwStateRefinementError(SourcePosition position, Predicate found, Predicate expected)

liquidjava-verifier/src/main/java/liquidjava/processor/refinement_checker/object_checkers/AuxHierarchyRefinementsPassage.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ static void transferArgumentsRefinements(RefinedFunction superFunction, RefinedF
8181
if (argRef.isBooleanTrue()) {
8282
arg.setRefinement(superArgRef.substituteVariable(newName, arg.getName()));
8383
} else {
84-
boolean ok = tc.checksStateSMT(superArgRef, argRef, params.get(i).getPosition());
84+
boolean ok = tc.checkStateSMT(superArgRef, argRef, params.get(i).getPosition());
8585
if (!ok) {
8686
tc.throwRefinementError(method.getPosition(), argRef, superArgRef);
8787
}

liquidjava-verifier/src/main/java/liquidjava/processor/refinement_checker/object_checkers/AuxStateHandler.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ private static Predicate createStatePredicate(String value, String targetClass,
213213
Predicate c1 = isTo ? getMissingStates(targetClass, tc, p) : p;
214214
Predicate c = c1.substituteVariable(Keys.THIS, name);
215215
c = c.changeOldMentions(nameOld, name);
216-
boolean ok = tc.checksStateSMT(new Predicate(), c.negate(), e.getPosition());
216+
boolean ok = tc.checkStateSMT(new Predicate(), c.negate(), e.getPosition());
217217
if (ok) {
218218
tc.throwStateConflictError(e.getPosition(), p);
219219
}
@@ -406,7 +406,7 @@ public static void updateGhostField(CtFieldWrite<?> fw, TypeChecker tc) throws L
406406
Predicate expectState = stateChange.getFrom().substituteVariable(Keys.THIS, instanceName)
407407
.changeOldMentions(vi.getName(), instanceName);
408408

409-
if (!tc.checksStateSMT(prevState, expectState, fw.getPosition())) { // Invalid field transition
409+
if (!tc.checkStateSMT(prevState, expectState, fw.getPosition())) { // Invalid field transition
410410
tc.throwStateRefinementError(fw.getPosition(), prevState, stateChange.getFrom());
411411
return;
412412
}
@@ -471,7 +471,7 @@ private static void changeState(TypeChecker tc, VariableInstance vi, List<Object
471471
}
472472
expectState = expectState.changeOldMentions(vi.getName(), instanceName);
473473

474-
found = tc.checksStateSMT(prevCheck, expectState, invocation.getPosition());
474+
found = tc.checkStateSMT(prevCheck, expectState, invocation.getPosition());
475475
if (found && stateChange.hasTo()) {
476476
String newInstanceName = String.format(Formats.INSTANCE, name, tc.getContext().getCounter());
477477
Predicate transitionedState = stateChange.getTo().substituteVariable(Keys.WILDCARD, newInstanceName)
@@ -489,7 +489,6 @@ private static void changeState(TypeChecker tc, VariableInstance vi, List<Object
489489
Predicate expectedStatesDisjunction = stateChanges.stream().filter(ObjectState::hasFrom)
490490
.map(ObjectState::getFrom)
491491
.reduce(Predicate.createLit("false", Types.BOOLEAN), Predicate::createDisjunction);
492-
String simpleInvocation = invocation.toString();
493492
tc.throwStateRefinementError(invocation.getPosition(), prevState, expectedStatesDisjunction);
494493
}
495494
}

liquidjava-verifier/src/main/java/liquidjava/smt/SMTEvaluator.java

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22

33
import com.martiansoftware.jsap.SyntaxException;
44
import com.microsoft.z3.Expr;
5+
import com.microsoft.z3.Model;
6+
import com.microsoft.z3.Solver;
57
import com.microsoft.z3.Status;
68
import com.microsoft.z3.Z3Exception;
79

10+
import liquidjava.diagnostics.Counterexample;
811
import liquidjava.processor.context.Context;
912
import liquidjava.rj_language.Predicate;
1013
import liquidjava.rj_language.ast.Expression;
@@ -17,13 +20,15 @@ public void verifySubtype(Predicate subRef, Predicate supRef, Context c) throws
1720
Predicate toVerify = Predicate.createConjunction(subRef, supRef.negate());
1821
try {
1922
Expression exp = toVerify.getExpression();
20-
Status s;
2123
try (TranslatorToZ3 tz3 = new TranslatorToZ3(c)) {
2224
ExpressionToZ3Visitor visitor = new ExpressionToZ3Visitor(tz3);
2325
Expr<?> e = exp.accept(visitor);
24-
s = tz3.verifyExpression(e);
25-
if (s.equals(Status.SATISFIABLE)) {
26-
throw new TypeCheckError(subRef + " not a subtype of " + supRef);
26+
Solver solver = tz3.makeSolverForExpression(e);
27+
Status result = solver.check();
28+
if (result.equals(Status.SATISFIABLE)) {
29+
Model model = solver.getModel();
30+
Counterexample counterexample = tz3.getCounterexample(model);
31+
throw new TypeCheckError(counterexample);
2732
}
2833
}
2934
} catch (SyntaxException e1) {

liquidjava-verifier/src/main/java/liquidjava/smt/TranslatorToZ3.java

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,25 @@
11
package liquidjava.smt;
22

3-
import com.martiansoftware.jsap.SyntaxException;
43
import com.microsoft.z3.ArithExpr;
54
import com.microsoft.z3.ArrayExpr;
65
import com.microsoft.z3.BoolExpr;
76
import com.microsoft.z3.Expr;
87
import com.microsoft.z3.FPExpr;
98
import com.microsoft.z3.FuncDecl;
9+
import com.microsoft.z3.FuncInterp;
1010
import com.microsoft.z3.IntExpr;
1111
import com.microsoft.z3.IntNum;
1212
import com.microsoft.z3.RealExpr;
1313
import com.microsoft.z3.Solver;
1414
import com.microsoft.z3.Sort;
15-
import com.microsoft.z3.Status;
15+
import com.microsoft.z3.Model;
16+
17+
import java.util.ArrayList;
1618
import java.util.HashMap;
1719
import java.util.List;
1820
import java.util.Map;
1921

22+
import liquidjava.diagnostics.Counterexample;
2023
import liquidjava.diagnostics.errors.LJError;
2124
import liquidjava.diagnostics.errors.NotFoundError;
2225
import liquidjava.processor.context.AliasWrapper;
@@ -42,10 +45,27 @@ public TranslatorToZ3(liquidjava.processor.context.Context c) {
4245
}
4346

4447
@SuppressWarnings("unchecked")
45-
public Status verifyExpression(Expr<?> e) {
46-
Solver s = z3.mkSolver();
47-
s.add((BoolExpr) e);
48-
return s.check();
48+
public Solver makeSolverForExpression(Expr<?> e) {
49+
Solver solver = z3.mkSolver();
50+
solver.add((BoolExpr) e);
51+
return solver;
52+
}
53+
54+
/**
55+
* Extracts the counterexample from the Z3 model
56+
*/
57+
public Counterexample getCounterexample(Model model) {
58+
List<String> assignments = new ArrayList<>();
59+
for (FuncDecl<?> decl : model.getDecls()) {
60+
// extract variable assignments with constants
61+
if (decl.getArity() == 0) {
62+
String name = decl.getName().toString();
63+
String value = model.getConstInterp(decl).toString();
64+
assignments.add(name + " == " + value);
65+
}
66+
// TODO: extract function assignments (arity > 0)?
67+
}
68+
return new Counterexample(assignments);
4969
}
5070

5171
// #####################Literals and Variables#####################
Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
11
package liquidjava.smt;
22

3+
import liquidjava.diagnostics.Counterexample;
4+
35
public class TypeCheckError extends Exception {
46

5-
public TypeCheckError(String message) {
6-
super(message);
7+
private final Counterexample counterexample;
8+
9+
public TypeCheckError(Counterexample counterexample) {
10+
super("Refinement was violated");
11+
this.counterexample = counterexample;
12+
}
13+
14+
public Counterexample getCounterexample() {
15+
return counterexample;
716
}
817
}

0 commit comments

Comments
 (0)