Skip to content

Commit 0b45301

Browse files
Keep a true binder only when it is linked to Expected, and name writes to other objects' fields after them
A binder refined only by true now stays in Found when a fact linked to Expected (sharing a variable or ghost application, directly or through other facts, with negated disjunctions split first) constrains its value, or, in a refinement error, when Expected constrains it. The rule no longer depends on the counterexample, so it is the same for refinement and state errors. A kept true binder moves before the first row that uses it. A write to another object's field is checked under that object's name (other.level) instead of this.level. The class-based disambiguation of fields is removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 9c50b4b commit 0b45301

17 files changed

Lines changed: 498 additions & 117 deletions

File tree

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package testSuite;
2+
3+
import liquidjava.specification.Refinement;
4+
5+
// a write to another object's field is checked against the field's refinement under that object's name (#457)
6+
public class ErrorOtherObjectFieldWrite {
7+
@Refinement("_ >= 0")
8+
int level;
9+
10+
void setOther(ErrorOtherObjectFieldWrite other) {
11+
other.level = -1; // Expect: Refinement Error
12+
}
13+
14+
void decrementOther(ErrorOtherObjectFieldWrite other) {
15+
other.level--; // Expect: Refinement Error
16+
}
17+
18+
void setOwn() {
19+
this.level = -1; // Expect: Refinement Error
20+
}
21+
}

‎liquidjava-example/src/main/java/testSuite/classes/found_true_binders_error/Basket.java‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,4 +96,25 @@ String pick() {
9696
return names.get(new Random().nextInt(names.size())); // Expect: Refinement Error
9797
}
9898
}
99+
100+
// a guard on a parameter unrelated to the expected one
101+
void guarded(int port, int t) {
102+
if (port < 0)
103+
return;
104+
positive(t); // Expect: Refinement Error
105+
}
106+
107+
// one guard on two flags unrelated to the expected parameter
108+
void flags(boolean p, boolean q, int t) {
109+
if (p || q)
110+
return;
111+
positive(t); // Expect: Refinement Error
112+
}
113+
114+
// the guard of a loop uses its variable before the variable's row
115+
void count(int start) {
116+
for (int i = start; i < served; i++) {
117+
positive(i); // Expect: Refinement Error
118+
}
119+
}
99120
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ private <T, A extends T> void visitAssignment(CtAssignment<T, A> assignment) thr
255255
} else if (ex instanceof CtFieldWrite<?> fw) {
256256
CtFieldReference<?> cr = fw.getVariable();
257257
CtField<?> f = fw.getVariable().getDeclaration();
258-
String updatedVarName = Utils.qualifyFieldName(cr);
258+
String updatedVarName = Utils.writtenFieldName(fw);
259259
// a field declared outside the analyzed sources (e.g. a library field) has no declared refinement: the
260260
// write stands in for its declaration
261261
checkAssignment(updatedVarName, cr.getType(), ex, assignment.getAssignment(), assignment,

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ public void processSubtyping(Predicate expectedType, List<GhostState> list, CtEl
8484
DebugLog.smtResult(result);
8585
if (result.isError()) {
8686
throw new RefinementError(element.getPosition(), declarationPosition, expectedType,
87-
implBeforeChange.simplify(NeededVariables.of(expectedType, result.getCounterexample())), map,
87+
implBeforeChange.simplify(NeededVariables.forRefinementError(expectedType)), map,
8888
result.getCounterexample(), customMessage);
8989
}
9090
}
@@ -445,7 +445,7 @@ protected void throwRefinementError(SourcePosition position, SourcePosition decl
445445
TranslationTable map = new TranslationTable();
446446
VCImplication premises = buildPremiseChain(map, expected, found);
447447
throw new RefinementError(position, declarationPosition, expected,
448-
premises.simplify(NeededVariables.of(expected, counterexample)), map, counterexample, customMessage);
448+
premises.simplify(NeededVariables.forRefinementError(expected)), map, counterexample, customMessage);
449449
}
450450

451451
protected void throwStateRefinementError(SourcePosition position, SourcePosition declarationPosition,
@@ -454,7 +454,8 @@ protected void throwStateRefinementError(SourcePosition position, SourcePosition
454454
TranslationTable map = new TranslationTable();
455455
VCImplication foundState = buildPremiseChain(map, expected, found);
456456
throw new StateRefinementError(position, declarationPosition, expected,
457-
foundState.simplify(NeededVariables.of(expected, null)), map, customMessage, hints, category, context);
457+
foundState.simplify(NeededVariables.forStateError(expected)), map, customMessage, hints, category,
458+
context);
458459
}
459460

460461
protected void throwStateConflictError(SourcePosition position, Predicate expected) throws StateConflictError {

‎liquidjava-verifier/src/main/java/liquidjava/processor/refinement_checker/general_checkers/OperationsChecker.java‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ public <T> void getUnaryOpRefinements(CtUnaryOperator<T> operator) throws LJErro
179179
if (ex instanceof CtVariableWrite<T> w) {
180180
name = w.getVariable().getSimpleName();
181181
if (w instanceof CtFieldWrite<?> fieldWrite)
182-
name = Utils.qualifyFieldName(fieldWrite.getVariable());
182+
name = Utils.writtenFieldName(fieldWrite);
183183
all = getRefinementUnaryVariableWrite(ex, operator, w, name);
184184
rtc.checkVariableRefinements(all, name, w.getType(), operator, w.getVariable().getDeclaration());
185185
return;
@@ -498,6 +498,8 @@ private <T> Predicate getRefinementUnaryVariableWrite(CtExpression<T> ex, CtUnar
498498
String newName = String.format(Formats.INSTANCE, name, rtc.getContext().getCounter());
499499

500500
Predicate metadada = rtc.getContext().getVariableRefinements(name);
501+
if (metadada == null) // a field of another object, whose current value is not tracked
502+
metadada = new Predicate();
501503
metadada = metadada.substituteVariable(Keys.WILDCARD, newName);
502504
metadada = metadada.substituteVariable(name, newName);
503505

‎liquidjava-verifier/src/main/java/liquidjava/processor/refinement_checker/object_checkers/AuxStateHandler.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -635,7 +635,7 @@ public static void checkTargetChanges(TypeChecker tc, RefinedFunction f, CtExpre
635635
*/
636636
public static void updateGhostField(CtFieldWrite<?> fw, TypeChecker tc) throws LJError {
637637
CtField<?> field = fw.getVariable().getDeclaration();
638-
String updatedVarName = Utils.qualifyFieldName(fw.getVariable());
638+
String updatedVarName = Utils.writtenFieldName(fw);
639639
String targetClass = field.getDeclaringType().getQualifiedName();
640640

641641
// state transition annotation construction

‎liquidjava-verifier/src/main/java/liquidjava/rj_language/ast/formatter/ExpressionNameResolver.java‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,9 @@ public static ExpressionNameResolver forExpression(Expression expression) {
2222

2323
public String resolveVariable(String name) {
2424
String formatted = VariableFormatter.format(name);
25-
// a field is always written as in code, and with its class only when another field is written the same way
25+
// a field is always written as in code, as its binder and the counterexample write it
2626
if (VariableFormatter.isField(name))
27-
return isAmbiguous(name, variables, VariableFormatter::format) ? VariableFormatter.formatWithOwner(name)
28-
: formatted;
27+
return formatted;
2928
return isAmbiguous(name, variables, ExpressionNameResolver::getVariableSimpleName) ? formatted
3029
: Utils.getSimpleName(formatted);
3130
}

‎liquidjava-verifier/src/main/java/liquidjava/rj_language/ast/formatter/VariableFormatter.java‎

Lines changed: 18 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,34 +10,24 @@
1010
*/
1111
public final class VariableFormatter {
1212
private static final Pattern INSTACE_VAR_PATTERN = Pattern.compile("^#(.+)_([0-9]+)$");
13+
// <object>#<declaring type>.<field>: a field of the current object (this) or of the object in a local variable
14+
private static final Pattern FIELD_PATTERN = Pattern.compile("^([A-Za-z_$][\\w$]*)#([\\w.$]+)\\.([\\w$]+)$");
1315
private static final char[] SUPERSCRIPT_CHARS = { '⁰', '¹', '²', '³', '⁴', '⁵', '⁶', '⁷', '⁸', '⁹' };
14-
private static final String FIELD_PREFIX = "this#";
16+
private static final String THIS = "this";
1517
private static final Set<String> STATIC_FIELDS = ConcurrentHashMap.newKeySet();
1618

1719
public static String format(String name) {
18-
return format(name, false);
19-
}
20-
21-
/**
22-
* Formats a name like {@link #format(String)}, but writes an instance field with its declaring class
23-
* ({@code Basket.this.items}) to tell it apart from a field with the same name in another class
24-
*/
25-
public static String formatWithOwner(String name) {
26-
return format(name, true);
27-
}
28-
29-
private static String format(String name, boolean withOwner) {
3020
if (name == null)
3121
return null;
3222

3323
Matcher matcher = INSTACE_VAR_PATTERN.matcher(name);
3424
if (!matcher.matches())
35-
return formatField(name, withOwner);
25+
return formatField(name);
3626

3727
String baseName = matcher.group(1);
3828
String counter = matcher.group(2);
3929
String prefix = isSpecialIdentifier(baseName) ? "#" : "";
40-
return prefix + formatField(baseName, withOwner) + toSuperscript(counter);
30+
return prefix + formatField(baseName) + toSuperscript(counter);
4131
}
4232

4333
public static String withoutInstance(String name) {
@@ -46,11 +36,10 @@ public static String withoutInstance(String name) {
4636
}
4737

4838
/**
49-
* Whether a name is that of a field, {@code this#<declaring type>.<field>}, possibly with an instance counter
39+
* Whether a name is that of a field, {@code <object>#<declaring type>.<field>}, possibly with an instance counter
5040
*/
5141
public static boolean isField(String name) {
52-
String baseName = withoutInstance(name);
53-
return baseName.startsWith(FIELD_PREFIX) && baseName.indexOf('.', FIELD_PREFIX.length()) > 0;
42+
return FIELD_PATTERN.matcher(withoutInstance(name)).matches();
5443
}
5544

5645
/**
@@ -69,20 +58,20 @@ public static void clearStaticFields() {
6958
}
7059

7160
/**
72-
* Writes a field name as it is written in code: {@code this.field} for an instance field and {@code Class.field}
73-
* for a static one, leaving any other name unchanged; the internal name keeps the declaring type to tell fields of
74-
* different classes apart
61+
* Writes a field name as it is written in code: {@code this.field} or {@code other.field} for an instance field and
62+
* {@code Class.field} for a static one, leaving any other name unchanged; the internal name keeps the declaring
63+
* type to tell fields of different classes apart
7564
*/
76-
private static String formatField(String name, boolean withOwner) {
77-
if (!isField(name))
65+
private static String formatField(String name) {
66+
Matcher matcher = FIELD_PATTERN.matcher(name);
67+
if (!matcher.matches())
7868
return name;
7969

80-
int fieldStart = name.lastIndexOf('.');
81-
String field = name.substring(fieldStart + 1);
82-
String owner = simpleTypeName(name.substring(FIELD_PREFIX.length(), fieldStart));
83-
if (STATIC_FIELDS.contains(name))
84-
return owner + "." + field;
85-
return (withOwner ? owner + ".this." : "this.") + field;
70+
String object = matcher.group(1);
71+
String field = matcher.group(3);
72+
if (object.equals(THIS) && STATIC_FIELDS.contains(name))
73+
return simpleTypeName(matcher.group(2)) + "." + field;
74+
return object + "." + field;
8675
}
8776

8877
private static String simpleTypeName(String qualifiedName) {
Lines changed: 59 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,79 @@
11
package liquidjava.rj_language.opt;
22

3+
import static liquidjava.rj_language.opt.VCSimplificationUtils.isTrue;
4+
import static liquidjava.rj_language.opt.VCSimplificationUtils.linkedConjuncts;
5+
import static liquidjava.rj_language.opt.VCSimplificationUtils.splitConjuncts;
6+
import static liquidjava.rj_language.opt.VCSimplificationUtils.termsOf;
7+
import static liquidjava.rj_language.opt.VCSimplificationUtils.valueVariablesOf;
8+
9+
import java.util.ArrayList;
310
import java.util.HashSet;
11+
import java.util.List;
412
import java.util.Set;
513

14+
import liquidjava.processor.VCImplication;
615
import liquidjava.rj_language.Predicate;
7-
import liquidjava.smt.Counterexample;
8-
import liquidjava.utils.Pair;
16+
import liquidjava.rj_language.ast.Expression;
917

1018
/**
11-
* Variables that a simplified VC chain must keep a binder for even when the binder only says {@code true}: those of the
12-
* expected refinement, and those that the counterexample gives a value
19+
* Decides which binders refined only by {@code true} a simplified VC chain keeps for an error. Such a binder states no
20+
* fact, so it is kept only when a fact linked to the expected refinement (sharing a term with it, directly or through
21+
* other facts) constrains its variable's value, or, when the error shows a counterexample, when expected itself
22+
* constrains its value, so that the counterexample can show it
23+
*
24+
* @param expectedTerms
25+
* variables and ghost applications of the expected refinement
26+
* @param expectedValues
27+
* variables whose value the expected refinement constrains
28+
* @param keepExpected
29+
* whether the binders of expectedValues are kept for that alone
1330
*/
14-
public record NeededVariables(Set<String> expected, Set<String> assigned) {
31+
public record NeededVariables(Set<String> expectedTerms, Set<String> expectedValues, boolean keepExpected) {
1532

16-
public static final NeededVariables NONE = new NeededVariables(Set.of(), Set.of());
33+
public static final NeededVariables NONE = new NeededVariables(Set.of(), Set.of(), false);
1734

1835
public NeededVariables {
19-
expected = Set.copyOf(expected);
20-
assigned = Set.copyOf(assigned);
36+
expectedTerms = Set.copyOf(expectedTerms);
37+
expectedValues = Set.copyOf(expectedValues);
38+
}
39+
40+
/**
41+
* For a refinement error, whose counterexample lists the values of the binders
42+
*/
43+
public static NeededVariables forRefinementError(Predicate expected) {
44+
return of(expected, true);
2145
}
2246

23-
public static NeededVariables of(Predicate expected, Counterexample counterexample) {
24-
Set<String> expectedNames = expected == null ? Set.of() : new HashSet<>(expected.getVariableNames());
25-
Set<String> assignedNames = new HashSet<>();
26-
if (counterexample != null)
27-
counterexample.assignments().stream().map(Pair::first).forEach(assignedNames::add);
28-
return new NeededVariables(expectedNames, assignedNames);
47+
/**
48+
* For a state refinement error, which has no counterexample
49+
*/
50+
public static NeededVariables forStateError(Predicate expected) {
51+
return of(expected, false);
52+
}
53+
54+
private static NeededVariables of(Predicate expected, boolean keepExpected) {
55+
if (expected == null)
56+
return new NeededVariables(Set.of(), Set.of(), keepExpected);
57+
return new NeededVariables(new HashSet<>(termsOf(expected.getExpression())),
58+
new HashSet<>(valueVariablesOf(expected.getExpression())), keepExpected);
2959
}
3060

3161
/**
32-
* Whether a binder refined only by {@code true} must stay: the expected refinement is about its variable, or a
33-
* later implication uses it and the counterexample has a value for it
62+
* Variables whose binder refined only by true the chain keeps: those whose own value a fact linked to expected
63+
* constrains. A variable named only as the object of a ghost or state application ({@code x} in
64+
* {@code b == size(x)}) has no value of its own there, so its row would say nothing
3465
*/
35-
public boolean needsTrueBinder(String name, boolean usedDownstream) {
36-
return expected.contains(name) || usedDownstream && assigned.contains(name);
66+
public Set<String> trueBindersToKeep(VCImplication chain) {
67+
List<Expression> facts = new ArrayList<>();
68+
for (VCImplication current = chain; current != null; current = current.getNext())
69+
if (!isTrue(current.getRefinement().getExpression()))
70+
facts.addAll(splitConjuncts(current.getRefinement().getExpression()));
71+
72+
Set<String> keep = new HashSet<>(keepExpected ? expectedValues : Set.of());
73+
for (Expression fact : linkedConjuncts(facts, VCSimplificationUtils::termsOf, expectedTerms))
74+
for (String name : valueVariablesOf(fact))
75+
if (!expectedTerms.contains(name))
76+
keep.add(name);
77+
return keep;
3778
}
3879
}

0 commit comments

Comments
 (0)