Skip to content

Commit 456ae10

Browse files
Forget non-final field initializers inside methods (#340)
A field initializer's instance stayed the field's last instance for the whole class, so every method read a non-final field as if it still held its initializer, although any other method (e.g. a setter) may have reassigned it. Passing such a field to a refined parameter was accepted. On entry to a method (constructors are unchanged), each non-final field with a recorded value gets a fresh instance carrying only its declared refinement. Final fields keep their initializer. Assignments made inside the method are still tracked as before. The field cases of the deferred-callback fixtures relied on the initializer state of a non-final field in a method; they now check that state in the constructor, where it does hold, so they keep testing that creating a callback does not run it. Fixes #340 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent a415821 commit 456ae10

5 files changed

Lines changed: 117 additions & 5 deletions

File tree

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ class CorrectDeferredCallbackState {
2020
UndoManager manager = new UndoManager();
2121
Runnable listener = () -> { manager.addEdit(null); };
2222

23-
void fieldCallbackMayNeverRun() {
23+
CorrectDeferredCallbackState() {
24+
// the field callback may never run: the initializer state still holds in the constructor
2425
manager.discardAllEdits();
2526
}
2627

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package testSuite;
2+
3+
import liquidjava.specification.Refinement;
4+
5+
public class CorrectMutableFieldInitializer {
6+
private final float fixed = 0.75f;
7+
8+
@Refinement("_ >= 0.0 && _ <= 1.0")
9+
private float refined = 0.75f;
10+
11+
private float local = 0.75f;
12+
13+
public CorrectMutableFieldInitializer() {
14+
use(local);
15+
}
16+
17+
public void setRefined(@Refinement("_ >= 0.0 && _ <= 1.0") float v) {
18+
this.refined = v;
19+
}
20+
21+
public void setLocal(float v) {
22+
this.local = v;
23+
}
24+
25+
static void use(@Refinement("_ >= 0.0 && _ <= 1.0") float quality) {}
26+
27+
void writeFixed() {
28+
use(fixed);
29+
}
30+
31+
void writeRefined() {
32+
use(refined);
33+
}
34+
35+
void writeAfterAssignment() {
36+
local = 0.5f;
37+
use(local);
38+
}
39+
}

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

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,6 @@ class ErrorAnonymousCallbackState {
2323
Runnable listener = new Runnable() {
2424
public void run() { manager.addEdit(null); }
2525
};
26-
}
27-
28-
void undo() {
2926
manager.undo(); // Expect: State Refinement Error
3027
}
3128
}
@@ -42,7 +39,7 @@ class ErrorFieldLambdaCallbackState {
4239
UndoManager manager = new UndoManager();
4340
Runnable listener = () -> { manager.addEdit(null); };
4441

45-
void undo() {
42+
ErrorFieldLambdaCallbackState() {
4643
manager.undo(); // Expect: State Refinement Error
4744
}
4845
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package testSuite;
2+
3+
import liquidjava.specification.Refinement;
4+
import liquidjava.specification.StateRefinement;
5+
import liquidjava.specification.StateSet;
6+
7+
@StateSet({"open", "closed"})
8+
class FieldResource {
9+
@StateRefinement(to = "open(this)")
10+
public FieldResource() {}
11+
12+
@StateRefinement(from = "open(this)")
13+
public void read() {}
14+
15+
@StateRefinement(from = "open(this)", to = "closed(this)")
16+
public void close() {}
17+
}
18+
19+
public class ErrorMutableFieldInitializer {
20+
private float q = 0.75f;
21+
private static int count = 1;
22+
private FieldResource resource = new FieldResource();
23+
24+
public void setQ(float v) {
25+
this.q = v;
26+
}
27+
28+
public static void setCount(int c) {
29+
count = c;
30+
}
31+
32+
static void use(@Refinement("_ >= 0.0 && _ <= 1.0") float quality) {}
33+
34+
static void positive(@Refinement("_ > 0") int n) {}
35+
36+
void write() {
37+
use(q); // Expect: Refinement Error
38+
}
39+
40+
void writeThis() {
41+
use(this.q); // Expect: Refinement Error
42+
}
43+
44+
static void report() {
45+
positive(count); // Expect: Refinement Error
46+
}
47+
48+
void closeResource() {
49+
resource.close(); // Expect: State Refinement Error
50+
}
51+
52+
void readResource() {
53+
resource.read(); // Expect: State Refinement Error
54+
}
55+
}

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ public <R> void visitCtMethod(CtMethod<R> method) {
142142
context.clearInstanceVariables();
143143
context.enterContext();
144144
context.restoreInstanceVariables();
145+
forgetMutableFieldValues(method.getDeclaringType());
145146
if (!method.getSignature().equals("main(java.lang.String[])")) {
146147
mfc.loadFunctionInfo(method);
147148
}
@@ -155,6 +156,25 @@ public <R> void visitCtMethod(CtMethod<R> method) {
155156
vcChecker.clearPathVariables();
156157
}
157158

159+
/**
160+
* Any method may have reassigned a non-final field before this one runs, so inside a method a non-final field
161+
* carries only its declared refinement, not the value of its initializer. Final fields keep their value.
162+
*/
163+
private void forgetMutableFieldValues(CtType<?> type) {
164+
if (type == null)
165+
return;
166+
for (CtField<?> f : type.getFields()) {
167+
String name = Utils.qualifyFieldName(f.getReference());
168+
if (f.isFinal() || !(context.getVariableByName(name)instanceof Variable field)
169+
|| field.getLastInstance().isEmpty())
170+
continue;
171+
String instanceName = String.format(Formats.INSTANCE, name, context.getCounter());
172+
context.addInstanceToContext(instanceName, field.getType(),
173+
field.getMainRefinement().substituteVariable(name, instanceName), f);
174+
context.addRefinementInstanceToVariable(name, instanceName);
175+
}
176+
}
177+
158178
@Override
159179
public <T> void visitCtLocalVariable(CtLocalVariable<T> localVariable) {
160180
super.visitCtLocalVariable(localVariable);

0 commit comments

Comments
 (0)