Skip to content

Commit 39c531f

Browse files
Drop path conditions that expected does not depend on from Found
A generated boolean binder (a path condition such as #if_condition) whose variable and refinement share no term with Expected, directly or through other facts, and that no other row mentions, is removed as a step of the Binder pass. The original chain the panel steps back to keeps it. The linking helpers (factsOf, linkedTerms, termsOf, termName, linkedToExpected) move from RefinementError to VCSimplificationUtils, so the simplified chain, the message and the counterexample link facts the same way. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 9aa2467 commit 39c531f

7 files changed

Lines changed: 190 additions & 108 deletions

File tree

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

Lines changed: 5 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import java.util.ArrayList;
44
import java.util.Comparator;
55
import java.util.HashSet;
6-
import java.util.Iterator;
76
import java.util.List;
87
import java.util.Set;
98
import java.util.stream.Collectors;
@@ -15,9 +14,6 @@
1514
import liquidjava.rj_language.ast.BinaryExpression;
1615
import liquidjava.rj_language.ast.Enum;
1716
import liquidjava.rj_language.ast.Expression;
18-
import liquidjava.rj_language.ast.FunctionInvocation;
19-
import liquidjava.rj_language.ast.LiteralBoolean;
20-
import liquidjava.rj_language.ast.UnaryExpression;
2117
import liquidjava.rj_language.ast.Var;
2218
import liquidjava.rj_language.ast.formatter.VariableFormatter;
2319
import liquidjava.rj_language.opt.VCSimplificationResult;
@@ -27,7 +23,12 @@
2723
import liquidjava.utils.Utils;
2824
import spoon.reflect.cu.SourcePosition;
2925

26+
import static liquidjava.rj_language.opt.VCSimplificationUtils.factsOf;
3027
import static liquidjava.rj_language.opt.VCSimplificationUtils.isTrue;
28+
import static liquidjava.rj_language.opt.VCSimplificationUtils.linkedTerms;
29+
import static liquidjava.rj_language.opt.VCSimplificationUtils.linkedToExpected;
30+
import static liquidjava.rj_language.opt.VCSimplificationUtils.termName;
31+
import static liquidjava.rj_language.opt.VCSimplificationUtils.termsOf;
3132

3233
/**
3334
* Error indicating that a refinement constraint either was violated or cannot be proven
@@ -72,21 +73,6 @@ private static String message(Predicate expected, VCSimplificationResult found)
7273
expected.getExpression().toDisplayString());
7374
}
7475

75-
/**
76-
* The facts of found linked to the terms of expected, as a conjunction, or true when there are none; found as
77-
* written when every fact is linked
78-
*/
79-
static Expression linkedToExpected(Expression found, Expression expected) {
80-
List<Expression> facts = factsOf(found);
81-
Set<String> linked = linkedTerms(facts, termsOf(expected));
82-
List<Expression> linkedFacts = facts.stream().filter(fact -> termsOf(fact).stream().anyMatch(linked::contains))
83-
.toList();
84-
if (linkedFacts.size() == facts.size())
85-
return found;
86-
return linkedFacts.stream().reduce((first, second) -> new BinaryExpression(first, "&&", second))
87-
.orElse(new LiteralBoolean(true));
88-
}
89-
9076
/** Unknown when what was found says nothing about the values the expected refinement constrains */
9177
private static ErrorCategory categoryOf(Predicate expected, VCSimplificationResult found) {
9278
List<String> foundVariables = found.getImplication().toPredicate().getVariableNames();
@@ -138,47 +124,6 @@ private Counterexample filterCounterexample(Counterexample counterexample) {
138124
.toList());
139125
}
140126

141-
// Conjuncts of found, also splitting negated disjunctions (!(a || b) is !a && !b) and dropping double negations, so
142-
// each fact links only the terms it relates
143-
private static List<Expression> factsOf(Expression expression) {
144-
List<Expression> facts = new ArrayList<>();
145-
if (expression instanceof BinaryExpression binary && "&&".equals(binary.getOperator())) {
146-
facts.addAll(factsOf(binary.getFirstOperand()));
147-
facts.addAll(factsOf(binary.getSecondOperand()));
148-
} else if (expression instanceof UnaryExpression not && "!".equals(not.getOp())
149-
&& not.getExpression()instanceof BinaryExpression binary && "||".equals(binary.getOperator())) {
150-
facts.addAll(factsOf(new UnaryExpression("!", binary.getFirstOperand())));
151-
facts.addAll(factsOf(new UnaryExpression("!", binary.getSecondOperand())));
152-
} else if (expression instanceof UnaryExpression not && "!".equals(not.getOp())
153-
&& not.getExpression()instanceof UnaryExpression inner && "!".equals(inner.getOp())) {
154-
facts.addAll(factsOf(inner.getExpression()));
155-
} else {
156-
facts.add(expression);
157-
}
158-
return facts;
159-
}
160-
161-
// Terms of expected plus those of every fact sharing a term with them, directly or through other facts. The
162-
// remaining facts constrain other values only, so any values satisfying them make the same counterexample
163-
private static Set<String> linkedTerms(List<Expression> facts, List<String> expectedTerms) {
164-
Set<String> linked = new HashSet<>(expectedTerms);
165-
List<List<String>> unlinked = facts.stream().map(RefinementError::termsOf)
166-
.collect(Collectors.toCollection(ArrayList::new));
167-
boolean changed = true;
168-
while (changed) {
169-
changed = false;
170-
for (Iterator<List<String>> it = unlinked.iterator(); it.hasNext();) {
171-
List<String> terms = it.next();
172-
if (terms.stream().anyMatch(linked::contains)) {
173-
linked.addAll(terms);
174-
it.remove();
175-
changed = true;
176-
}
177-
}
178-
}
179-
return linked;
180-
}
181-
182127
// Whether a fact of found is term == value, which the counterexample would only repeat
183128
private static boolean isKnown(Pair<String, String> assignment, List<Expression> facts) {
184129
return facts.stream()
@@ -209,32 +154,4 @@ private static int relevance(String term, List<String> expectedTerms, List<Strin
209154
return foundTerms.contains(term) ? 1 : 0;
210155
return GeneratedNames.isGenerated(term) ? 3 : 2;
211156
}
212-
213-
// Variables and ghost applications of an expression, named as in the SMT model
214-
private static List<String> termsOf(Expression expression) {
215-
List<String> terms = new ArrayList<>();
216-
expression.getVariableNames(terms);
217-
addGhostApplications(expression, terms);
218-
return terms;
219-
}
220-
221-
private static void addGhostApplications(Expression expression, List<String> terms) {
222-
String term = expression instanceof FunctionInvocation ? termName(expression) : null;
223-
if (term != null && !terms.contains(term))
224-
terms.add(term);
225-
for (Expression child : expression.getChildren())
226-
addGhostApplications(child, terms);
227-
}
228-
229-
// Name of a variable, or of a ghost application over variables, as in the SMT model; null for other expressions
230-
private static String termName(Expression expression) {
231-
if (expression instanceof Var var)
232-
return var.getName();
233-
if (expression instanceof FunctionInvocation invocation
234-
&& invocation.getArgs().stream().allMatch(Var.class::isInstance)) {
235-
String args = invocation.getArgs().stream().map(Expression::toString).collect(Collectors.joining(", "));
236-
return Utils.getSimpleName(invocation.getName()) + "(" + args + ")";
237-
}
238-
return null;
239-
}
240157
}

‎liquidjava-verifier/src/main/java/liquidjava/diagnostics/errors/StateRefinementError.java‎

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

33
import static liquidjava.rj_language.opt.VCSimplificationUtils.isTrue;
4+
import static liquidjava.rj_language.opt.VCSimplificationUtils.linkedToExpected;
45

56
import java.util.List;
67

@@ -44,7 +45,7 @@ public StateRefinementError(SourcePosition position, SourcePosition declarationP
4445
// Only the facts of found about the objects of expected, and only when one of them tells a state: an alias or a
4546
// guard on another value says nothing about the state expected needs
4647
private static String message(Predicate expected, VCSimplificationResult found) {
47-
Expression shown = RefinementError.linkedToExpected(found.getImplication().toPredicate().getExpression(),
48+
Expression shown = linkedToExpected(found.getImplication().toPredicate().getExpression(),
4849
expected.getExpression());
4950
if (!appliesGhost(shown))
5051
return String.format("expected %s but could not prove it", expected.getExpression().toDisplayString());

‎liquidjava-verifier/src/main/java/liquidjava/rj_language/opt/NeededVariables.java‎

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
package liquidjava.rj_language.opt;
22

33
import static liquidjava.rj_language.opt.VCSimplificationUtils.containsVar;
4+
import static liquidjava.rj_language.opt.VCSimplificationUtils.factsOf;
5+
import static liquidjava.rj_language.opt.VCSimplificationUtils.linkedTerms;
46
import static liquidjava.rj_language.opt.VCSimplificationUtils.termsOf;
57
import static liquidjava.rj_language.opt.VCSimplificationUtils.valueVariablesOf;
68

79
import java.util.HashSet;
10+
import java.util.List;
811
import java.util.Set;
912

1013
import liquidjava.processor.VCImplication;
@@ -15,7 +18,8 @@
1518
* Decides which binders refined only by {@code true} a simplified VC chain keeps for an error. Such a binder states no
1619
* fact, and a variable that another row mentions is already shown there, so its row would only read as if its value
1720
* were true. It is kept only when the error shows a counterexample, expected constrains the variable's value and no
18-
* other row mentions it: the row then shows that nothing is known about a value the error depends on
21+
* other row mentions it: the row then shows that nothing is known about a value the error depends on. It also tells
22+
* which binders expected does not depend on, such as path conditions over other values
1923
*
2024
* @param expectedTerms
2125
* variables and ghost applications of the expected refinement
@@ -68,6 +72,35 @@ public Set<String> trueBindersToKeep(VCImplication chain) {
6872
return keep;
6973
}
7074

75+
/**
76+
* Binders that share no term with expected, neither their variable nor a term of their refinement, directly or
77+
* through other facts of the chain, and whose variable no other row mentions: expected does not depend on them.
78+
* None without expected terms
79+
*/
80+
public Set<String> unlinkedBinders(VCImplication chain) {
81+
Set<String> unlinked = new HashSet<>();
82+
if (expectedTerms.isEmpty())
83+
return unlinked;
84+
List<Expression> facts = factsOf(chain.toPredicate().getExpression());
85+
Set<String> linked = linkedTerms(facts, expectedTerms);
86+
for (VCImplication row = chain; row != null; row = row.getNext())
87+
if (row.hasBinder() && !isLinked(row, linked) && !mentionedByOtherRows(chain, row))
88+
unlinked.add(row.getName());
89+
return unlinked;
90+
}
91+
92+
private static boolean isLinked(VCImplication row, Set<String> linked) {
93+
return linked.contains(row.getName())
94+
|| termsOf(row.getRefinement().getExpression()).stream().anyMatch(linked::contains);
95+
}
96+
97+
private static boolean mentionedByOtherRows(VCImplication chain, VCImplication row) {
98+
for (VCImplication other = chain; other != null; other = other.getNext())
99+
if (other != row && containsVar(other.getRefinement().getExpression(), row.getName()))
100+
return true;
101+
return false;
102+
}
103+
71104
private static Set<String> primitiveBinders(VCImplication chain) {
72105
Set<String> primitives = new HashSet<>();
73106
for (VCImplication current = chain; current != null; current = current.getNext())

‎liquidjava-verifier/src/main/java/liquidjava/rj_language/opt/VCBinderSimplification.java‎

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,24 +34,25 @@ public VCBinderSimplification(NeededVariables needed) {
3434
@Override
3535
public VCImplication apply(VCImplication implication) {
3636
VCImplication cloned = implication.clone();
37-
VCImplication simplified = simplify(cloned, needed.trueBindersToKeep(cloned));
37+
VCImplication simplified = simplify(cloned, needed.trueBindersToKeep(cloned), needed.unlinkedBinders(cloned));
3838
return simplified == null ? cloned : simplified;
3939
}
4040

4141
/**
4242
* Simplifies the first applicable binder in a VC chain
4343
*/
44-
private VCImplication simplify(VCImplication implication, Set<String> keptTrueBinders) {
44+
private VCImplication simplify(VCImplication implication, Set<String> keptTrueBinders,
45+
Set<String> unlinkedBinders) {
4546
if (implication == null)
4647
return null;
4748

4849
if (isFalseBinder(implication))
4950
return collapseFalseBinder(implication);
5051

51-
if (isRemovableBinder(implication, keptTrueBinders))
52+
if (isRemovableBinder(implication, keptTrueBinders, unlinkedBinders))
5253
return removeBinder(implication);
5354

54-
VCImplication next = simplify(implication.getNext(), keptTrueBinders);
55+
VCImplication next = simplify(implication.getNext(), keptTrueBinders, unlinkedBinders);
5556
if (next == null)
5657
return null;
5758

@@ -87,29 +88,39 @@ private VCImplication removeBinder(VCImplication implication) {
8788
/**
8889
* Checks whether a binder can be removed without changing the VC conclusion or hiding what explains the error. A
8990
* binder refined only by true states no fact, so it is removed even when a later implication uses its variable,
90-
* unless the error needs it
91+
* unless the error needs it. A generated path condition that expected does not depend on is removed too
9192
*/
92-
private boolean isRemovableBinder(VCImplication implication, Set<String> keptTrueBinders) {
93+
private boolean isRemovableBinder(VCImplication implication, Set<String> keptTrueBinders,
94+
Set<String> unlinkedBinders) {
9395
if (!implication.hasBinder())
9496
return false;
9597

9698
if (isTrueBinder(implication))
9799
return !keptTrueBinders.contains(implication.getName());
100+
if (isGeneratedBooleanBinder(implication) && unlinkedBinders.contains(implication.getName()))
101+
return true;
98102
return !containsVar(implication.getNext(), implication.getName()) && isUnusedFreshPathBinder(implication);
99103
}
100104

101105
/**
102106
* Checks for a generated boolean path binder refined exactly by itself
103107
*/
104108
private boolean isUnusedFreshPathBinder(VCImplication implication) {
105-
if (!implication.hasNext() || !GeneratedNames.isGenerated(implication.getName())
106-
|| !"boolean".equals(implication.getType().getQualifiedName()))
109+
if (!implication.hasNext() || !isGeneratedBooleanBinder(implication))
107110
return false;
108111

109112
return implication.getRefinement().getExpression()instanceof Var var
110113
&& implication.getName().equals(var.getName());
111114
}
112115

116+
/**
117+
* Checks for a binder of a generated boolean, such as a path condition
118+
*/
119+
private boolean isGeneratedBooleanBinder(VCImplication implication) {
120+
return GeneratedNames.isGenerated(implication.getName())
121+
&& "boolean".equals(implication.getType().getQualifiedName());
122+
}
123+
113124
/**
114125
* Replaces a false binder implication with true
115126
*/

0 commit comments

Comments
 (0)