Skip to content

Commit 19ebba1

Browse files
authored
Unsatisfiable Refinement Warning (#202)
1 parent 40e105f commit 19ebba1

6 files changed

Lines changed: 115 additions & 1 deletion

File tree

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package testSuite;
2+
3+
import liquidjava.specification.Refinement;
4+
5+
public class ErrorWarningUnsatRefinement {
6+
7+
public void example1() {
8+
@Refinement("x == 1 && x != 1") // Unsat Refinement Warning
9+
int x = 1; // Refinement Error
10+
}
11+
12+
public void example2() {
13+
@Refinement("x % 2 > 1") // Unsat Refinement Warning
14+
int x = 5; // Refinement Error
15+
}
16+
17+
public void example3() {
18+
@Refinement("false") // Unsat Refinement Warning
19+
int x = 0; // Refinement Error
20+
}
21+
22+
public void example4(@Refinement("x > 0 && x < 0") int x) {} // Unsat Refinement Warning
23+
24+
@Refinement("_ == true && _ == false") // Unsat Refinement Warning
25+
public boolean example5() {
26+
return true; // Refinement Error
27+
}
28+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package liquidjava.diagnostics.warnings;
2+
3+
import spoon.reflect.cu.SourcePosition;
4+
5+
/**
6+
* Warning indicating that a refinement predicate is unsatisfiable
7+
*
8+
* @see LJWarning
9+
*/
10+
public class UnsatisfiableRefinementWarning extends LJWarning {
11+
12+
private final String refinement;
13+
14+
public UnsatisfiableRefinementWarning(SourcePosition position, String refinement) {
15+
super("This refinement can never be true", position);
16+
this.refinement = refinement;
17+
}
18+
19+
public String getRefinement() {
20+
return refinement;
21+
}
22+
}

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

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
import java.util.Map;
77
import java.util.Optional;
88

9+
import liquidjava.diagnostics.Diagnostics;
910
import liquidjava.diagnostics.errors.*;
11+
import liquidjava.diagnostics.warnings.UnsatisfiableRefinementWarning;
1012
import liquidjava.processor.context.AliasWrapper;
1113
import liquidjava.processor.context.Context;
1214
import liquidjava.processor.context.GhostFunction;
@@ -16,6 +18,7 @@
1618
import liquidjava.processor.facade.GhostDTO;
1719
import liquidjava.rj_language.Predicate;
1820
import liquidjava.rj_language.parsing.RefinementsParser;
21+
import liquidjava.smt.SMTEvaluator;
1922
import liquidjava.smt.SMTResult;
2023
import liquidjava.utils.Utils;
2124
import liquidjava.utils.constants.Formats;
@@ -29,6 +32,9 @@
2932
import spoon.reflect.declaration.CtClass;
3033
import spoon.reflect.declaration.CtElement;
3134
import spoon.reflect.declaration.CtInterface;
35+
import spoon.reflect.declaration.CtMethod;
36+
import spoon.reflect.declaration.CtTypedElement;
37+
import spoon.reflect.declaration.CtVariable;
3238
import spoon.reflect.declaration.CtType;
3339
import spoon.reflect.factory.Factory;
3440
import spoon.reflect.reference.CtTypeReference;
@@ -41,6 +47,7 @@ public abstract class TypeChecker extends CtScanner {
4147
protected final Context context;
4248
protected final Factory factory;
4349
protected final VCChecker vcChecker;
50+
private final Diagnostics diagnostics = Diagnostics.getInstance();
4451

4552
public TypeChecker(Context context, Factory factory) {
4653
this.context = context;
@@ -88,11 +95,53 @@ public Optional<Predicate> getRefinementFromAnnotation(CtElement element) throws
8895
throw new InvalidRefinementError(position, "Refinement predicate must be a boolean expression",
8996
ref.get());
9097
}
98+
if (!Boolean.TRUE.equals(element.getMetadata(Keys.REFINEMENT_SAT_CHECK)))
99+
checkRefinementSatisfiability(ref.get(), p, element);
91100
constr = Optional.of(p);
92101
}
93102
return constr;
94103
}
95104

105+
/**
106+
* Performs a best-effort satisfiability check for a refinement reporting a warning if unsat. Runs an SMT check on a
107+
* temporary scope and if the refinement mentions other names that are still unavailable at this point, the SMT
108+
* check fails and that failure is ignored.
109+
*/
110+
private void checkRefinementSatisfiability(String refinement, Predicate predicate, CtElement element) {
111+
context.enterContext();
112+
try {
113+
Predicate p = new Predicate();
114+
CtTypeReference<?> annotationType = element instanceof CtTypedElement<?> typedElement
115+
? typedElement.getType() : null;
116+
if (annotationType != null && !context.hasVariable(Keys.WILDCARD))
117+
context.addVarToContext(Keys.WILDCARD, annotationType, p, element);
118+
119+
if (element instanceof CtVariable<?> variable && !context.hasVariable(variable.getSimpleName()))
120+
context.addVarToContext(variable.getSimpleName(), variable.getType(), p, element);
121+
122+
if (element instanceof CtMethod<?> method && method.getType() != null && !context.hasVariable("return"))
123+
context.addVarToContext("return", method.getType(), p, element);
124+
125+
String qualifiedClassName = getQualifiedClassName(element);
126+
if (qualifiedClassName != null && !context.hasVariable(Keys.THIS))
127+
context.addVarToContext(Keys.THIS, factory.Type().createReference(qualifiedClassName), p, element);
128+
129+
Predicate refinementPredicate = predicate.changeStatesToRefinements(context.getGhostStates(),
130+
new String[] { Keys.WILDCARD, Keys.THIS });
131+
refinementPredicate = refinementPredicate.changeAliasToRefinement(context, factory);
132+
133+
if (new SMTEvaluator().isUnsatisfiable(refinementPredicate, context)) {
134+
SourcePosition position = Utils.getLJAnnotationPosition(element, refinement);
135+
diagnostics.add(new UnsatisfiableRefinementWarning(position, refinement));
136+
}
137+
element.putMetadata(Keys.REFINEMENT_SAT_CHECK, true); // for caching the satisfiability check result
138+
} catch (Exception e) {
139+
// ignore
140+
} finally {
141+
context.exitContext();
142+
}
143+
}
144+
96145
@SuppressWarnings({ "rawtypes" })
97146
public Optional<String> getMessageFromAnnotation(CtElement element) {
98147
for (CtAnnotation<? extends Annotation> ann : element.getAnnotations()) {

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,18 @@ public SMTResult verifySubtype(Predicate subRef, Predicate supRef, Context conte
4848
}
4949
return SMTResult.ok();
5050
}
51+
52+
public boolean isUnsatisfiable(Predicate predicate, Context context) throws Exception {
53+
try {
54+
Expression exp = predicate.getExpression();
55+
try (TranslatorToZ3 tz3 = new TranslatorToZ3(context)) {
56+
ExpressionToZ3Visitor visitor = new ExpressionToZ3Visitor(tz3);
57+
Expr<?> e = exp.accept(visitor);
58+
Solver solver = tz3.makeSolverForExpression(e);
59+
return solver.check().equals(Status.UNSATISFIABLE);
60+
}
61+
} catch (Z3Exception e) {
62+
throw new Z3Exception(e.getLocalizedMessage());
63+
}
64+
}
5165
}

liquidjava-verifier/src/main/java/liquidjava/utils/constants/Keys.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
public final class Keys {
44
public static final String REFINEMENT = "refinement";
5+
public static final String REFINEMENT_SAT_CHECK = "refinement_sat_check";
56
public static final String TARGET = "target";
67
public static final String THIS = "this";
78
public static final String WILDCARD = "_";

liquidjava-verifier/src/test/java/liquidjava/utils/TestUtils.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public class TestUtils {
3434
* @param path
3535
*/
3636
public static boolean shouldPass(String path) {
37-
return path.toLowerCase().contains("correct") || path.toLowerCase().contains("warning");
37+
return path.toLowerCase().contains("correct");
3838
}
3939

4040
/**

0 commit comments

Comments
 (0)