Skip to content

Commit ceee6be

Browse files
Fold ternaries that a fact of Found decides on an enum value (#531)
Closes #523. ## What Found kept ternaries whose tests a top-level fact of Found already decides, such as `newMode == BORDERLESS` in A1_09. - **New display pass `VCKnownEnumValues`** (step name **Known Enum Values**, runs after Binder, before Folding). For a fact `x == C` or `x != C` with `C` an enum constant, it replaces the comparisons of `x` with constants of the same enum in the other facts by `true`/`false`. Folding and Logical then fold the ternaries and conjunctions. It handles one fact per step and never rewrites the fact it uses. - **`VCLogicalSimplification`** now factors the conjuncts both disjuncts share out of a disjunction: `c && p || c && q` becomes `c && (p || q)`, and `c || c && p` becomes `c`. This shortens the `setVisible(false)` attempt. ## Soundness The change is display only, so the verdict does not change. The pass uses only the constants of the enum types of the chain's binders (`CtEnum` declarations). The solver translates these as distinct `EnumSort` values. `static final` constants, which may share a value, are never used: an `Enum` node with a resolved literal is skipped. Each rewrite is an equivalence, given the fact it uses, and that fact stays in Found. ## A1_09: Found before and after **Exercise as given** (`setUndecorated`, line 49): ``` before: found newMode == BORDERLESS && (previousMode == BORDERLESS && newMode != BORDERLESS ? display(host) == notDisplayable : (previousMode != BORDERLESS && newMode == BORDERLESS ? display(host) == notDisplayable : display(host) == displayable)) but expected display(host) == notDisplayable after: found newMode == BORDERLESS && (previousMode != BORDERLESS ? display(host) == notDisplayable : display(host) == displayable) but expected display(host) == notDisplayable ``` **Partial fix**, adding `else if (newMode == BORDERLESS) host.dispose();` (error moves to the FULLSCREEN fallback, line 64): ``` before: found newMode == FULLSCREEN && (previousMode == BORDERLESS && newMode != BORDERLESS ? display(host) == notDisplayable : (previousMode != BORDERLESS && newMode == BORDERLESS ? display(host) == notDisplayable : (newMode == BORDERLESS ? display(host) == notDisplayable : display(host) == displayable))) but expected display(host) == notDisplayable after: found previousMode == BORDERLESS ? display(host) == notDisplayable : display(host) == displayable but expected display(host) == notDisplayable ``` **`host.dispose()` only in `case BORDERLESS`** (FULLSCREEN fallback, line 63): ``` before: found newMode == FULLSCREEN && (previousMode == BORDERLESS && newMode != BORDERLESS ? display(host) == notDisplayable : (previousMode != BORDERLESS && newMode == BORDERLESS ? display(host) == notDisplayable : display(host) == displayable)) but expected display(host) == notDisplayable after: found previousMode == BORDERLESS ? display(host) == notDisplayable : display(host) == displayable but expected display(host) == notDisplayable ``` **`host.setVisible(false)` before `setUndecorated`** (line 50): ``` before: found (previousMode == BORDERLESS && newMode != BORDERLESS ? display(host@22) == notDisplayable : (previousMode != BORDERLESS && newMode == BORDERLESS ? display(host@22) == notDisplayable : display(host@22) == displayable)) && newMode == BORDERLESS && (display(host@22) == notDisplayable && display(host) == notDisplayable && (previousMode == BORDERLESS && newMode != BORDERLESS ? display(host) == notDisplayable : (previousMode != BORDERLESS && newMode == BORDERLESS ? display(host) == notDisplayable : display(host) == displayable)) || display(host@22) == displayable && display(host) == displayable && (previousMode == BORDERLESS && newMode != BORDERLESS ? display(host) == notDisplayable : (previousMode != BORDERLESS && newMode == BORDERLESS ? display(host) == notDisplayable : display(host) == displayable))) but expected display(host) == notDisplayable after: found (previousMode != BORDERLESS ? display(host@22) == notDisplayable : display(host@22) == displayable) && newMode == BORDERLESS && (previousMode != BORDERLESS ? display(host) == notDisplayable : display(host) == displayable) && (display(host@22) == notDisplayable && display(host) == notDisplayable || display(host@22) == displayable && display(host) == displayable) but expected display(host) == notDisplayable ``` #501 should remove the remaining `host@22` versions. Note: in the two FULLSCREEN attempts, `newMode == FULLSCREEN` is no longer shown. Once the ternary is folded, it no longer mentions `newMode`, so Relevant to Expected drops the fact as unrelated to `display(host)`. The error line is in the FULLSCREEN fallback, so the context is still visible there. In the BORDERLESS case, `newMode == BORDERLESS` stays only because the folded ternary still mentions the constant `BORDERLESS`. ## Tests - `VCKnownEnumValuesTest`: a `!=` fact decides only its own constant; constants read in code, which are `Var`s named after the constant, are decided; a fact of the last row, which may be the conclusion, does not rewrite the premises; and step by step through `simplifyOnce`, a decided ternary folds even after its enum binder is removed. - `VCLogicalSimplificationTest#factorsConjunctsSharedByDisjuncts`. - `VCTestUtils` gains a `Mode` enum binder type. - `mvn test`: 733 tests, 0 failures. VS Code client: the new step name is **Known Enum Values**. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 590268e commit ceee6be

6 files changed

Lines changed: 223 additions & 7 deletions

File tree

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package liquidjava.rj_language.opt;
2+
3+
import static liquidjava.rj_language.opt.VCSimplificationUtils.copyWithRefinement;
4+
import static liquidjava.rj_language.opt.VCSimplificationUtils.factsOf;
5+
6+
import java.util.HashMap;
7+
import java.util.Map;
8+
9+
import liquidjava.processor.VCImplication;
10+
import liquidjava.rj_language.Predicate;
11+
import liquidjava.rj_language.ast.BinaryExpression;
12+
import liquidjava.rj_language.ast.Enum;
13+
import liquidjava.rj_language.ast.Expression;
14+
import liquidjava.rj_language.ast.LiteralBoolean;
15+
import liquidjava.rj_language.ast.Var;
16+
import liquidjava.utils.constants.Formats;
17+
import spoon.reflect.declaration.CtEnum;
18+
import spoon.reflect.declaration.CtEnumValue;
19+
20+
/**
21+
* Decides the comparisons of a variable with an enum constant that a fact about the variable already decides: under
22+
* {@code x == A}, {@code x == A} is true and {@code x == B} false, and under {@code x != A}, {@code x == A} is false,
23+
* so the ternaries they test can fold. Only the constants of the enum types of the chain's binders are used, which the
24+
* solver translates as distinct values, unlike {@code static final} constants that may share a value
25+
*/
26+
public class VCKnownEnumValues implements VCSimplificationPass {
27+
28+
/**
29+
* A comparison {@code variable == constant}, or {@code variable != constant} when not {@code equal}
30+
*/
31+
private record EnumComparison(Expression expression, String variable, String constant, boolean equal) {
32+
}
33+
34+
/** 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+
37+
public VCKnownEnumValues(VCImplication implication) {
38+
for (VCImplication row = implication; row != null; row = row.getNext())
39+
if (row.getType() != null && row.getType().getDeclaration()instanceof CtEnum<?> enumDecl)
40+
for (CtEnumValue<?> value : enumDecl.getEnumValues())
41+
enumTypes.put(String.format(Formats.ENUM, enumDecl.getSimpleName(), value.getSimpleName()),
42+
enumDecl.getQualifiedName());
43+
}
44+
45+
/**
46+
* Uses the first fact that decides some comparison to replace those comparisons by their value
47+
*/
48+
@Override
49+
public VCImplication apply(VCImplication implication) {
50+
for (VCImplication row = implication; row != null; row = row.getNext())
51+
for (Expression fact : factsOf(row.getRefinement().getExpression())) {
52+
EnumComparison known = enumComparison(fact);
53+
if (known == null)
54+
continue;
55+
// the last row may be the conclusion, which the other rows cannot assume
56+
boolean onlyLastRow = row.getNext() == null && row != implication;
57+
VCImplication decided = decideRows(implication, known, onlyLastRow);
58+
if (!decided.equals(implication))
59+
return decided;
60+
}
61+
return implication;
62+
}
63+
64+
private VCImplication decideRows(VCImplication implication, EnumComparison known, boolean onlyLastRow) {
65+
if (implication == null)
66+
return null;
67+
Expression expression = implication.getRefinement().getExpression();
68+
boolean decides = !onlyLastRow || implication.getNext() == null;
69+
VCImplication result = copyWithRefinement(implication,
70+
new Predicate(decides ? decide(expression, known) : expression.clone()));
71+
result.setNext(decideRows(implication.getNext(), known, onlyLastRow));
72+
return result;
73+
}
74+
75+
// The expression with the comparisons known decides replaced by their value, except for its own fact
76+
private Expression decide(Expression expression, EnumComparison known) {
77+
if (expression == known.expression())
78+
return expression.clone();
79+
EnumComparison compared = enumComparison(expression);
80+
if (compared != null && compared.variable().equals(known.variable())
81+
&& enumTypes.get(compared.constant()).equals(enumTypes.get(known.constant()))) {
82+
boolean sameConstant = compared.constant().equals(known.constant());
83+
if (known.equal())
84+
return new LiteralBoolean(sameConstant == compared.equal());
85+
if (sameConstant)
86+
return new LiteralBoolean(!compared.equal());
87+
}
88+
Expression result = expression.clone();
89+
for (int i = 0; i < expression.getChildren().size(); i++)
90+
result.setChild(i, decide(expression.getChildren().get(i), known));
91+
return result;
92+
}
93+
94+
// A comparison of a variable with an enum constant, either way round, or null
95+
private EnumComparison enumComparison(Expression expression) {
96+
if (!(expression instanceof BinaryExpression binary))
97+
return null;
98+
boolean equal = "==".equals(binary.getOperator());
99+
if (!equal && !"!=".equals(binary.getOperator()))
100+
return null;
101+
Expression left = binary.getFirstOperand(), right = binary.getSecondOperand();
102+
String leftConstant = enumConstant(left), rightConstant = enumConstant(right);
103+
if (left instanceof Var variable && leftConstant == null && rightConstant != null)
104+
return new EnumComparison(expression, variable.getName(), rightConstant, equal);
105+
if (right instanceof Var variable && rightConstant == null && leftConstant != null)
106+
return new EnumComparison(expression, variable.getName(), leftConstant, equal);
107+
return null;
108+
}
109+
110+
// The name of the enum constant an expression is, written in code or in a refinement, or null
111+
private String enumConstant(Expression expression) {
112+
String name = null;
113+
if (expression instanceof Var var)
114+
name = var.getName();
115+
else if (expression instanceof Enum en && en.getResolvedLiteral() == null)
116+
name = en.toString();
117+
return enumTypes.containsKey(name) ? name : null;
118+
}
119+
}

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

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import static liquidjava.rj_language.opt.VCSimplificationUtils.isFalse;
44
import static liquidjava.rj_language.opt.VCSimplificationUtils.isTrue;
55

6+
import java.util.List;
7+
68
import liquidjava.rj_language.ast.BinaryExpression;
79
import liquidjava.rj_language.ast.Expression;
810
import liquidjava.rj_language.ast.Ite;
@@ -146,7 +148,30 @@ private Expression simplifyDisjunction(Expression left, Expression right) {
146148
// p || p -> p
147149
if (left.equals(right))
148150
return left.clone();
149-
return null;
151+
return factorCommonConjuncts(left, right);
152+
}
153+
154+
/**
155+
* Factors the conjuncts both disjuncts share out of a disjunction
156+
*/
157+
private Expression factorCommonConjuncts(Expression left, Expression right) {
158+
List<Expression> leftConjuncts = left.getConjuncts(), rightConjuncts = right.getConjuncts();
159+
List<Expression> common = leftConjuncts.stream().filter(rightConjuncts::contains).distinct().toList();
160+
if (common.isEmpty())
161+
return null;
162+
List<Expression> leftRest = leftConjuncts.stream().filter(c -> !common.contains(c)).toList();
163+
List<Expression> rightRest = rightConjuncts.stream().filter(c -> !common.contains(c)).toList();
164+
// c || c && p -> c
165+
if (leftRest.isEmpty() || rightRest.isEmpty())
166+
return conjunction(common);
167+
// c && p || c && q -> c && (p || q)
168+
return new BinaryExpression(conjunction(common), "&&",
169+
new BinaryExpression(conjunction(leftRest), "||", conjunction(rightRest)));
170+
}
171+
172+
private Expression conjunction(List<Expression> conjuncts) {
173+
return conjuncts.stream().map(Expression::clone)
174+
.reduce((first, second) -> new BinaryExpression(first, "&&", second)).get();
150175
}
151176

152177
/**

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

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public static VCSimplificationResult simplifyToFixedPoint(VCImplication implicat
2525
if (implication == null)
2626
return null;
2727

28-
List<VCSimplificationPass> passes = passes(needed);
28+
List<VCSimplificationPass> passes = passes(implication, needed);
2929
DebugLog.simplificationStart(implication);
3030
VCSimplificationResult current = new VCSimplificationResult(implication);
3131
while (true) {
@@ -42,7 +42,9 @@ public static VCSimplificationResult simplifyToFixedPoint(VCImplication implicat
4242
* Applies one simplification step to a VC chain from all available simplification passes
4343
*/
4444
public static VCSimplificationResult simplifyOnce(VCSimplificationResult implication) {
45-
return simplifyOnce(implication, passes(NeededVariables.NONE));
45+
if (implication == null)
46+
return null;
47+
return simplifyOnce(implication, passes(implication.getOriginal().getImplication(), NeededVariables.NONE));
4648
}
4749

4850
private static VCSimplificationResult simplifyOnce(VCSimplificationResult implication,
@@ -55,10 +57,10 @@ private static VCSimplificationResult simplifyOnce(VCSimplificationResult implic
5557
return implication;
5658
}
5759

58-
private static List<VCSimplificationPass> passes(NeededVariables needed) {
60+
private static List<VCSimplificationPass> passes(VCImplication implication, NeededVariables needed) {
5961
return List.of(new VCSubstitution(), new VCFunctionSubstitution(), new VCBinderSimplification(needed),
60-
new VCFolding(), new VCArithmeticSimplification(), new VCLogicalSimplification(),
61-
new VCConstraintSimplification());
62+
new VCKnownEnumValues(implication), new VCFolding(), new VCArithmeticSimplification(),
63+
new VCLogicalSimplification(), new VCConstraintSimplification());
6264
}
6365

6466
/**
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package liquidjava.rj_language.opt;
2+
3+
import static liquidjava.utils.VCTestUtils.*;
4+
5+
import liquidjava.processor.VCImplication;
6+
import liquidjava.rj_language.Predicate;
7+
import liquidjava.rj_language.ast.BinaryExpression;
8+
import liquidjava.rj_language.ast.Ite;
9+
import liquidjava.rj_language.ast.Var;
10+
import org.junit.jupiter.api.Test;
11+
12+
class VCKnownEnumValuesTest {
13+
14+
@Test
15+
void decidesOnlyTheExcludedConstant() {
16+
VCImplication implication = vc("∀mode:Mode. mode != Mode.Photo",
17+
"mode == Mode.Photo ? a : (mode == Mode.Video ? b : c)");
18+
assertSimplificationSteps(new VCKnownEnumValues(implication), implication,
19+
step("mode != Mode.Photo", "false ? a : (mode == Mode.Video ? b : c)"));
20+
}
21+
22+
@Test
23+
void decidesConstantsReadInCode() {
24+
// code reads an enum constant as a variable named after it
25+
Var mode = new Var("mode"), photo = new Var("Mode.Photo"), video = new Var("Mode.Video");
26+
VCImplication implication = vc("∀mode:Mode. true", "true");
27+
implication.setRefinement(new Predicate(new BinaryExpression(mode, "==", photo)));
28+
implication.getNext().setRefinement(
29+
new Predicate(new Ite(new BinaryExpression(video, "==", mode), new Var("a"), new Var("b"))));
30+
assertSimplificationSteps(new VCKnownEnumValues(implication), implication,
31+
step("mode == Photo", "false ? a : b"));
32+
}
33+
34+
@Test
35+
void keepsPremisesTheConclusionWouldDecide() {
36+
VCImplication implication = vc("∀mode:Mode. mode == Mode.Video || p", "mode == Mode.Photo");
37+
assertSimplificationSteps(new VCKnownEnumValues(implication), implication,
38+
step("mode == Mode.Video || p", "mode == Mode.Photo"));
39+
}
40+
41+
@Test
42+
void foldsTheTernariesAFactDecides() {
43+
assertSimplificationSteps(vc("∀mode:Mode. true", "mode == Mode.Video && (mode == Mode.Photo ? a : b)"),
44+
step("mode == Mode.Video && (mode == Mode.Photo ? a : b)"),
45+
step("mode == Mode.Video && (false ? a : b)"), step("mode == Mode.Video && b"));
46+
}
47+
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ void simplifiesDisjunctionWithBooleanLiterals() {
2323
assertSimplificationSteps(simplification, vc("false || x"), step("x"));
2424
}
2525

26+
@Test
27+
void factorsConjunctsSharedByDisjuncts() {
28+
assertSimplificationSteps(simplification, vc("a && c || b && c"), step("c && (a || b)"));
29+
assertSimplificationSteps(simplification, vc("c || c && b"), step("c"));
30+
}
31+
2632
@Test
2733
void simplifiesDoubleNegation() {
2834
assertSimplificationSteps(simplification, vc("!!x"), step("x"));

‎liquidjava-verifier/src/test/java/liquidjava/utils/VCTestUtils.java‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,17 @@
1313
import liquidjava.rj_language.opt.VCSimplificationResult;
1414
import liquidjava.rj_language.parsing.RefinementsParser;
1515
import spoon.Launcher;
16+
import spoon.reflect.declaration.CtEnum;
17+
import spoon.reflect.declaration.CtEnumValue;
18+
import spoon.reflect.factory.Factory;
1619
import spoon.reflect.factory.TypeFactory;
1720
import spoon.reflect.reference.CtTypeReference;
1821

1922
public class VCTestUtils {
2023

21-
private static final TypeFactory TYPE_FACTORY = new Launcher().getFactory().Type();
24+
private static final Factory FACTORY = new Launcher().getFactory();
25+
private static final TypeFactory TYPE_FACTORY = FACTORY.Type();
26+
private static final CtTypeReference<?> MODE = enumType("Mode", "Photo", "Video");
2227

2328
public static VCImplication vc(String... implications) {
2429
VCImplication first = null;
@@ -103,9 +108,21 @@ private static CtTypeReference<?> type(String name) {
103108
return TYPE_FACTORY.BOOLEAN_PRIMITIVE;
104109
if ("Object".equals(name))
105110
return TYPE_FACTORY.OBJECT;
111+
if ("Mode".equals(name))
112+
return MODE;
106113
throw new IllegalArgumentException("Unsupported test type: " + name);
107114
}
108115

116+
private static CtTypeReference<?> enumType(String name, String... values) {
117+
CtEnum<?> enumDecl = FACTORY.Enum().create(name);
118+
for (String value : values) {
119+
CtEnumValue<?> enumValue = FACTORY.Core().createEnumValue();
120+
enumValue.setSimpleName(value);
121+
enumDecl.addEnumValue(enumValue);
122+
}
123+
return enumDecl.getReference();
124+
}
125+
109126
private static String formatRefinement(VCImplication implication) {
110127
return implication.getRefinement().getExpression().toDisplayString();
111128
}

0 commit comments

Comments
 (0)