Skip to content

Commit c3df13c

Browse files
Drop true binders that the error does not need and write fields as in code
A binder refined only by true now stays in Found only when Expected mentions its variable, or a later row uses it and the counterexample gives it a value. Field variables this#pkg.Class.field are displayed as this.field (Class.field when static) in messages, Found rows and counterexamples; their internal names are unchanged. Fixes #457 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 00cdb11 commit c3df13c

19 files changed

Lines changed: 638 additions & 25 deletions

File tree

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package testSuite.classes.found_field_state_error;
2+
3+
import java.util.Vector;
4+
5+
// the field's row holds its state, so Found keeps it under its source name (#457)
6+
public class InterruptManager {
7+
private Vector<String> interrupts;
8+
9+
public void initialize(int intrNumber) {
10+
interrupts = new Vector<String>(67);
11+
if (intrNumber < 0 || intrNumber >= 67) {
12+
return;
13+
}
14+
String handlers = interrupts.get(intrNumber); // Expect: State Refinement Error
15+
}
16+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package testSuite.classes.found_field_state_error;
2+
3+
import liquidjava.specification.ExternalRefinementsFor;
4+
import liquidjava.specification.Refinement;
5+
import liquidjava.specification.StateRefinement;
6+
import liquidjava.specification.StateSet;
7+
8+
@ExternalRefinementsFor("java.util.Vector")
9+
@StateSet({"empty", "maybeElements"})
10+
public interface VectorRefinements<E> {
11+
12+
@StateRefinement(to = "empty(this)")
13+
public void Vector(@Refinement("_ >= 0") int initialCapacity);
14+
15+
@StateRefinement(from = "empty(this)", to = "maybeElements(this)")
16+
@StateRefinement(from = "maybeElements(this)")
17+
public boolean add(E e);
18+
19+
@StateRefinement(from = "maybeElements(this)")
20+
public E get(@Refinement("_ >= 0") int index);
21+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package testSuite.classes.found_true_binders_error;
2+
3+
import liquidjava.specification.ExternalRefinementsFor;
4+
import liquidjava.specification.Ghost;
5+
import liquidjava.specification.Refinement;
6+
7+
@ExternalRefinementsFor("java.util.ArrayList")
8+
@Ghost("int size")
9+
public interface ArrayListRefinements<E> {
10+
11+
public void ArrayList();
12+
13+
@Refinement("_ == size(this) && _ >= 0")
14+
public int size();
15+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package testSuite.classes.found_true_binders_error;
2+
3+
import java.util.ArrayList;
4+
import java.util.Random;
5+
6+
import liquidjava.specification.Refinement;
7+
8+
// Found keeps a binder refined only by true when the error needs it (#457)
9+
public class Basket {
10+
static int served;
11+
12+
private final ArrayList<String> items;
13+
private final ArrayList<String> extras;
14+
private final Random random = new Random();
15+
int amount;
16+
17+
Basket(ArrayList<String> items, ArrayList<String> extras) {
18+
this.items = items;
19+
this.extras = extras;
20+
}
21+
22+
// the issue's reproducer: the field row only says true
23+
String pick() {
24+
ArrayList<String> items = this.items;
25+
return items.get(random.nextInt(items.size())); // Expect: Refinement Error
26+
}
27+
28+
// a parameter refined only by true
29+
String pickFrom(ArrayList<String> list) {
30+
return list.get(random.nextInt(list.size())); // Expect: Refinement Error
31+
}
32+
33+
// a call result refined only by true
34+
String pickCopy() {
35+
ArrayList<String> copy = copyItems();
36+
return copy.get(random.nextInt(copy.size())); // Expect: Refinement Error
37+
}
38+
39+
ArrayList<String> copyItems() {
40+
return new ArrayList<>(items);
41+
}
42+
43+
// a local refined only by true: the loop may have reassigned it
44+
String pickLocal(ArrayList<String> list) {
45+
ArrayList<String> local = list;
46+
for (String name : list) {
47+
local = list;
48+
}
49+
return local.get(random.nextInt(local.size())); // Expect: Refinement Error
50+
}
51+
52+
// several fields, read with and without this
53+
int pickAny() {
54+
int first = this.items.size();
55+
int second = extras.size();
56+
return random.nextInt(first + second); // Expect: Refinement Error
57+
}
58+
59+
// a later row uses the parameter and the counterexample has a value for it
60+
void next(int x) {
61+
@Refinement("_ > 0")
62+
int y = x + 1; // Expect: Refinement Error
63+
}
64+
65+
// the expected refinement is about the parameter
66+
void forward(int x, int unrelated) {
67+
positive(x); // Expect: Refinement Error
68+
}
69+
70+
void positive(@Refinement("_ > 0") int v) {
71+
}
72+
73+
// a static field
74+
void serve() {
75+
@Refinement("_ > 0")
76+
int count = served + 1; // Expect: Refinement Error
77+
}
78+
79+
// an instance field read with this
80+
void withThis() {
81+
@Refinement("_ > 0")
82+
int a = this.amount; // Expect: Refinement Error
83+
}
84+
85+
// the same field read without this
86+
void withoutThis() {
87+
@Refinement("_ > 0")
88+
int a = amount; // Expect: Refinement Error
89+
}
90+
91+
class Shelf {
92+
ArrayList<String> names;
93+
94+
// a field of an inner class
95+
String pick() {
96+
return names.get(new Random().nextInt(names.size())); // Expect: Refinement Error
97+
}
98+
}
99+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package testSuite.classes.found_true_binders_error;
2+
3+
import liquidjava.specification.ExternalRefinementsFor;
4+
import liquidjava.specification.Refinement;
5+
6+
@ExternalRefinementsFor("java.util.Random")
7+
public interface RandomRefinements {
8+
9+
public void Random();
10+
11+
public int nextInt(@Refinement("bound > 0") int bound);
12+
}

‎liquidjava-verifier/src/main/java/liquidjava/api/CommandLineLauncher.java‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import liquidjava.diagnostics.warnings.CustomWarning;
1010
import liquidjava.processor.RefinementProcessor;
1111
import liquidjava.processor.context.ContextHistory;
12+
import liquidjava.rj_language.ast.formatter.VariableFormatter;
1213
import liquidjava.specification.Refinement;
1314
import picocli.CommandLine;
1415
import spoon.Launcher;
@@ -57,6 +58,7 @@ public static void launch(String... paths) {
5758

5859
diagnostics.clear();
5960
contextHistory.clearHistory();
61+
VariableFormatter.clearStaticFields();
6062
Launcher launcher = new Launcher();
6163
for (String path : paths) {
6264
if (!new File(path).exists()) {

‎liquidjava-verifier/src/main/java/liquidjava/processor/VCImplication.java‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import java.util.Objects;
44

55
import liquidjava.rj_language.Predicate;
6+
import liquidjava.rj_language.opt.NeededVariables;
67
import liquidjava.rj_language.opt.VCSimplification;
78
import liquidjava.rj_language.opt.VCSimplificationResult;
89
import liquidjava.utils.Utils;
@@ -79,6 +80,10 @@ public VCSimplificationResult simplify() {
7980
return VCSimplification.simplifyToFixedPoint(this);
8081
}
8182

83+
public VCSimplificationResult simplify(NeededVariables needed) {
84+
return VCSimplification.simplifyToFixedPoint(this, needed);
85+
}
86+
8287
public Predicate toPredicate() {
8388
return hasBinder() || hasNext() ? toConjunctions() : refinement;
8489
}

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

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import liquidjava.rj_language.ast.Expression;
2121
import liquidjava.rj_language.ast.Ite;
2222
import liquidjava.rj_language.ast.Var;
23+
import liquidjava.rj_language.opt.NeededVariables;
2324
import liquidjava.smt.Counterexample;
2425
import liquidjava.smt.SMTEvaluator;
2526
import liquidjava.smt.SMTResult;
@@ -83,7 +84,8 @@ public void processSubtyping(Predicate expectedType, List<GhostState> list, CtEl
8384
DebugLog.smtResult(result);
8485
if (result.isError()) {
8586
throw new RefinementError(element.getPosition(), declarationPosition, expectedType,
86-
implBeforeChange.simplify(), map, result.getCounterexample(), customMessage);
87+
implBeforeChange.simplify(NeededVariables.of(expectedType, result.getCounterexample())), map,
88+
result.getCounterexample(), customMessage);
8789
}
8890
}
8991

@@ -442,17 +444,17 @@ protected void throwRefinementError(SourcePosition position, SourcePosition decl
442444
Predicate found, Counterexample counterexample, String customMessage) throws RefinementError {
443445
TranslationTable map = new TranslationTable();
444446
VCImplication premises = buildPremiseChain(map, expected, found);
445-
throw new RefinementError(position, declarationPosition, expected, premises.simplify(), map, counterexample,
446-
customMessage);
447+
throw new RefinementError(position, declarationPosition, expected,
448+
premises.simplify(NeededVariables.of(expected, counterexample)), map, counterexample, customMessage);
447449
}
448450

449451
protected void throwStateRefinementError(SourcePosition position, SourcePosition declarationPosition,
450452
Predicate found, Predicate expected, String customMessage, List<Hint> hints, ErrorCategory category,
451453
StateErrorContext context) throws StateRefinementError {
452454
TranslationTable map = new TranslationTable();
453455
VCImplication foundState = buildPremiseChain(map, expected, found);
454-
throw new StateRefinementError(position, declarationPosition, expected, foundState.simplify(), map,
455-
customMessage, hints, category, context);
456+
throw new StateRefinementError(position, declarationPosition, expected,
457+
foundState.simplify(NeededVariables.of(expected, null)), map, customMessage, hints, category, context);
456458
}
457459

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

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ 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
26+
if (VariableFormatter.isField(name))
27+
return isAmbiguous(name, variables, VariableFormatter::format) ? VariableFormatter.formatWithOwner(name)
28+
: formatted;
2529
return isAmbiguous(name, variables, ExpressionNameResolver::getVariableSimpleName) ? formatted
2630
: Utils.getSimpleName(formatted);
2731
}

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

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package liquidjava.rj_language.ast.formatter;
22

3+
import java.util.Set;
4+
import java.util.concurrent.ConcurrentHashMap;
35
import java.util.regex.Matcher;
46
import java.util.regex.Pattern;
57

@@ -9,8 +11,22 @@
911
public final class VariableFormatter {
1012
private static final Pattern INSTACE_VAR_PATTERN = Pattern.compile("^#(.+)_([0-9]+)$");
1113
private static final char[] SUPERSCRIPT_CHARS = { '⁰', '¹', '²', '³', '⁴', '⁵', '⁶', '⁷', '⁸', '⁹' };
14+
private static final String FIELD_PREFIX = "this#";
15+
private static final Set<String> STATIC_FIELDS = ConcurrentHashMap.newKeySet();
1216

1317
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) {
1430
if (name == null)
1531
return null;
1632

@@ -19,19 +35,64 @@ public static String format(String name) {
1935

2036
Matcher matcher = INSTACE_VAR_PATTERN.matcher(name);
2137
if (!matcher.matches())
22-
return name;
38+
return formatField(name, withOwner);
2339

2440
String baseName = matcher.group(1);
2541
String counter = matcher.group(2);
2642
String prefix = isSpecialIdentifier(baseName) ? "#" : "";
27-
return prefix + baseName + toSuperscript(counter);
43+
return prefix + formatField(baseName, withOwner) + toSuperscript(counter);
2844
}
2945

3046
public static String withoutInstance(String name) {
3147
Matcher matcher = INSTACE_VAR_PATTERN.matcher(name);
3248
return matcher.matches() ? matcher.group(1) : name;
3349
}
3450

51+
/**
52+
* Whether a name is that of a field, {@code this#<declaring type>.<field>}, possibly with an instance counter
53+
*/
54+
public static boolean isField(String name) {
55+
String baseName = withoutInstance(name);
56+
return baseName.startsWith(FIELD_PREFIX) && baseName.indexOf('.', FIELD_PREFIX.length()) > 0;
57+
}
58+
59+
/**
60+
* Records that a field name ({@code this#<declaring type>.<field>}) belongs to a static field, which is then
61+
* displayed as {@code Class.field} instead of {@code this.field}
62+
*/
63+
public static void registerStaticField(String name) {
64+
STATIC_FIELDS.add(name);
65+
}
66+
67+
/**
68+
* Forgets the static fields recorded by the previous verification
69+
*/
70+
public static void clearStaticFields() {
71+
STATIC_FIELDS.clear();
72+
}
73+
74+
/**
75+
* Writes a field name as it is written in code: {@code this.field} for an instance field and {@code Class.field}
76+
* for a static one, leaving any other name unchanged; the internal name keeps the declaring type to tell fields of
77+
* different classes apart
78+
*/
79+
private static String formatField(String name, boolean withOwner) {
80+
if (!isField(name))
81+
return name;
82+
83+
int fieldStart = name.lastIndexOf('.');
84+
String field = name.substring(fieldStart + 1);
85+
String owner = simpleTypeName(name.substring(FIELD_PREFIX.length(), fieldStart));
86+
if (STATIC_FIELDS.contains(name))
87+
return owner + "." + field;
88+
return (withOwner ? owner + ".this." : "this.") + field;
89+
}
90+
91+
private static String simpleTypeName(String qualifiedName) {
92+
int start = Math.max(qualifiedName.lastIndexOf('.'), qualifiedName.lastIndexOf('$'));
93+
return qualifiedName.substring(start + 1);
94+
}
95+
3596
private static String toSuperscript(String number) {
3697
StringBuilder sb = new StringBuilder(number.length());
3798
for (char c : number.toCharArray()) {

0 commit comments

Comments
 (0)