Skip to content

Commit faaeb06

Browse files
keep translation from final to literal value
1 parent 2e77dfa commit faaeb06

7 files changed

Lines changed: 82 additions & 13 deletions

File tree

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,10 @@ public String getCounterExampleString() {
4646

4747
List<String> foundVarNames = new ArrayList<>();
4848
found.getValue().getVariableNames(foundVarNames);
49-
List<String> foundAssignments = found.getValue().getConjuncts().stream().map(Expression::toString).toList();
49+
// also keep resolved static-final constants (e.g. Integer.MAX_VALUE) referenced by either side of the
50+
// subtyping check, so the counterexample maps the symbolic name back to its compile-time value
51+
found.getValue().getResolvedConstantNames(foundVarNames);
52+
expected.getValue().getResolvedConstantNames(foundVarNames);
5053
String counterexampleString = counterexample.assignments().stream()
5154
// only include variables that appear in the found value and are not already fixed there
5255
.filter(a -> CommandLineLauncher.cmdArgs.debugMode || (foundVarNames.contains(a.first())

liquidjava-verifier/src/main/java/liquidjava/rj_language/Predicate.java

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,19 +81,21 @@ public Predicate(String ref, CtElement element, String prefix) throws LJError {
8181
}
8282

8383
/**
84-
* Walks {@code root}, replacing {@link Enum} nodes that resolve (via reflection, java.lang fallback, or imports
84+
* Walks {@code root}, decorating {@link Enum} nodes that resolve (via reflection, java.lang fallback, or imports
8585
* declared in {@code context}'s compilation unit) to a {@code static final} primitive/String constant with the
86-
* corresponding literal node. User-defined enums and unresolvable references are left untouched.
86+
* corresponding literal expression via {@link Enum#setResolvedLiteral}. The AST shape is preserved, so error
87+
* messages and counterexamples can render the symbolic {@code Type.CONST} form; the SMT translator emits the
88+
* literal binding axiom from the decoration. User-defined enums and unresolvable-but-known-type references are left
89+
* untouched (the SMT side handles them as user enum constants).
8790
*/
8891
private static Expression resolveStaticFinalConstants(Expression root, CtElement context) throws LJError {
8992
List<Enum> enums = new ArrayList<>();
9093
collectEnums(root, enums);
91-
Expression e = root;
9294
for (Enum en : enums) {
9395
Object v = StaticConstants.resolve(en.getTypeName(), en.getConstName(), context);
9496
Predicate lit = StaticConstants.asLiteralPredicate(v);
9597
if (lit != null) {
96-
e = e.substitute(en, lit.getExpression());
98+
en.setResolvedLiteral(lit.getExpression());
9799
continue;
98100
}
99101
if (StaticConstants.userTypeExists(en.getTypeName(), context))
@@ -108,7 +110,7 @@ private static Expression resolveStaticFinalConstants(Expression root, CtElement
108110
en.getTypeName(), en.getConstName(), hint),
109111
pos);
110112
}
111-
return e;
113+
return root;
112114
}
113115

114116
private static void collectEnums(Expression e, List<Enum> out) {

liquidjava-verifier/src/main/java/liquidjava/rj_language/ast/Enum.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@ public class Enum extends Expression {
99

1010
private final String typeName;
1111
private final String constName;
12+
/**
13+
* If this {@code Type.CONST} reference resolved to a Java {@code static final} primitive/String constant, the
14+
* corresponding RJ literal expression is stashed here so the SMT translator can emit a binding axiom
15+
* ({@code Type.CONST == literalValue}) while preserving the symbolic name in the AST. {@code null} for user-defined
16+
* enum constants and for unresolvable references.
17+
*/
18+
private Expression resolvedLiteral;
1219

1320
public Enum(String typeName, String constName) {
1421
this.typeName = typeName;
@@ -23,6 +30,14 @@ public String getConstName() {
2330
return constName;
2431
}
2532

33+
public Expression getResolvedLiteral() {
34+
return resolvedLiteral;
35+
}
36+
37+
public void setResolvedLiteral(Expression resolvedLiteral) {
38+
this.resolvedLiteral = resolvedLiteral;
39+
}
40+
2641
@Override
2742
public <T> T accept(ExpressionVisitor<T> visitor) throws LJError {
2843
return visitor.visitEnum(this);
@@ -69,6 +84,8 @@ public boolean equals(Object obj) {
6984

7085
@Override
7186
public Expression clone() {
72-
return new Enum(typeName, constName);
87+
Enum c = new Enum(typeName, constName);
88+
c.resolvedLiteral = resolvedLiteral == null ? null : resolvedLiteral.clone();
89+
return c;
7390
}
7491
}

liquidjava-verifier/src/main/java/liquidjava/rj_language/ast/Expression.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,18 @@ public abstract class Expression {
2525

2626
public abstract void getVariableNames(List<String> toAdd);
2727

28+
/**
29+
* Collect dotted names ({@code Type.CONST}) of {@link Enum} nodes that have been resolved to a Java
30+
* {@code static final} literal value. Used by the counterexample renderer to keep these constants in the displayed
31+
* model even though they are not program variables.
32+
*/
33+
public void getResolvedConstantNames(List<String> toAdd) {
34+
if (this instanceof liquidjava.rj_language.ast.Enum en && en.getResolvedLiteral() != null)
35+
toAdd.add(en.getTypeName() + "." + en.getConstName());
36+
for (Expression c : getChildren())
37+
c.getResolvedConstantNames(toAdd);
38+
}
39+
2840
public abstract void getStateInvocations(List<String> toAdd, List<String> all);
2941

3042
public abstract boolean isBooleanTrue();

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,8 @@ public Expr<?> visitUnaryExpression(UnaryExpression exp) throws LJError {
124124

125125
@Override
126126
public Expr<?> visitEnum(Enum en) throws LJError {
127+
if (en.getResolvedLiteral() != null)
128+
return ctx.makeStaticConstant(en.getTypeName(), en.getConstName(), en.getResolvedLiteral().accept(this));
127129
return ctx.makeEnum(en.getTypeName(), en.getConstName());
128130
}
129131
}

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,17 @@ public class TranslatorToZ3 implements AutoCloseable {
4242
private final Map<String, FuncDecl<?>> funcTranslation = new HashMap<>();
4343
private final Map<String, Expr<?>> funcAppTranslation = new HashMap<>();
4444
private final Map<Expr<?>, String> exprToNameTranslation = new HashMap<>();
45+
/**
46+
* Z3 constants for resolved {@code static final} references (e.g. {@code Integer.MAX_VALUE}). Keyed by the dotted
47+
* symbolic name so the same constant is reused across the query and the binding axiom is emitted once.
48+
*/
49+
private final Map<String, Expr<?>> staticConstantTranslation = new HashMap<>();
50+
/**
51+
* Equality axioms binding each {@link #staticConstantTranslation} entry to its compile-time literal value.
52+
* Conjoined into every solver built by {@link #makeSolverForExpression} so the constant is always pinned but still
53+
* appears symbolically in counterexample models.
54+
*/
55+
private final List<BoolExpr> staticConstantAxioms = new ArrayList<>();
4556

4657
public TranslatorToZ3(liquidjava.processor.context.Context context) {
4758
TranslatorContextToZ3.translateVariables(z3, context.getContext(), varTranslation);
@@ -54,6 +65,8 @@ public TranslatorToZ3(liquidjava.processor.context.Context context) {
5465
@SuppressWarnings("unchecked")
5566
public Solver makeSolverForExpression(Expr<?> e) {
5667
Solver solver = z3.mkSolver();
68+
for (BoolExpr axiom : staticConstantAxioms)
69+
solver.add(axiom);
5770
solver.add((BoolExpr) e);
5871
return solver;
5972
}
@@ -128,6 +141,24 @@ public Expr<?> makeEnum(String enumTypeName, String enumConstantName) throws LJE
128141
return getVariableTranslation(varName);
129142
}
130143

144+
/**
145+
* Declare (or look up) a Z3 constant named {@code Type.CONST} for a resolved Java {@code static final} reference,
146+
* and emit a single equality axiom binding it to its compile-time literal value. Subsequent calls with the same
147+
* name reuse the existing constant. The axiom is added to every solver built via {@link #makeSolverForExpression},
148+
* so the constant always evaluates to its literal value while still appearing symbolically in error messages and
149+
* counterexample models.
150+
*/
151+
public Expr<?> makeStaticConstant(String typeName, String constName, Expr<?> literalValue) {
152+
String name = String.format(Formats.ENUM, typeName, constName);
153+
Expr<?> cached = staticConstantTranslation.get(name);
154+
if (cached != null)
155+
return cached;
156+
Expr<?> constant = z3.mkConst(name, literalValue.getSort());
157+
staticConstantTranslation.put(name, constant);
158+
staticConstantAxioms.add(z3.mkEq(constant, literalValue));
159+
return constant;
160+
}
161+
131162
public Expr<?> makeFunctionInvocation(String name, Expr<?>[] params) throws LJError {
132163
if (name.equals("addToIndex"))
133164
return makeStore(params);

liquidjava-verifier/src/main/java/liquidjava/utils/StaticConstants.java

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,10 @@ private StaticConstants() {
2727
* Resolve a Spoon {@code static final} field reference to its compile-time value, so reads like
2828
* {@code Integer.MAX_VALUE} or {@code MyConfig.LIMIT} fold to literals before SMT translation.
2929
*
30-
* <p>Tries the source AST first ({@link CtLiteral} initializer in Spoon's model), then reflection via
31-
* {@link CtFieldReference#getActualField()} + {@link #readStaticFinal}. Returns {@code null} if the field
32-
* isn't static-final, has a non-literal initializer, or any lookup step fails.
30+
* <p>
31+
* Tries the source AST first ({@link CtLiteral} initializer in Spoon's model), then reflection via
32+
* {@link CtFieldReference#getActualField()} + {@link #readStaticFinal}. Returns {@code null} if the field isn't
33+
* static-final, has a non-literal initializer, or any lookup step fails.
3334
*
3435
* @see #resolve(String, String, CtElement) sibling for refinement-string {@code Type.CONST} references
3536
*/
@@ -146,9 +147,10 @@ public static Predicate asLiteralPredicate(Object value) {
146147
* {@code lookup("java.lang.Integer", "MAX_VALUE")} returns {@code 2147483647}, which is how
147148
* {@code Integer.MAX_VALUE} gets baked into the AST before SMT translation.
148149
*
149-
* <p>Returns {@code null} on any failure ({@link ClassNotFoundException}, {@link NoSuchFieldException},
150-
* {@link LinkageError}, {@link SecurityException}, or non-static-final) so callers can fall through to the
151-
* next resolution strategy without a try/catch. Only public fields are visible to {@link Class#getField}.
150+
* <p>
151+
* Returns {@code null} on any failure ({@link ClassNotFoundException}, {@link NoSuchFieldException},
152+
* {@link LinkageError}, {@link SecurityException}, or non-static-final) so callers can fall through to the next
153+
* resolution strategy without a try/catch. Only public fields are visible to {@link Class#getField}.
152154
*/
153155
private static Object lookup(String className, String fieldName) {
154156
try {

0 commit comments

Comments
 (0)