Skip to content

Commit 7bee4d7

Browse files
Keep writes to another object's field as on main and only name the object in their display
Writes through another object are checked and tracked exactly as on main again; the new instance they create is only displayed with the receiver's name. A true binder that is a primitive ghost argument is kept like any other value variable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 4d8a3ed commit 7bee4d7

12 files changed

Lines changed: 118 additions & 45 deletions

File tree

‎liquidjava-example/src/main/java/testSuite/ErrorOtherObjectFieldWrite.java‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import liquidjava.specification.Refinement;
44

5-
// a write to another object's field is checked against the field's refinement under that object's name (#457)
5+
// writes to another object's field are checked as on main; only their display names the object (#457)
66
public class ErrorOtherObjectFieldWrite {
77
@Refinement("_ >= 0")
88
int level;
@@ -11,10 +11,26 @@ void setOther(ErrorOtherObjectFieldWrite other) {
1111
other.level = -1; // Expect: Refinement Error
1212
}
1313

14+
void incrementOther(ErrorOtherObjectFieldWrite other) {
15+
other.level++;
16+
}
17+
18+
void addToOther(ErrorOtherObjectFieldWrite other) {
19+
other.level += 1;
20+
}
21+
1422
void decrementOther(ErrorOtherObjectFieldWrite other) {
1523
other.level--; // Expect: Refinement Error
1624
}
1725

26+
void writeThroughAlias() {
27+
level = 7;
28+
ErrorOtherObjectFieldWrite other = this;
29+
other.level = 3;
30+
@Refinement("_ == 7")
31+
int k = level; // Expect: Refinement Error
32+
}
33+
1834
void setOwn() {
1935
this.level = -1; // Expect: Refinement Error
2036
}

‎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
@@ -267,7 +267,7 @@ private <T, A extends T> void visitAssignment(CtAssignment<T, A> assignment) thr
267267
} else if (ex instanceof CtFieldWrite<?> fw) {
268268
CtFieldReference<?> cr = fw.getVariable();
269269
CtField<?> f = fw.getVariable().getDeclaration();
270-
String updatedVarName = Utils.writtenFieldName(fw);
270+
String updatedVarName = Utils.qualifyFieldName(cr);
271271
// a field declared outside the analyzed sources (e.g. a library field) has no declared refinement: the
272272
// write stands in for its declaration
273273
checkAssignment(updatedVarName, cr.getType(), ex, assignment.getAssignment(), assignment,

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import liquidjava.processor.state_machine.StateMachineBuilder;
2424
import liquidjava.processor.state_machine.StateSolver;
2525
import liquidjava.rj_language.Predicate;
26+
import liquidjava.rj_language.ast.formatter.VariableFormatter;
2627
import liquidjava.rj_language.parsing.RefinementsParser;
2728
import liquidjava.smt.SMTEvaluator;
2829
import liquidjava.smt.SMTResult;
@@ -382,6 +383,8 @@ public void checkVariableRefinements(Predicate refinementFound, String simpleNam
382383
Predicate cet = cEt.substituteVariable(Keys.WILDCARD, simpleName);
383384

384385
String newName = String.format(Formats.INSTANCE, simpleName, context.getCounter());
386+
Utils.untrackedWriteReceiver(usage)
387+
.ifPresent(receiver -> VariableFormatter.registerReceiver(newName, receiver));
385388
Predicate correctNewRefinement = refinementFound.substituteVariable(Keys.WILDCARD, newName);
386389
correctNewRefinement = correctNewRefinement.substituteVariable(Keys.THIS, newName);
387390
cEt = cEt.substituteVariable(simpleName, newName);

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

Lines changed: 1 addition & 3 deletions
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.writtenFieldName(fieldWrite);
182+
name = Utils.qualifyFieldName(fieldWrite.getVariable());
183183
all = getRefinementUnaryVariableWrite(ex, operator, w, name);
184184
rtc.checkVariableRefinements(all, name, w.getType(), operator, w.getVariable().getDeclaration());
185185
return;
@@ -498,8 +498,6 @@ 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();
503501
metadada = metadada.substituteVariable(Keys.WILDCARD, newName);
504502
metadada = metadada.substituteVariable(name, newName);
505503

‎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.writtenFieldName(fw);
638+
String updatedVarName = Utils.qualifyFieldName(fw.getVariable());
639639
String targetClass = field.getDeclaringType().getQualifiedName();
640640

641641
// state transition annotation construction

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

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

3+
import java.util.Map;
34
import java.util.Set;
45
import java.util.concurrent.ConcurrentHashMap;
56
import java.util.regex.Matcher;
@@ -10,11 +11,11 @@
1011
*/
1112
public final class VariableFormatter {
1213
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$]+)$");
14+
// this#<declaring type>.<field>
15+
private static final Pattern FIELD_PATTERN = Pattern.compile("^this#([\\w.$]+)\\.([\\w$]+)$");
1516
private static final char[] SUPERSCRIPT_CHARS = { '⁰', '¹', '²', '³', '⁴', '⁵', '⁶', '⁷', '⁸', '⁹' };
16-
private static final String THIS = "this";
1717
private static final Set<String> STATIC_FIELDS = ConcurrentHashMap.newKeySet();
18+
private static final Map<String, String> WRITE_RECEIVERS = new ConcurrentHashMap<>();
1819

1920
public static String format(String name) {
2021
if (name == null)
@@ -26,6 +27,9 @@ public static String format(String name) {
2627

2728
String baseName = matcher.group(1);
2829
String counter = matcher.group(2);
30+
String receiver = WRITE_RECEIVERS.get(name);
31+
if (receiver != null && isField(baseName))
32+
return (receiver.isEmpty() ? "" : receiver + ".") + fieldOf(baseName) + toSuperscript(counter);
2933
String prefix = isSpecialIdentifier(baseName) ? "#" : "";
3034
return prefix + formatField(baseName) + toSuperscript(counter);
3135
}
@@ -36,7 +40,7 @@ public static String withoutInstance(String name) {
3640
}
3741

3842
/**
39-
* Whether a name is that of a field, {@code <object>#<declaring type>.<field>}, possibly with an instance counter
43+
* Whether a name is that of a field, {@code this#<declaring type>.<field>}, possibly with an instance counter
4044
*/
4145
public static boolean isField(String name) {
4246
return FIELD_PATTERN.matcher(withoutInstance(name)).matches();
@@ -51,27 +55,40 @@ public static void registerStaticField(String name) {
5155
}
5256

5357
/**
54-
* Forgets the static fields recorded by the previous verification
58+
* Records that an instance of a field ({@code #this#<declaring type>.<field>_<n>}) was created by a write through
59+
* another object ({@code receiver.field = v}), so that it is displayed as {@code receiver.field} instead of
60+
* {@code this.field}; an empty receiver displays the bare field name. Only the display changes
61+
*/
62+
public static void registerReceiver(String instanceName, String receiver) {
63+
WRITE_RECEIVERS.put(instanceName, receiver);
64+
}
65+
66+
/**
67+
* Forgets the static fields and write receivers recorded by the previous verification
5568
*/
5669
public static void clearStaticFields() {
5770
STATIC_FIELDS.clear();
71+
WRITE_RECEIVERS.clear();
5872
}
5973

6074
/**
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
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
6478
*/
6579
private static String formatField(String name) {
6680
Matcher matcher = FIELD_PATTERN.matcher(name);
6781
if (!matcher.matches())
6882
return name;
6983

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;
84+
if (STATIC_FIELDS.contains(name))
85+
return simpleTypeName(matcher.group(1)) + "." + matcher.group(2);
86+
return "this." + matcher.group(2);
87+
}
88+
89+
private static String fieldOf(String name) {
90+
Matcher matcher = FIELD_PATTERN.matcher(name);
91+
return matcher.matches() ? matcher.group(2) : name;
7592
}
7693

7794
private static String simpleTypeName(String qualifiedName) {

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

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import static liquidjava.rj_language.opt.VCSimplificationUtils.splitConjuncts;
66
import static liquidjava.rj_language.opt.VCSimplificationUtils.termsOf;
77
import static liquidjava.rj_language.opt.VCSimplificationUtils.valueVariablesOf;
8+
import static liquidjava.rj_language.opt.VCSimplificationUtils.variablesOf;
89

910
import java.util.ArrayList;
1011
import java.util.HashSet;
@@ -60,19 +61,32 @@ private static NeededVariables of(Predicate expected, boolean keepExpected) {
6061

6162
/**
6263
* 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
64+
* constrains. A variable of an object type that a fact names only as the argument of a ghost or state application
65+
* ({@code x} in {@code b == size(x)}) has no value of its own there, so its row would say nothing; a primitive
66+
* argument ({@code k} in {@code y == f(k)}) has one
6567
*/
6668
public Set<String> trueBindersToKeep(VCImplication chain) {
6769
List<Expression> facts = new ArrayList<>();
68-
for (VCImplication current = chain; current != null; current = current.getNext())
70+
Set<String> primitives = new HashSet<>();
71+
for (VCImplication current = chain; current != null; current = current.getNext()) {
72+
if (current.hasBinder() && current.getType().isPrimitive())
73+
primitives.add(current.getName());
6974
if (!isTrue(current.getRefinement().getExpression()))
7075
facts.addAll(splitConjuncts(current.getRefinement().getExpression()));
76+
}
7177

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))
78+
Set<String> keep = new HashSet<>();
79+
if (keepExpected)
80+
keep.addAll(expectedValues);
81+
for (Expression fact : linkedConjuncts(facts, VCSimplificationUtils::termsOf, expectedTerms)) {
82+
List<String> values = valueVariablesOf(fact);
83+
for (String name : variablesOf(fact))
84+
if ((values.contains(name) || primitives.contains(name)) && !expectedTerms.contains(name))
85+
keep.add(name);
86+
}
87+
if (keepExpected)
88+
for (String name : expectedTerms)
89+
if (primitives.contains(name))
7690
keep.add(name);
7791
return keep;
7892
}

‎liquidjava-verifier/src/main/java/liquidjava/utils/Utils.java‎

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,25 @@
33
import java.util.List;
44
import java.util.Map;
55
import java.util.Objects;
6+
import java.util.Optional;
67
import java.util.Scanner;
78
import java.util.Set;
89
import java.util.stream.Stream;
910

1011
import liquidjava.rj_language.ast.formatter.VariableFormatter;
1112
import liquidjava.utils.constants.Types;
13+
import spoon.reflect.code.CtAssignment;
1214
import spoon.reflect.code.CtBlock;
15+
import spoon.reflect.code.CtExpression;
1316
import spoon.reflect.code.CtFieldAccess;
17+
import spoon.reflect.code.CtFieldWrite;
1418
import spoon.reflect.code.CtFor;
1519
import spoon.reflect.code.CtForEach;
1620
import spoon.reflect.code.CtIf;
1721
import spoon.reflect.code.CtSuperAccess;
1822
import spoon.reflect.code.CtThisAccess;
1923
import spoon.reflect.code.CtTry;
24+
import spoon.reflect.code.CtUnaryOperator;
2025
import spoon.reflect.code.CtVariableRead;
2126
import spoon.reflect.code.CtWhile;
2227
import spoon.reflect.cu.SourcePosition;
@@ -67,17 +72,17 @@ public static String qualifyFieldName(CtFieldReference<?> field) {
6772
}
6873

6974
/**
70-
* Name of the context variable that a field write updates: the tracked field ({@link #qualifyFieldName}) for a
71-
* static field or a field of the current object, and otherwise a variable of its own for that field of the other
72-
* object, {@code <object>#<declaring type>.<field>}, named after the variable holding the object when there is one
75+
* The object through which a write updates a field of another object ({@code other} in {@code other.f = v} or
76+
* {@code other.f++}), used only to display that write; empty when the write is to a tracked field (see
77+
* {@link #accessesTrackedField}) or not a field write, and an empty string when the object is not in a variable
7378
*/
74-
public static String writtenFieldName(CtFieldAccess<?> access) {
75-
String name = qualifyFieldName(access.getVariable());
76-
if (accessesTrackedField(access))
77-
return name;
78-
String object = access.getTarget()instanceof CtVariableRead<?> read ? read.getVariable().getSimpleName()
79-
: "other";
80-
return object + name.substring("this".length());
79+
public static Optional<String> untrackedWriteReceiver(CtElement usage) {
80+
CtExpression<?> written = usage instanceof CtAssignment<?, ?> assignment ? assignment.getAssigned()
81+
: usage instanceof CtUnaryOperator<?> unary ? unary.getOperand() : null;
82+
if (!(written instanceof CtFieldWrite<?> write) || accessesTrackedField(write))
83+
return Optional.empty();
84+
return Optional
85+
.of(write.getTarget()instanceof CtVariableRead<?> read ? read.getVariable().getSimpleName() : "");
8186
}
8287

8388
public static String getFile(CtElement element) {

‎liquidjava-verifier/src/test/java/liquidjava/api/tests/TestFoundBinders.java‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -165,13 +165,19 @@ void trueRowDecidingAPathDependentStateIsKept() {
165165
assertEquals(List.of("∀ready true", "∀f ready ? open(f) : created(f)"), shown.rows());
166166
}
167167

168+
@Test
169+
void writesToAnotherObjectsFieldAreCheckedAsOnMain() {
170+
// other.level++ and other.level += 1 verify, other.level-- and the write through an alias of this are reported
171+
assertEquals(List.of(11, 23, 31, 35), new ArrayList<>(otherObjectWrite.keySet()));
172+
assertEquals("k == 7", otherObjectWrite.get(31).message().replaceAll(".* is not a subtype of ", ""));
173+
}
174+
168175
@Test
169176
void writeToAnotherObjectsFieldIsWrittenWithThatObject() {
170-
assertEquals(List.of(11, 15, 19), new ArrayList<>(otherObjectWrite.keySet()));
171177
assertEquals("other.level == -1 is not a subtype of other.level >= 0", otherObjectWrite.get(11).message());
172-
assertEquals(List.of("∀other.level true", "∀other.level other.level == other.level - 1"),
173-
otherObjectWrite.get(15).rows());
174-
assertEquals("this.level == -1 is not a subtype of this.level >= 0", otherObjectWrite.get(19).message());
178+
assertEquals(List.of("∀this.level this.level >= 0", "∀other.level other.level == this.level - 1"),
179+
otherObjectWrite.get(23).rows());
180+
assertEquals("this.level == -1 is not a subtype of this.level >= 0", otherObjectWrite.get(35).message());
175181
}
176182

177183
@Test

‎liquidjava-verifier/src/test/java/liquidjava/rj_language/ast/formatter/VariableFormatterTest.java‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,16 +50,22 @@ void forgetsStaticFieldsOfThePreviousVerification() {
5050
}
5151

5252
@Test
53-
void writesFieldsOfAnotherObjectWithThatObject() {
54-
assertEquals("other.level", VariableFormatter.format("other#otherwrite.W.level"));
55-
assertEquals("other.level¹", VariableFormatter.format("#other#otherwrite.W.level_1"));
53+
void writesAnInstanceCreatedByAWriteThroughAnotherObjectWithThatObject() {
54+
VariableFormatter.registerReceiver("#this#otherwrite.W.level_1", "other");
55+
VariableFormatter.registerReceiver("#this#otherwrite.W.level_2", "");
56+
assertEquals("other.level¹", VariableFormatter.format("#this#otherwrite.W.level_1"));
57+
assertEquals("level²", VariableFormatter.format("#this#otherwrite.W.level_2"));
58+
assertEquals("this.level³", VariableFormatter.format("#this#otherwrite.W.level_3"));
59+
assertEquals("this.level", VariableFormatter.format("this#otherwrite.W.level"));
60+
VariableFormatter.clearStaticFields();
61+
assertEquals("this.level¹", VariableFormatter.format("#this#otherwrite.W.level_1"));
5662
}
5763

5864
@Test
5965
void recognisesFieldNames() {
6066
assertTrue(VariableFormatter.isField("this#shop.Basket.items"));
6167
assertTrue(VariableFormatter.isField("#this#shop.Basket.items_4"));
62-
assertTrue(VariableFormatter.isField("other#otherwrite.W.level"));
68+
assertFalse(VariableFormatter.isField("other#otherwrite.W.level"));
6369
assertFalse(VariableFormatter.isField("this#Basket"));
6470
assertFalse(VariableFormatter.isField("#this_4"));
6571
assertFalse(VariableFormatter.isField("items"));

0 commit comments

Comments
 (0)