Skip to content

Commit 93c331c

Browse files
Drop path conditions that expected does not depend on from Found (#487)
Refs #375. Follow-up to #476 and #485, approved by the PM. The simplified Found (the last step the VS Code panel shows) no longer lists path conditions that Expected does not depend on. ## Problem #485 trims the message to the facts linked to Expected. The panel's last step still listed every `if`/loop guard, so the message and the panel disagreed. In the study exercises: - A1_56: the message is `expected redoable(undoManager) but could not prove it`, but the panel still shows two `switch` guards. - A1_01: the panel shows `baseline || extended`, which says nothing about `wParam`. ## Rule A binder of a generated boolean (a path condition such as `#if_condition`) is dropped when both hold: - **Not linked to Expected:** neither its variable nor any term of its refinement shares a term with Expected, directly or through other facts of Found. - **Not mentioned by another row**, so no remaining row refers to a variable whose binder was dropped. Rows that are not generated booleans are never dropped by this rule; the message still trims those (#485), so the message is always a subset of the panel's last step. Without expected terms (`NeededVariables.NONE`), nothing is dropped. Each removal is a step of the existing `Binder` pass. **Stepping back in the panel still shows the original facts:** the root of the `VCSimplificationResult` origin chain keeps every dropped guard. I checked this for the A1_56 shape (each guard is its own `Binder` step and the root has both), and `TestGeneratedNames` now reads the original Found, so it checks it on every run. **One source for the linking.** `factsOf`, `linkedTerms`, `termsOf`, `termName` and `linkedToExpected` moved from `RefinementError` to `VCSimplificationUtils`, so simplification no longer needs anything from diagnostics. `RefinementError` (message and counterexample), `StateRefinementError` (message) and `NeededVariables` (panel) now all use them. `VCSimplificationUtils` had its own `termsOf` that named applications by `toString()`. Now applications over variables are named as in the SMT model (`size(x)`), which the counterexample needs, and the other applications are named as written (`f(1)`). All tests pass with that change. The "could not prove it" frame, the shortcut that keeps Found as written when every fact is linked, and the counterexample are unchanged. ## Before / after (the panel's last step) The rows below are the simplified chain exactly as `VCImplication.toString()` prints it, from `origin/main` and this branch. A1_56 (JabRef `EditAction`, line 69). The message is unchanged on both: `expected redoable(undoManager¹⁴¹) but could not prove it`. ``` before: ∀##if_condition_122:boolean, !##operation_result_121 => ∀##if_condition_136:boolean, !##operation_result_134 || ##operation_result_135 => ∀#undoManager_141:CountingUndoManager, #undoManager_141 == ##getUndoManager_result_139 after: ∀#undoManager_141:CountingUndoManager, #undoManager_141 == ##getUndoManager_result_139 ``` A1_01 (dcm4che `SlideRecoder`, line 131). The message is unchanged: `found startTiling(wParam²⁹⁷) && startCompression(wParam²⁹⁷) but expected compressionExplicit(wParam²⁹⁷)`. ``` before: ∀##if_condition_302:boolean, #baseline_298 || #extended_299 => ∀#wParam_297:ImageWriteParam, startTiling(#wParam_297) && startCompression(#wParam_297) after: ∀#wParam_297:ImageWriteParam, startTiling(#wParam_297) && startCompression(#wParam_297) ``` A guard Expected depends on stays (`ErrorCounterexampleLinkedPathCondition`). The message is unchanged: `!(port < 0) && timeout¹⁰ == port - 1 is not a subtype of timeout¹⁰ >= 0`. ``` before: ∀##if_condition_0:boolean, !retries > 3 => ∀##if_condition_4:boolean, !port < 0 => ∀#timeout_10:int, #timeout_10 == port - 1 after: ∀##if_condition_4:boolean, !port < 0 => ∀#timeout_10:int, #timeout_10 == port - 1 ``` `retries > 3` is dropped. `port < 0` is linked through `timeout == port - 1` and stays. In all three cases the original step still shows every guard. No screenshots: the `liquidjava-dev` harness has no panel-capture script yet, and the extension server would need this verifier built into it, so I skipped them. ## Tests `VCBinderSimplificationTest` has 4 new tests. Each fails without the change. - `removesPathConditionsExpectedDoesNotDependOn`: the A1_56 shape, including a negated disjunction. - `keepsPathConditionExpectedDependsOnThroughAnotherRow`: a guard linked through `y == x - 10` stays while an unlinked one goes. - `keepsPathConditionAnotherRowMentions`: a guard whose variable a ternary row uses stays. - `keepsPathConditionsExpectedNames`: a guard whose own variable Expected names stays, and so does a guard on a call Expected names (`f(1)`, Codex round 1). **Changed:** `TestGeneratedNames` now reads the original Found instead of the simplified one, because the simplified one no longer has the guards whose names it checks. Only line 54 changes: it is now `isNull == #operation_result && #if_condition == isNull && #if_condition && x == 0`, the alias as written before simplification. `mvn test`: **702 tests, 0 failures**. **Size:** +192 / −108 over 7 files. About 90 of the removed lines are the helpers that moved out of `RefinementError`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 9aa2467 commit 93c331c

7 files changed

Lines changed: 192 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)