Skip to content

Commit 478eb94

Browse files
Resolve field reads by declaring type and target object (#321, #322)
A field read first looked up the field's simple name in the context. Since #327 fields live there only under their qualified name, so that lookup could only find an unrelated local with the same name: Format.JPG was read as the local JPG (#321). That branch is removed. The context variable this#C.f tracks the value of the field of the current object, but reads of the same field on another object (other.val) used it too, so facts about this.val leaked into other.val (#322). Utils gains accessesTrackedField, true for static fields and for this/super/Outer.this targets; field reads on any other target use the field's declared refinement instead, in visitCtFieldRead and in the operand and compound assignment paths of OperationsChecker. Limits.max vs this.max (the first #322 reproducer) was already fixed by the qualified names of #327 and gets a regression test. Fixes #321 Fixes #322 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent ad055b4 commit 478eb94

7 files changed

Lines changed: 173 additions & 15 deletions

File tree

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package testSuite;
2+
3+
import liquidjava.specification.Refinement;
4+
5+
// #321, #322: field reads resolve to the field of the class and object they name
6+
public class CorrectFieldReadResolution {
7+
enum Codec {
8+
JPG, PNG
9+
}
10+
11+
static final int LIMIT = 10;
12+
13+
@Refinement("val > 0")
14+
int val;
15+
16+
int max = 5;
17+
18+
public CorrectFieldReadResolution(@Refinement("_ > 0") int v) {
19+
val = v;
20+
}
21+
22+
static void onlyPng(@Refinement("_ == Codec.PNG") Codec f) {}
23+
24+
void otherClass() {
25+
@Refinement("_ == 10")
26+
int k = Limits.max;
27+
@Refinement("_ == 5")
28+
int m = max;
29+
@Refinement("_ == 5")
30+
int t = this.max;
31+
}
32+
33+
void otherObject(CorrectFieldReadResolution other) {
34+
val = 7;
35+
@Refinement("_ > 0")
36+
int k = other.val;
37+
@Refinement("_ > 7")
38+
int s = val + other.val;
39+
if (other.val == 3) {
40+
@Refinement("_ == 7")
41+
int t = val;
42+
}
43+
@Refinement("_ == 7")
44+
int u = this.val;
45+
}
46+
47+
static void sameNamedLocals() {
48+
Codec JPG = Codec.JPG;
49+
Codec PNG = Codec.PNG;
50+
onlyPng(PNG);
51+
onlyPng(Codec.PNG);
52+
Codec g = Codec.PNG;
53+
onlyPng(g);
54+
55+
int LIMIT = 3;
56+
@Refinement("_ == 10")
57+
int k = CorrectFieldReadResolution.LIMIT;
58+
@Refinement("_ == 3")
59+
int l = LIMIT;
60+
}
61+
62+
static class Limits {
63+
static final int max = 10;
64+
}
65+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package testSuite;
2+
3+
import liquidjava.specification.Refinement;
4+
5+
// #322: Limits.max must not be read as this class's own field max
6+
public class ErrorFieldOfOtherClass {
7+
int max = 5;
8+
9+
void check() {
10+
@Refinement("_ == 5")
11+
int k = Limits.max; // Expect: Refinement Error
12+
}
13+
14+
static class Limits {
15+
static final int max = 10;
16+
}
17+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package testSuite;
2+
3+
import liquidjava.specification.Refinement;
4+
5+
// #322: other.val is a different object's field, not this.val
6+
public class ErrorFieldOfOtherObject {
7+
@Refinement("val > 0")
8+
int val;
9+
10+
public ErrorFieldOfOtherObject(@Refinement("_ > 0") int v) {
11+
val = v;
12+
}
13+
14+
void read(ErrorFieldOfOtherObject other) {
15+
val = 7;
16+
@Refinement("_ == 7")
17+
int k = other.val; // Expect: Refinement Error
18+
}
19+
20+
void sum(ErrorFieldOfOtherObject other) {
21+
val = 7;
22+
@Refinement("_ == 14")
23+
int k = val + other.val; // Expect: Refinement Error
24+
}
25+
26+
void compound(ErrorFieldOfOtherObject other) {
27+
val = 7;
28+
int j = 0;
29+
j += other.val;
30+
@Refinement("_ == 7")
31+
int k = j; // Expect: Refinement Error
32+
}
33+
34+
void condition(ErrorFieldOfOtherObject other) {
35+
val = 7;
36+
if (other.val == 3) {
37+
@Refinement("_ == 3")
38+
int k = val; // Expect: Refinement Error
39+
}
40+
}
41+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package testSuite;
2+
3+
import liquidjava.specification.Refinement;
4+
5+
// #321: a local named like a constant must not stand for the constant
6+
public class ErrorLocalShadowsConstant {
7+
enum Encoding {
8+
JPG, PNG
9+
}
10+
11+
static final int LIMIT = 10;
12+
13+
static void onlyPng(@Refinement("_ == Encoding.PNG") Encoding f) {}
14+
15+
public static void main(String[] args) {
16+
Encoding JPG = Encoding.PNG;
17+
Encoding g = Encoding.JPG;
18+
onlyPng(g); // Expect: Refinement Error
19+
}
20+
21+
static void staticConstant() {
22+
int LIMIT = 3;
23+
@Refinement("_ == 3")
24+
int k = ErrorLocalShadowsConstant.LIMIT; // Expect: Refinement Error
25+
}
26+
}

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

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -316,17 +316,10 @@ public <T> void visitCtField(CtField<T> f) {
316316
@Override
317317
public <T> void visitCtFieldRead(CtFieldRead<T> fieldRead) {
318318
String fieldName = fieldRead.getVariable().getSimpleName();
319-
if (context.hasVariable(fieldName)) {
320-
RefinedVariable rv = context.getVariableByName(fieldName);
321-
if (rv instanceof Variable && ((Variable) rv).getLocation().isPresent()
322-
&& ((Variable) rv).getLocation().get().equals(fieldRead.getTarget().toString())) {
323-
fieldRead.putMetadata(Keys.REFINEMENT, context.getVariableRefinements(fieldName));
324-
} else {
325-
fieldRead.putMetadata(Keys.REFINEMENT,
326-
Predicate.createEquals(Predicate.createVar(Keys.WILDCARD), Predicate.createVar(fieldName)));
327-
}
328-
329-
} else if (context.hasVariable(Utils.qualifyFieldName(fieldRead.getVariable()))) {
319+
// fields live in the context under their qualified name only: a variable with the field's simple name is an
320+
// unrelated local (#321), and the tracked value is that of this object, not of another one (#322)
321+
if (Utils.accessesTrackedField(fieldRead)
322+
&& context.hasVariable(Utils.qualifyFieldName(fieldRead.getVariable()))) {
330323
String thisName = Utils.qualifyFieldName(fieldRead.getVariable());
331324
fieldRead.putMetadata(Keys.REFINEMENT,
332325
Predicate.createEquals(Predicate.createVar(Keys.WILDCARD), Predicate.createVar(thisName)));

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

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -222,9 +222,10 @@ private Predicate getOperationRefinements(CtBinaryOperator<?> operator, CtVariab
222222
elemName = Utils.qualifyFieldName(fieldRead.getVariable());
223223
Predicate elemRef = rtc.getContext().getVariableRefinements(elemName);
224224

225-
// Not a field of this class (e.g. an enum constant or a static constant): use the value given by the
226-
// field read refinement (e.g. Format.JPG), or an unconstrained fresh value when there is none
227-
if (elemRef == null && elemVar instanceof CtFieldRead)
225+
// Not a field of this class or of this object (e.g. an enum constant, a static constant or other.f): use
226+
// the value given by the field read refinement (e.g. Format.JPG), or an unconstrained fresh value
227+
if (elemVar instanceof CtFieldRead<?> fieldRead
228+
&& (elemRef == null || !Utils.accessesTrackedField(fieldRead)))
228229
return valueFromRefinement(elemVar, rtc.getRefinement(elemVar));
229230

230231
String returnName = elemName;
@@ -361,7 +362,9 @@ private Predicate getCurrentVariableValue(String name) {
361362
* Converts a operator assignment into an arithmetic predicate operand
362363
*/
363364
private Predicate getOperatorAssignmentRefinement(CtExpression<?> element) throws LJError {
364-
if (element instanceof CtVariableRead<?> variableRead) {
365+
if (element instanceof CtFieldRead<?> fieldRead && !Utils.accessesTrackedField(fieldRead)) {
366+
return valueFromRefinement(element, rtc.getRefinement(element)); // a field of another object
367+
} else if (element instanceof CtVariableRead<?> variableRead) {
365368
String name = variableRead.getVariable().getSimpleName();
366369
if (variableRead instanceof CtFieldRead<?> fieldRead)
367370
name = Utils.qualifyFieldName(fieldRead.getVariable());

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@
99

1010
import liquidjava.utils.constants.Types;
1111
import spoon.reflect.code.CtBlock;
12+
import spoon.reflect.code.CtFieldAccess;
1213
import spoon.reflect.code.CtFor;
1314
import spoon.reflect.code.CtForEach;
1415
import spoon.reflect.code.CtIf;
16+
import spoon.reflect.code.CtSuperAccess;
17+
import spoon.reflect.code.CtThisAccess;
1518
import spoon.reflect.code.CtTry;
1619
import spoon.reflect.code.CtWhile;
1720
import spoon.reflect.cu.SourcePosition;
@@ -144,4 +147,14 @@ public static String getExpressionFromPosition(SourcePosition position) {
144147
}
145148
return null;
146149
}
150+
151+
/**
152+
* Whether the context variable {@link #qualifyFieldName} names the field accessed here: a static field, or a field
153+
* of the current object ({@code f}, {@code this.f}, {@code super.f}, {@code Outer.this.f}). A field of another
154+
* object ({@code other.f}) has its own value, which is not tracked in the context.
155+
*/
156+
public static boolean accessesTrackedField(CtFieldAccess<?> access) {
157+
return access.getVariable().isStatic() || access.getTarget() == null
158+
|| access.getTarget() instanceof CtThisAccess<?> || access.getTarget() instanceof CtSuperAccess<?>;
159+
}
147160
}

0 commit comments

Comments
 (0)