Skip to content

Commit e8660d1

Browse files
Do not link Found facts through an enum constant they only compare with (#534)
Stacked on #531 (base `feat/found-fold-decided-ternary`). Part of #523: the last step of folding A1_09's Found. ## What After #531 folds A1_09's ternary under `newMode == BORDERLESS`, the ternary no longer mentions `newMode`, yet Found still shows `newMode == BORDERLESS`. The cause is in **Relevant to Expected** (`VCRelevantFacts.addSymbols`, `VCRelevantFacts.java:125` on #531): every `Var` is a symbol, and an enum constant read in code is a `Var` named `DisplayMode.BORDERLESS`. So `newMode == BORDERLESS` and the folded ternary stay linked through the constant. (An enum constant written in a refinement is an `Enum` node, which already linked nothing.) A constant is a fixed value: two facts that only compare values with the same constant share no unknown. The relevance pass now ignores enum constants as symbols, **except** when a function or alias takes the constant, or a value one takes is compared with it. There, equality through the constant does relate facts (`x == A && state(A) == shown` gives `state(x) == shown`). This exception comes from Codex's round-1 counterexample. - `VCRelevantFacts` takes the enum constants of the chain. `VCKnownEnumValues.enumConstants` exposes the set #531 already builds. - There is still one relevance run and no new pass. The step order is unchanged: Substitution, Function Substitution, Binder, **Known Enum Values**, Folding, Arithmetic, Logical, Constraint to a fixed point, then **Relevant to Expected** once, then `ErrorMessage`'s fact selection. ## Soundness Display only; the verdict is unchanged. The relevance pass drops only facts D whose unknowns are disjoint from Expected's and from the kept facts S. Enum constants are distinct `EnumSort` values. When no function takes a value equal to a shared constant, a model of S and a model of D combine. So the premises imply S, and S fails Expected exactly when the premises do. `static final` constants are not in the set. ## A1_09 (the only study task that changes) ``` #531: found newMode == BORDERLESS && (previousMode != BORDERLESS ? display(host) == notDisplayable : display(host) == displayable) but expected display(host) == notDisplayable this: found previousMode != BORDERLESS ? display(host) == notDisplayable : display(host) == displayable but expected display(host) == notDisplayable ``` The other seven study tasks have the same output as on #531, byte for byte, with one error each on `reportedLine`. All 287 `testSuite` fixtures have the same headlines as on #531. The formatter writes a Found that is a single ternary without parentheses, as in #531's FULLSCREEN case. That is outside this PR. ## Tests - `VCSimplificationTest#relevantStepDoesNotLinkFactsThroughAConstant` (fails without the change). - `VCSimplificationTest#relevantStepLinksFactsThroughAConstantAnObjectEquals` (Codex's counterexample; fails with the naive "drop every constant" version). - `mvn test`: 735 tests, 0 failures. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent cfe9405 commit e8660d1

4 files changed

Lines changed: 119 additions & 13 deletions

File tree

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import liquidjava.rj_language.ast.Ite;
2424
import liquidjava.rj_language.ast.Var;
2525
import liquidjava.rj_language.opt.NeededVariables;
26+
import liquidjava.rj_language.opt.VCKnownEnumValues;
2627
import liquidjava.rj_language.opt.VCRelevantFacts;
2728
import liquidjava.rj_language.opt.VCSimplification;
2829
import liquidjava.rj_language.opt.VCSimplificationResult;
@@ -466,8 +467,8 @@ protected void throwStateRefinementError(SourcePosition position, List<SourcePos
466467
TranslationTable map = new TranslationTable();
467468
VCImplication foundState = buildPremiseChain(map, expected, found);
468469
VCSimplificationResult simplified = foundState.simplify(NeededVariables.forStateError(expected));
469-
VCSimplificationResult shown = VCSimplification.simplifyOnce(simplified,
470-
new VCRelevantFacts(expected.getExpression(), this::ghostFunctionsOf));
470+
VCSimplificationResult shown = VCSimplification.simplifyOnce(simplified, new VCRelevantFacts(
471+
expected.getExpression(), this::ghostFunctionsOf, VCKnownEnumValues.enumConstants(foundState)));
471472
StateRefinementError error = new StateRefinementError(position, declarationPositions, expected, shown, map,
472473
customMessage, hints, category, context,
473474
e -> StateValues.toSetForm(e, this.context.getGhostStates(), this.context.getGhosts()),

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import java.util.HashMap;
77
import java.util.Map;
8+
import java.util.Set;
89

910
import liquidjava.processor.VCImplication;
1011
import liquidjava.rj_language.Predicate;
@@ -32,14 +33,25 @@ private record EnumComparison(Expression expression, String variable, String con
3233
}
3334

3435
/** The enum type of each enum constant, by the name the solver translates the constant from */
35-
private final Map<String, String> enumTypes = new HashMap<>();
36+
private final Map<String, String> enumTypes;
3637

3738
public VCKnownEnumValues(VCImplication implication) {
39+
this.enumTypes = enumTypesOf(implication);
40+
}
41+
42+
/** The names the constants of the enum types of the chain's binders are translated from, which denote values */
43+
public static Set<String> enumConstants(VCImplication implication) {
44+
return enumTypesOf(implication).keySet();
45+
}
46+
47+
private static Map<String, String> enumTypesOf(VCImplication implication) {
48+
Map<String, String> enumTypes = new HashMap<>();
3849
for (VCImplication row = implication; row != null; row = row.getNext())
3950
if (row.getType() != null && row.getType().getDeclaration()instanceof CtEnum<?> enumDecl)
4051
for (CtEnumValue<?> value : enumDecl.getEnumValues())
4152
enumTypes.put(String.format(Formats.ENUM, enumDecl.getSimpleName(), value.getSimpleName()),
4253
enumDecl.getQualifiedName());
54+
return enumTypes;
4355
}
4456

4557
/**

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

Lines changed: 58 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
import liquidjava.processor.VCImplication;
1717
import liquidjava.rj_language.Predicate;
18+
import liquidjava.rj_language.ast.AliasInvocation;
1819
import liquidjava.rj_language.ast.BinaryExpression;
1920
import liquidjava.rj_language.ast.Expression;
2021
import liquidjava.rj_language.ast.FunctionInvocation;
@@ -27,22 +28,28 @@
2728
* value it constrains. Different ghost functions, or the same one over different objects, are different solver terms,
2829
* so facts that share no symbol with expected, directly or through other facts, are independent of it and are left out.
2930
* An object whose value a fact constrains, such as {@code x == y}, or that an alias or unknown function takes, is one
30-
* symbol with all its ghost functions, so its facts are never split. Nothing changes when every fact or none is
31-
* relevant
31+
* symbol with all its ghost functions, so its facts are never split. A constant, such as an enum constant read in code,
32+
* is a fixed value, so facts that only compare values with it share no symbol through it; it stays one when a function
33+
* or alias takes it or a chain of comparisons relates it to a value one takes, as equal objects have equal ghost
34+
* values. Nothing changes when every fact or none is relevant
3235
*/
3336
public class VCRelevantFacts implements VCSimplificationPass {
3437

3538
private final Expression expected;
3639
private final Function<String, Set<String>> ghostFunctionsOf;
40+
private final Set<String> constants;
3741

3842
/**
3943
* @param ghostFunctionsOf
4044
* the ghost functions an invocation name stands for: a state's state set function, or the ghost itself;
4145
* none for any other name
46+
* @param constants
47+
* the variable names that denote constants, such as enum constants
4248
*/
43-
public VCRelevantFacts(Expression expected, Function<String, Set<String>> ghostFunctionsOf) {
49+
public VCRelevantFacts(Expression expected, Function<String, Set<String>> ghostFunctionsOf, Set<String> constants) {
4450
this.expected = expected;
4551
this.ghostFunctionsOf = ghostFunctionsOf;
52+
this.constants = constants;
4653
}
4754

4855
@Override
@@ -60,8 +67,10 @@ public VCImplication apply(VCImplication implication) {
6067
addSymbols(expected, new HashMap<>(), values);
6168
allFacts.forEach(fact -> addSymbols(fact, new HashMap<>(), values));
6269

63-
Set<String> linked = symbolsOf(expected, values);
64-
List<Set<String>> unlinked = new ArrayList<>(allFacts.stream().map(fact -> symbolsOf(fact, values)).toList());
70+
Set<String> valueConstants = valueConstants(expected, allFacts);
71+
Set<String> linked = symbolsOf(expected, values, valueConstants);
72+
List<Set<String>> unlinked = new ArrayList<>(
73+
allFacts.stream().map(fact -> symbolsOf(fact, values, valueConstants)).toList());
6574
boolean changed = true;
6675
while (changed) {
6776
changed = false;
@@ -75,8 +84,11 @@ public VCImplication apply(VCImplication implication) {
7584
}
7685
}
7786

78-
List<List<Expression>> kept = rowFacts.stream().map(facts -> facts.stream()
79-
.filter(fact -> symbolsOf(fact, values).stream().anyMatch(linked::contains)).toList()).toList();
87+
List<List<Expression>> kept = rowFacts.stream()
88+
.map(facts -> facts.stream()
89+
.filter(fact -> symbolsOf(fact, values, valueConstants).stream().anyMatch(linked::contains))
90+
.toList())
91+
.toList();
8092
int relevant = kept.stream().mapToInt(List::size).sum();
8193
if (relevant == 0 || relevant == allFacts.size())
8294
return implication;
@@ -99,13 +111,50 @@ private VCImplication rebuild(VCImplication row, Iterator<List<Expression>> all,
99111
return result;
100112
}
101113

114+
// The constants no function or alias takes and that no chain of comparisons relates to a value one takes
115+
private Set<String> valueConstants(Expression expected, List<Expression> facts) {
116+
Set<String> objects = new HashSet<>();
117+
addArguments(expected, objects);
118+
facts.forEach(fact -> addArguments(fact, objects));
119+
List<Expression> all = new ArrayList<>(facts);
120+
all.add(expected);
121+
while (all.stream().anyMatch(expression -> addComparedWithObjects(expression, objects)))
122+
;
123+
Set<String> valueConstants = new HashSet<>(constants);
124+
valueConstants.removeAll(objects);
125+
return valueConstants;
126+
}
127+
128+
private static void addArguments(Expression expression, Set<String> objects) {
129+
if (expression instanceof FunctionInvocation invocation)
130+
invocation.getArgs().forEach(argument -> objects.addAll(variablesOf(argument)));
131+
else if (expression instanceof AliasInvocation alias)
132+
alias.getArgs().forEach(argument -> objects.addAll(variablesOf(argument)));
133+
expression.getChildren().forEach(child -> addArguments(child, objects));
134+
}
135+
136+
// Adds to objects the variables a comparison relates to one of them; true when it added any
137+
private static boolean addComparedWithObjects(Expression expression, Set<String> objects) {
138+
boolean added = false;
139+
if (expression instanceof BinaryExpression binary
140+
&& ("==".equals(binary.getOperator()) || "!=".equals(binary.getOperator()))) {
141+
List<String> compared = variablesOf(binary);
142+
if (compared.stream().anyMatch(objects::contains))
143+
added = objects.addAll(compared);
144+
}
145+
for (Expression child : expression.getChildren())
146+
added |= addComparedWithObjects(child, objects);
147+
return added;
148+
}
149+
102150
// The symbols an expression constrains: f(x) for a ghost function over an object, or the object itself when values
103-
// has it, and the variables whose value it constrains
104-
private Set<String> symbolsOf(Expression expression, Set<String> values) {
151+
// has it, and the variables whose value it constrains, except the value constants
152+
private Set<String> symbolsOf(Expression expression, Set<String> values, Set<String> valueConstants) {
105153
Map<String, String> applications = new HashMap<>();
106154
Set<String> symbols = new HashSet<>();
107155
addSymbols(expression, applications, symbols);
108156
applications.forEach((application, object) -> symbols.add(values.contains(object) ? object : application));
157+
symbols.removeAll(valueConstants);
109158
return symbols;
110159
}
111160

‎liquidjava-verifier/src/test/java/liquidjava/rj_language/opt/VCSimplificationTest.java‎

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,12 @@
99
import org.junit.jupiter.api.AfterEach;
1010
import org.junit.jupiter.api.Test;
1111

12+
import liquidjava.processor.VCImplication;
1213
import liquidjava.processor.context.Context;
14+
import liquidjava.rj_language.Predicate;
15+
import liquidjava.rj_language.ast.BinaryExpression;
16+
import liquidjava.rj_language.ast.Ite;
17+
import liquidjava.rj_language.ast.Var;
1318
import liquidjava.rj_language.parsing.RefinementsParser;
1419
import liquidjava.utils.TestUtils;
1520

@@ -190,7 +195,7 @@ private static VCRelevantFacts relevantTo(String expected) {
190195
Map<String, String> stateSets = Map.of("startTiling", "state1", "tilingSet", "state1", "state1", "state1",
191196
"compressionSet", "state2", "state2", "state2");
192197
return new VCRelevantFacts(RefinementsParser.createAST(expected, ""),
193-
name -> stateSets.containsKey(name) ? Set.of(stateSets.get(name)) : Set.of());
198+
name -> stateSets.containsKey(name) ? Set.of(stateSets.get(name)) : Set.of(), Set.of("Mode.Photo"));
194199
}
195200

196201
@Test
@@ -200,6 +205,45 @@ void relevantStepKeepsEveryStateSetOfAnObjectComparedAsAValue() {
200205
step("compressionSet(q) && startTiling(p)", "p == q"));
201206
}
202207

208+
@Test
209+
void relevantStepDoesNotLinkFactsThroughAConstant() {
210+
// code reads an enum constant as a variable named after it
211+
Var photo = new Var("Mode.Photo");
212+
VCImplication implication = vc("∀n:Mode. true", "true");
213+
implication.setRefinement(new Predicate(new BinaryExpression(new Var("n"), "==", photo)));
214+
implication.getNext()
215+
.setRefinement(new Predicate(new Ite(new BinaryExpression(new Var("m"), "==", photo),
216+
RefinementsParser.createAST("startTiling(p)", ""),
217+
RefinementsParser.createAST("compressionSet(p)", ""))));
218+
assertSimplificationSteps(relevantTo("tilingSet(p)"), implication,
219+
step("m == Photo ? startTiling(p) : compressionSet(p)"));
220+
}
221+
222+
@Test
223+
void relevantStepLinksFactsThroughAConstantAnObjectEquals() {
224+
Var photo = new Var("Mode.Photo");
225+
VCImplication implication = vc("∀p:Mode. true", "true");
226+
implication.setRefinement(new Predicate(new BinaryExpression(new Var("p"), "==", photo)));
227+
implication.getNext().setRefinement(new Predicate(
228+
new BinaryExpression(RefinementsParser.createAST("startTiling(q)", ""), "&&", new Var("ready"))));
229+
implication.getNext().getRefinement().getExpression().getChildren().get(0).setChild(0, photo);
230+
assertSimplificationSteps(relevantTo("tilingSet(p)"), implication, step("p == Photo", "startTiling(Photo)"));
231+
}
232+
233+
@Test
234+
void relevantStepLinksFactsThroughAConstantAChainOfEqualitiesRelatesToAnObject() {
235+
// p == r == Photo == s == q, so the facts about q constrain tilingSet(p)
236+
Var photo = new Var("Mode.Photo");
237+
VCImplication implication = vc("∀p:Mode. true", "true");
238+
implication.setRefinement(new Predicate(new BinaryExpression(RefinementsParser.createAST("p == r", ""), "&&",
239+
new BinaryExpression(new Var("r"), "==", photo))));
240+
implication.getNext()
241+
.setRefinement(new Predicate(new BinaryExpression(new BinaryExpression(new Var("s"), "==", photo), "&&",
242+
RefinementsParser.createAST("s == q && startTiling(q)", ""))));
243+
assertSimplificationSteps(relevantTo("tilingSet(p)"), implication,
244+
step("p == r && r == Photo", "s == Photo && s == q && startTiling(q)"));
245+
}
246+
203247
@Test
204248
void relevantStepKeepsFactsAnUnknownFunctionTakesTheObjectOf() {
205249
assertSimplificationSteps(relevantTo("tilingSet(p)"), vc("compressionSet(p) && ready(p)", "startTiling(p)"),

0 commit comments

Comments
 (0)