diff --git a/liquidjava-api/src/main/java/liquidjava/specification/StateSet.java b/liquidjava-api/src/main/java/liquidjava/specification/StateSet.java index 376c071bd..ad074c482 100644 --- a/liquidjava-api/src/main/java/liquidjava/specification/StateSet.java +++ b/liquidjava-api/src/main/java/liquidjava/specification/StateSet.java @@ -9,9 +9,10 @@ /** * Annotation to create a set of disjoint states in which class objects can be. *
- * An object will always be in exactly one state from each state set at any given time, - * and cannot be in more than one state from the same state set (e.g., {@code open} and {@code closed} simultaneously). - * To allow an object to be in multiple states at once, they must be from different state sets. + * An object cannot be in more than one state from the same state set at a time (e.g., {@code open} and + * {@code closed} simultaneously). To allow an object to be in multiple states at once, they must be from different + * state sets. An object may also be in none of the states of a set, e.g. after + * {@code @StateRefinement(to = "!open(this) && !closed(this)")}. *
* Example: *
@@ -21,6 +22,7 @@
* public class File {
* // ...
* }
+ * }
*
*
* @see StateRefinement
diff --git a/liquidjava-example/src/main/java/testSuite/CorrectLoopFreshReceivers.java b/liquidjava-example/src/main/java/testSuite/CorrectLoopFreshReceivers.java
new file mode 100644
index 000000000..e1f1b9c61
--- /dev/null
+++ b/liquidjava-example/src/main/java/testSuite/CorrectLoopFreshReceivers.java
@@ -0,0 +1,32 @@
+package testSuite;
+
+import liquidjava.specification.StateRefinement;
+import liquidjava.specification.StateSet;
+
+// A loop body checked more than once creates fresh receivers in each check, not reusing one of an earlier check
+@StateSet({"open", "marked", "closed"})
+public class CorrectLoopFreshReceivers {
+ @StateRefinement(to = "open(this)")
+ CorrectLoopFreshReceivers() {}
+
+ @StateRefinement(from = "open(this)", to = "marked(this)")
+ @StateRefinement(from = "marked(this)")
+ void mark() {}
+
+ @StateRefinement(from = "marked(this)", to = "open(this)")
+ void reset() {}
+
+ @StateRefinement(from = "!closed(this)", to = "closed(this)")
+ void close() {}
+
+ static void freshReceivers(int n) {
+ CorrectLoopFreshReceivers b = new CorrectLoopFreshReceivers();
+ while (n > 0) {
+ b.mark(); // the body is checked from open, then from open or marked
+ new CorrectLoopFreshReceivers().close();
+ new CorrectLoopFreshReceivers().mark();
+ Runnable r = () -> new CorrectLoopFreshReceivers().close();
+ n--;
+ }
+ }
+}
diff --git a/liquidjava-example/src/main/java/testSuite/CorrectLoopStateFixpoint.java b/liquidjava-example/src/main/java/testSuite/CorrectLoopStateFixpoint.java
new file mode 100644
index 000000000..ed5bcff5d
--- /dev/null
+++ b/liquidjava-example/src/main/java/testSuite/CorrectLoopStateFixpoint.java
@@ -0,0 +1,113 @@
+package testSuite;
+
+import liquidjava.specification.StateRefinement;
+import liquidjava.specification.StateSet;
+
+// The states of an object at the start of every iteration of a loop are those before the loop and those at the end of
+// any iteration, computed until they stop changing
+@StateSet({"open", "marked", "closed"})
+public class CorrectLoopStateFixpoint {
+ @StateRefinement(to = "open(this)")
+ CorrectLoopStateFixpoint() {}
+
+ @StateRefinement(from = "open(this)")
+ @StateRefinement(from = "marked(this)")
+ void read() {}
+
+ @StateRefinement(from = "open(this)", to = "marked(this)")
+ @StateRefinement(from = "marked(this)")
+ void mark() {}
+
+ @StateRefinement(from = "marked(this)", to = "open(this)")
+ void reset() {}
+
+ @StateRefinement(from = "!closed(this)", to = "closed(this)")
+ void close() {}
+
+ // b is open or marked in every iteration: both allow read and mark
+ static void openOrMarked(int n) {
+ CorrectLoopStateFixpoint b = new CorrectLoopStateFixpoint();
+ for (int i = 0; i < n; i++) {
+ b.read();
+ b.mark();
+ }
+ b.read();
+ b.close();
+ }
+
+ // the body restores the state it starts from
+ static void restoredInBody(int n) {
+ CorrectLoopStateFixpoint b = new CorrectLoopStateFixpoint();
+ while (n > 0) {
+ b.mark();
+ b.reset();
+ n--;
+ }
+ b.mark();
+ b.reset();
+ }
+
+ static void nestedLoops(int n) {
+ CorrectLoopStateFixpoint b = new CorrectLoopStateFixpoint();
+ for (int i = 0; i < n; i++) {
+ for (int j = 0; j < i; j++)
+ b.mark();
+ b.read();
+ }
+ b.close();
+ }
+
+ static void doWhile(int n) {
+ CorrectLoopStateFixpoint b = new CorrectLoopStateFixpoint();
+ do {
+ b.mark();
+ n--;
+ } while (n > 0);
+ b.reset();
+ }
+
+ static void forEach(int[] values) {
+ CorrectLoopStateFixpoint b = new CorrectLoopStateFixpoint();
+ for (int value : values) {
+ b.mark();
+ b.read();
+ }
+ b.read();
+ }
+
+ // the only exit is the break, with b marked
+ static void exitOnlyThroughBreak(int n) {
+ CorrectLoopStateFixpoint b = new CorrectLoopStateFixpoint();
+ for (;;) {
+ b.mark();
+ if (n > 3)
+ break;
+ b.reset();
+ n++;
+ }
+ b.reset();
+ }
+
+ // a continue skips the close, which ends the loop with a return
+ static void continueOrClose(int n) {
+ CorrectLoopStateFixpoint b = new CorrectLoopStateFixpoint();
+ for (int i = 0; i < n; i++) {
+ b.read();
+ if (i < 5)
+ continue;
+ b.close();
+ return;
+ }
+ b.close();
+ }
+
+ @StateRefinement(from = "open(this)")
+ void thisOpenOrMarked(int n) {
+ while (n > 0) {
+ read();
+ this.mark();
+ n--;
+ }
+ read();
+ }
+}
diff --git a/liquidjava-example/src/main/java/testSuite/CorrectLoopStateNone.java b/liquidjava-example/src/main/java/testSuite/CorrectLoopStateNone.java
new file mode 100644
index 000000000..8f8698b4e
--- /dev/null
+++ b/liquidjava-example/src/main/java/testSuite/CorrectLoopStateNone.java
@@ -0,0 +1,31 @@
+package testSuite;
+
+import liquidjava.specification.StateRefinement;
+import liquidjava.specification.StateSet;
+
+// An object may be in none of the states of a set, before the loop or after a call in its body
+@StateSet({"open", "closed"})
+public class CorrectLoopStateNone {
+ @StateRefinement(to = "!open(this) && !closed(this)")
+ CorrectLoopStateNone() {}
+
+ @StateRefinement(from = "!open(this) && !closed(this)", to = "open(this)")
+ void open() {}
+
+ @StateRefinement(from = "open(this)", to = "!open(this) && !closed(this)")
+ void read() {}
+
+ @StateRefinement(from = "!closed(this)", to = "closed(this)")
+ void close() {}
+
+ // every iteration starts and ends in no state
+ static void openAndRead(int n) {
+ CorrectLoopStateNone value = new CorrectLoopStateNone();
+ for (int i = 0; i < n; i++) {
+ value.open();
+ value.read();
+ }
+ value.open();
+ value.close();
+ }
+}
diff --git a/liquidjava-example/src/main/java/testSuite/CorrectStateChangeFromUnion.java b/liquidjava-example/src/main/java/testSuite/CorrectStateChangeFromUnion.java
new file mode 100644
index 000000000..804fc9510
--- /dev/null
+++ b/liquidjava-example/src/main/java/testSuite/CorrectStateChangeFromUnion.java
@@ -0,0 +1,38 @@
+package testSuite;
+
+import liquidjava.specification.StateRefinement;
+import liquidjava.specification.StateSet;
+
+// A call whose entry states are each unprovable but whose union is provable is allowed, and the object ends up in a
+// state given by any alternative whose entry state it may be in
+@StateSet({"open", "marked", "closed"})
+public class CorrectStateChangeFromUnion {
+ @StateRefinement(to = "open(this)")
+ CorrectStateChangeFromUnion() {}
+
+ @StateRefinement(from = "open(this)", to = "marked(this)")
+ @StateRefinement(from = "marked(this)")
+ void mark() {}
+
+ @StateRefinement(from = "marked(this)", to = "open(this)")
+ void reset() {}
+
+ @StateRefinement(from = "open(this)", to = "closed(this)")
+ @StateRefinement(from = "marked(this)", to = "closed(this)")
+ void close() {}
+
+ // open goes to marked, marked stays marked
+ @StateRefinement(from = "open(this)")
+ @StateRefinement(from = "marked(this)")
+ void markFromEither() {
+ mark();
+ reset();
+ }
+
+ // both alternatives go to closed
+ @StateRefinement(from = "open(this)")
+ @StateRefinement(from = "marked(this)")
+ void closeFromEither() {
+ close();
+ }
+}
diff --git a/liquidjava-example/src/main/java/testSuite/ErrorLabelledBlockInIf.java b/liquidjava-example/src/main/java/testSuite/ErrorLabelledBlockInIf.java
new file mode 100644
index 000000000..0eca519d5
--- /dev/null
+++ b/liquidjava-example/src/main/java/testSuite/ErrorLabelledBlockInIf.java
@@ -0,0 +1,28 @@
+package testSuite;
+
+import liquidjava.specification.StateRefinement;
+import liquidjava.specification.StateSet;
+
+// A block left by a labelled break completes normally: the state it leaves reaches the code after the if
+@StateSet({"open", "closed"})
+public class ErrorLabelledBlockInIf {
+ @StateRefinement(to = "open(this)")
+ ErrorLabelledBlockInIf() {}
+
+ @StateRefinement(from = "open(this)", to = "closed(this)")
+ void close() {}
+
+ @StateRefinement(from = "open(this)")
+ void read() {}
+
+ static void closedInLabelledBlock(boolean c) {
+ ErrorLabelledBlockInIf b = new ErrorLabelledBlockInIf();
+ if (c) {
+ done: {
+ b.close();
+ break done;
+ }
+ }
+ b.read(); // Expect: State Refinement Error
+ }
+}
diff --git a/liquidjava-example/src/main/java/testSuite/ErrorLoopFreshReceivers.java b/liquidjava-example/src/main/java/testSuite/ErrorLoopFreshReceivers.java
new file mode 100644
index 000000000..baa585ffa
--- /dev/null
+++ b/liquidjava-example/src/main/java/testSuite/ErrorLoopFreshReceivers.java
@@ -0,0 +1,31 @@
+package testSuite;
+
+import liquidjava.specification.StateRefinement;
+import liquidjava.specification.StateSet;
+
+// A loop body checked more than once creates fresh receivers in each check, not reusing one of an earlier check
+@StateSet({"open", "marked", "closed"})
+public class ErrorLoopFreshReceivers {
+ @StateRefinement(to = "open(this)")
+ ErrorLoopFreshReceivers() {}
+
+ @StateRefinement(from = "open(this)", to = "marked(this)")
+ @StateRefinement(from = "marked(this)")
+ void mark() {}
+
+ @StateRefinement(from = "marked(this)", to = "open(this)")
+ void reset() {}
+
+ @StateRefinement(from = "!closed(this)", to = "closed(this)")
+ void close() {}
+
+ // a fresh object is open, and reset needs it marked
+ static void freshReceiverInLambda(int n) {
+ ErrorLoopFreshReceivers b = new ErrorLoopFreshReceivers();
+ while (n > 0) {
+ b.mark(); // the body is checked from open, then from open or marked
+ Runnable r = () -> new ErrorLoopFreshReceivers().reset(); // Expect: State Refinement Error
+ n--;
+ }
+ }
+}
diff --git a/liquidjava-example/src/main/java/testSuite/ErrorLoopStateFixpoint.java b/liquidjava-example/src/main/java/testSuite/ErrorLoopStateFixpoint.java
new file mode 100644
index 000000000..8e3e520ec
--- /dev/null
+++ b/liquidjava-example/src/main/java/testSuite/ErrorLoopStateFixpoint.java
@@ -0,0 +1,53 @@
+package testSuite;
+
+import liquidjava.specification.StateRefinement;
+import liquidjava.specification.StateSet;
+
+// The states of an object at the start of every iteration of a loop are those before the loop and those at the end of
+// any iteration, computed until they stop changing
+@StateSet({"first", "second", "third"})
+public class ErrorLoopStateFixpoint {
+ @StateRefinement(to = "first(this)")
+ ErrorLoopStateFixpoint() {}
+
+ @StateRefinement(from = "first(this)", to = "second(this)")
+ @StateRefinement(from = "second(this)", to = "third(this)")
+ @StateRefinement(from = "third(this)")
+ void step() {}
+
+ @StateRefinement(from = "first(this)")
+ @StateRefinement(from = "second(this)")
+ void early() {}
+
+ @StateRefinement(from = "third(this)")
+ void last() {}
+
+ // third is reached in the third iteration only, after the states grow twice
+ static void thirdIteration(int n) {
+ ErrorLoopStateFixpoint o = new ErrorLoopStateFixpoint();
+ for (int i = 0; i < n; i++) {
+ o.early(); // Expect: State Refinement Error
+ o.step();
+ }
+ }
+
+ // after two steps in the loop, o may still be first or second
+ static void notEnoughIterations(int n) {
+ ErrorLoopStateFixpoint o = new ErrorLoopStateFixpoint();
+ while (n > 0) {
+ o.step();
+ o.step();
+ n--;
+ }
+ o.last(); // Expect: State Refinement Error
+ }
+
+ // the error in the lambda is reported once, although the loop body is checked from more than one set of states
+ static void errorInLambdaInLoop(int n) {
+ ErrorLoopStateFixpoint o = new ErrorLoopStateFixpoint();
+ for (int i = 0; i < n; i++) {
+ o.step();
+ Runnable r = () -> o.last(); // Expect: State Refinement Error
+ }
+ }
+}
diff --git a/liquidjava-example/src/main/java/testSuite/ErrorLoopStateJumps.java b/liquidjava-example/src/main/java/testSuite/ErrorLoopStateJumps.java
new file mode 100644
index 000000000..dd82fb1f7
--- /dev/null
+++ b/liquidjava-example/src/main/java/testSuite/ErrorLoopStateJumps.java
@@ -0,0 +1,77 @@
+package testSuite;
+
+import liquidjava.specification.StateRefinement;
+import liquidjava.specification.StateSet;
+
+// Loops with jumps whose states are not followed (a break to a labelled block, or through a finally) havoc the
+// typestates of the objects they change, as before the fixpoint: these tests pin that fallback
+@StateSet({"open", "marked", "closed"})
+public class ErrorLoopStateJumps {
+ @StateRefinement(to = "open(this)")
+ ErrorLoopStateJumps() {}
+
+ @StateRefinement(from = "open(this)", to = "marked(this)")
+ @StateRefinement(from = "marked(this)")
+ void mark() {}
+
+ @StateRefinement(from = "marked(this)", to = "open(this)")
+ void reset() {}
+
+ @StateRefinement(from = "!closed(this)", to = "closed(this)")
+ void close() {}
+
+ @StateRefinement(from = "open(this)")
+ void needOpen() {}
+
+ // break done: the state of b is unknown in the body (the fixpoint would find it closed in the second iteration)
+ static void labelledBlockEndsBody(int n) {
+ ErrorLoopStateJumps b = new ErrorLoopStateJumps();
+ while (n > 0) {
+ b.close(); // Expect: State Refinement Error
+ n--;
+ done: {
+ break done;
+ }
+ }
+ }
+
+ static void labelledBlockEndsDoWhileBody(int n) {
+ ErrorLoopStateJumps b = new ErrorLoopStateJumps();
+ do {
+ b.close(); // Expect: State Refinement Error
+ n--;
+ done: {
+ break done;
+ }
+ } while (n > 0);
+ }
+
+ // break out: the state of b is unknown in the body (the fixpoint would miss the marked state at the break)
+ static void breakOutOfEnclosingBlock(int n) {
+ ErrorLoopStateJumps b = new ErrorLoopStateJumps();
+ out: {
+ while (n > 0) {
+ b.mark(); // Expect: State Refinement Error
+ if (n == 3)
+ break out;
+ b.reset();
+ n--;
+ }
+ }
+ b.needOpen();
+ }
+
+ // a break through a finally: the state of b is unknown in the body (the finally closes b after the break)
+ static void breakThroughFinally() {
+ ErrorLoopStateJumps b = new ErrorLoopStateJumps();
+ while (true) {
+ try {
+ b.mark(); // Expect: State Refinement Error
+ break;
+ } finally {
+ b.close();
+ }
+ }
+ b.reset();
+ }
+}
diff --git a/liquidjava-example/src/main/java/testSuite/ErrorLoopStateNone.java b/liquidjava-example/src/main/java/testSuite/ErrorLoopStateNone.java
new file mode 100644
index 000000000..dc699592f
--- /dev/null
+++ b/liquidjava-example/src/main/java/testSuite/ErrorLoopStateNone.java
@@ -0,0 +1,35 @@
+package testSuite;
+
+import liquidjava.specification.StateRefinement;
+import liquidjava.specification.StateSet;
+
+// An object may be in none of the states of a set, before the loop or after a call in its body
+@StateSet({"open", "closed"})
+public class ErrorLoopStateNone {
+ @StateRefinement(to = "!open(this) && !closed(this)")
+ ErrorLoopStateNone() {}
+
+ @StateRefinement(from = "!open(this) && !closed(this)", to = "open(this)")
+ void open() {}
+
+ @StateRefinement(from = "open(this)", to = "!open(this) && !closed(this)")
+ void read() {}
+
+ // value is in no state before the loop, as after the constructor
+ static void noneBeforeLoop(int count) {
+ ErrorLoopStateNone value = new ErrorLoopStateNone();
+ while (count > 0) {
+ value.read(); // Expect: State Refinement Error
+ count--;
+ }
+ }
+
+ // read leaves value in no state, so the second iteration cannot read
+ static void noneInSecondIteration(int n) {
+ ErrorLoopStateNone value = new ErrorLoopStateNone();
+ value.open();
+ for (int i = 0; i < n; i++) {
+ value.read(); // Expect: State Refinement Error
+ }
+ }
+}
diff --git a/liquidjava-example/src/main/java/testSuite/ErrorLoopStatePasses.java b/liquidjava-example/src/main/java/testSuite/ErrorLoopStatePasses.java
new file mode 100644
index 000000000..4d07a1f53
--- /dev/null
+++ b/liquidjava-example/src/main/java/testSuite/ErrorLoopStatePasses.java
@@ -0,0 +1,49 @@
+package testSuite;
+
+import liquidjava.specification.Refinement;
+import liquidjava.specification.StateRefinement;
+import liquidjava.specification.StateSet;
+
+// Each check of a loop body starts from the same facts: the values left by an earlier check must not leak into it
+@StateSet({"open", "closed"})
+public class ErrorLoopStatePasses {
+ @StateRefinement(to = "open(this)")
+ ErrorLoopStatePasses() {}
+
+ @StateRefinement(to = "closed(this)")
+ void close() {}
+
+ @StateRefinement(from = "closed(this)", to = "open(this)")
+ void reopen() {}
+
+ @StateRefinement(from = "open(this)", to = "closed(this)")
+ @StateRefinement(from = "closed(this)", to = "open(this)")
+ void toggle() {}
+
+ // the loop is never entered, so b is still open; x == 0 from the previous check must not make the exit impossible
+ static void neverEntered() {
+ ErrorLoopStatePasses b = new ErrorLoopStatePasses();
+ int x = 100;
+ while (x < 10) {
+ b.close();
+ x = 0;
+ }
+ b.reopen(); // Expect: State Refinement Error
+ }
+
+ // in the first iteration y == 0; x == 7 from the previous check must not hide the error in the lambda
+ static void lambdaInFirstIteration(int n) {
+ ErrorLoopStatePasses t = new ErrorLoopStatePasses();
+ int x = 0;
+ while (n > 0) {
+ t.toggle();
+ final int y = x;
+ Runnable r = () -> {
+ @Refinement("_ == 7")
+ int z = y; // Expect: Refinement Error
+ };
+ x = 7;
+ n--;
+ }
+ }
+}
diff --git a/liquidjava-example/src/main/java/testSuite/ErrorStateChangeFromUnion.java b/liquidjava-example/src/main/java/testSuite/ErrorStateChangeFromUnion.java
new file mode 100644
index 000000000..4d5182472
--- /dev/null
+++ b/liquidjava-example/src/main/java/testSuite/ErrorStateChangeFromUnion.java
@@ -0,0 +1,36 @@
+package testSuite;
+
+import liquidjava.specification.StateRefinement;
+import liquidjava.specification.StateSet;
+
+@StateSet({"open", "marked", "closed"})
+public class ErrorStateChangeFromUnion {
+ @StateRefinement(to = "open(this)")
+ ErrorStateChangeFromUnion() {}
+
+ @StateRefinement(from = "open(this)", to = "marked(this)")
+ @StateRefinement(from = "marked(this)")
+ void mark() {}
+
+ @StateRefinement(from = "marked(this)", to = "open(this)")
+ void reset() {}
+
+ @StateRefinement(from = "open(this)", to = "closed(this)")
+ @StateRefinement(from = "marked(this)")
+ void closeIfOpen() {}
+
+ // closed is in no entry state of mark
+ @StateRefinement(from = "open(this)")
+ @StateRefinement(from = "closed(this)")
+ void markFromOpenOrClosed() {
+ mark(); // Expect: State Refinement Error
+ }
+
+ // after closeIfOpen, the object is closed or still marked, so reset is not allowed
+ @StateRefinement(from = "open(this)")
+ @StateRefinement(from = "marked(this)")
+ void resetAfterEither() {
+ closeIfOpen();
+ reset(); // Expect: State Refinement Error
+ }
+}
diff --git a/liquidjava-example/src/main/java/testSuite/classes/loop_state_external_correct/BufferedReaderRefinements.java b/liquidjava-example/src/main/java/testSuite/classes/loop_state_external_correct/BufferedReaderRefinements.java
new file mode 100644
index 000000000..4e0b9fcf1
--- /dev/null
+++ b/liquidjava-example/src/main/java/testSuite/classes/loop_state_external_correct/BufferedReaderRefinements.java
@@ -0,0 +1,26 @@
+package testSuite.classes.loop_state_external_correct;
+
+import liquidjava.specification.ExternalRefinementsFor;
+import liquidjava.specification.StateRefinement;
+import liquidjava.specification.StateSet;
+
+@ExternalRefinementsFor("java.io.BufferedReader")
+@StateSet({"open", "marked", "closed"})
+public interface BufferedReaderRefinements {
+ @StateRefinement(to = "open(this)")
+ void BufferedReader(java.io.Reader in);
+
+ @StateRefinement(from = "open(this)")
+ @StateRefinement(from = "marked(this)")
+ String readLine();
+
+ @StateRefinement(from = "open(this)", to = "marked(this)")
+ @StateRefinement(from = "marked(this)")
+ void mark(int readAheadLimit);
+
+ @StateRefinement(from = "marked(this)", to = "open(this)")
+ void reset();
+
+ @StateRefinement(from = "!closed(this)", to = "closed(this)")
+ void close();
+}
diff --git a/liquidjava-example/src/main/java/testSuite/classes/loop_state_external_correct/Test.java b/liquidjava-example/src/main/java/testSuite/classes/loop_state_external_correct/Test.java
new file mode 100644
index 000000000..b3bed7f83
--- /dev/null
+++ b/liquidjava-example/src/main/java/testSuite/classes/loop_state_external_correct/Test.java
@@ -0,0 +1,106 @@
+package testSuite.classes.loop_state_external_correct;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.Reader;
+
+// Issue #427: the states of an object whose class has an external spec, computed over the iterations of a loop
+public class Test {
+ // the body restores the state it starts from
+ static void markAndResetInLoop(Reader in, int n) throws IOException {
+ BufferedReader br = new BufferedReader(in);
+ int k = 0;
+ while (k < n) {
+ br.mark(100);
+ br.readLine();
+ br.reset();
+ k++;
+ }
+ br.mark(100);
+ br.reset();
+ br.close();
+ }
+
+ // br is open or marked at the start of every iteration and after the loop
+ static void openOrMarked(Reader in, int n) throws IOException {
+ BufferedReader br = new BufferedReader(in);
+ for (int i = 0; i < n; i++) {
+ br.readLine();
+ br.mark(100);
+ }
+ br.readLine();
+ br.close();
+ }
+
+ static void doWhile(Reader in, int n) throws IOException {
+ BufferedReader br = new BufferedReader(in);
+ int k = 0;
+ do {
+ br.mark(100);
+ br.reset();
+ k++;
+ } while (k < n);
+ br.close();
+ }
+
+ static void forEach(Reader in, int[] values) throws IOException {
+ BufferedReader br = new BufferedReader(in);
+ for (int value : values) {
+ br.mark(value);
+ br.readLine();
+ }
+ br.readLine();
+ br.close();
+ }
+
+ // marked on every path to the next iteration and to the exit
+ static void markBeforeContinueAndBreak(Reader in, int n) throws IOException {
+ BufferedReader br = new BufferedReader(in);
+ br.mark(100);
+ for (int i = 0; i < n; i++) {
+ br.reset();
+ br.mark(100);
+ if (i > 2)
+ continue;
+ if (i > 5)
+ break;
+ }
+ br.reset();
+ br.close();
+ }
+
+ // the inner loop leaves br open or marked, which the outer loop allows
+ static void nestedLoops(Reader in, int n) throws IOException {
+ BufferedReader br = new BufferedReader(in);
+ for (int i = 0; i < n; i++) {
+ br.readLine();
+ for (int j = 0; j < i; j++)
+ br.mark(100);
+ }
+ br.close();
+ }
+
+ // the fix of the tabu-timetabling bug: mark before the inner loop
+ static void markBeforeInnerLoop(Reader in) throws IOException {
+ BufferedReader br = new BufferedReader(in);
+ String currentLine;
+ while ((currentLine = br.readLine()) != null) {
+ br.mark(10000);
+ String nextLine;
+ while ((nextLine = br.readLine()) != null && currentLine.equals(nextLine)) {
+ br.mark(10000);
+ }
+ br.reset();
+ }
+ br.close();
+ }
+
+ // closed only when the loop ends
+ static void closeAfterLoop(Reader in) throws IOException {
+ BufferedReader br = new BufferedReader(in);
+ while (br.readLine() != null) {
+ br.readLine();
+ }
+ br.close();
+ }
+}
diff --git a/liquidjava-example/src/main/java/testSuite/classes/loop_state_external_error/BufferedReaderRefinements.java b/liquidjava-example/src/main/java/testSuite/classes/loop_state_external_error/BufferedReaderRefinements.java
new file mode 100644
index 000000000..d23446171
--- /dev/null
+++ b/liquidjava-example/src/main/java/testSuite/classes/loop_state_external_error/BufferedReaderRefinements.java
@@ -0,0 +1,26 @@
+package testSuite.classes.loop_state_external_error;
+
+import liquidjava.specification.ExternalRefinementsFor;
+import liquidjava.specification.StateRefinement;
+import liquidjava.specification.StateSet;
+
+@ExternalRefinementsFor("java.io.BufferedReader")
+@StateSet({"open", "marked", "closed"})
+public interface BufferedReaderRefinements {
+ @StateRefinement(to = "open(this)")
+ void BufferedReader(java.io.Reader in);
+
+ @StateRefinement(from = "open(this)")
+ @StateRefinement(from = "marked(this)")
+ String readLine();
+
+ @StateRefinement(from = "open(this)", to = "marked(this)")
+ @StateRefinement(from = "marked(this)")
+ void mark(int readAheadLimit);
+
+ @StateRefinement(from = "marked(this)", to = "open(this)")
+ void reset();
+
+ @StateRefinement(from = "!closed(this)", to = "closed(this)")
+ void close();
+}
diff --git a/liquidjava-example/src/main/java/testSuite/classes/loop_state_external_error/Test.java b/liquidjava-example/src/main/java/testSuite/classes/loop_state_external_error/Test.java
new file mode 100644
index 000000000..ae7a9d095
--- /dev/null
+++ b/liquidjava-example/src/main/java/testSuite/classes/loop_state_external_error/Test.java
@@ -0,0 +1,98 @@
+package testSuite.classes.loop_state_external_error;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.Reader;
+
+// Issue #427: the state after a loop, and at the start of every iteration, of an object whose class has an external
+// spec
+public class Test {
+ // the loop may run zero times, leaving br open
+ static void zeroIterations(Reader in, int n) throws IOException {
+ BufferedReader br = new BufferedReader(in);
+ int k = 0;
+ while (k < n) {
+ br.mark(100);
+ k++;
+ }
+ br.reset(); // Expect: State Refinement Error
+ }
+
+ // from the second iteration on, br is closed
+ static void laterIteration(Reader in, int n) throws IOException {
+ BufferedReader br = new BufferedReader(in);
+ for (int i = 0; i < n; i++) {
+ br.readLine(); // Expect: State Refinement Error
+ br.close();
+ }
+ }
+
+ static void doWhileLaterIteration(Reader in, int n) throws IOException {
+ BufferedReader br = new BufferedReader(in);
+ int k = 0;
+ do {
+ br.readLine(); // Expect: State Refinement Error
+ br.close();
+ k++;
+ } while (k < n);
+ }
+
+ static void forEachMayBeEmpty(Reader in, int[] values) throws IOException {
+ BufferedReader br = new BufferedReader(in);
+ for (int value : values) {
+ br.mark(value);
+ }
+ br.reset(); // Expect: State Refinement Error
+ }
+
+ // a continue goes on to the next iteration with br closed
+ static void closedBeforeContinue(Reader in, int n) throws IOException {
+ BufferedReader br = new BufferedReader(in);
+ for (int i = 0; i < n; i++) {
+ br.readLine(); // Expect: State Refinement Error
+ if (i > 2) {
+ br.close();
+ continue;
+ }
+ }
+ }
+
+ // the loop exits through its condition with br open, or through the break with br marked
+ static void breakOrCondition(Reader in, int n) throws IOException {
+ BufferedReader br = new BufferedReader(in);
+ int k = 0;
+ while (k < n) {
+ br.mark(100);
+ if (k > 2)
+ break;
+ br.reset();
+ k++;
+ }
+ br.reset(); // Expect: State Refinement Error
+ }
+
+ // marked by an inner loop, which may run zero times, in the second iteration of the outer one
+ static void nestedLoops(Reader in, int n) throws IOException {
+ BufferedReader br = new BufferedReader(in);
+ for (int i = 0; i < n; i++) {
+ br.reset(); // Expect: State Refinement Error
+ for (int j = 0; j < i; j++)
+ br.mark(100);
+ }
+ }
+
+ // tabu-timetabling InputReader: the inner loop may stop at once, when the next line belongs to another student,
+ // and then reset runs without a mark ("Stream not marked")
+ static void markOnlyInInnerLoop(Reader in) throws IOException {
+ BufferedReader br = new BufferedReader(in);
+ String currentLine;
+ while ((currentLine = br.readLine()) != null) {
+ String nextLine;
+ while ((nextLine = br.readLine()) != null && currentLine.equals(nextLine)) {
+ br.mark(10000);
+ }
+ br.reset(); // Expect: State Refinement Error
+ }
+ br.close();
+ }
+}
diff --git a/liquidjava-verifier/src/main/java/liquidjava/processor/refinement_checker/LoopTypestates.java b/liquidjava-verifier/src/main/java/liquidjava/processor/refinement_checker/LoopTypestates.java
new file mode 100644
index 000000000..96d20235c
--- /dev/null
+++ b/liquidjava-verifier/src/main/java/liquidjava/processor/refinement_checker/LoopTypestates.java
@@ -0,0 +1,172 @@
+package liquidjava.processor.refinement_checker;
+
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+import liquidjava.diagnostics.errors.LJError;
+import liquidjava.processor.context.RefinedVariable;
+import liquidjava.processor.context.Variable;
+import liquidjava.processor.context.VariableInstance;
+import liquidjava.processor.state_machine.State;
+import liquidjava.processor.state_machine.StateMachineBuilder;
+import liquidjava.processor.state_machine.StateSolver;
+import liquidjava.rj_language.Predicate;
+import liquidjava.utils.constants.Keys;
+import liquidjava.utils.constants.Types;
+import spoon.reflect.code.CtLoop;
+
+/**
+ * The typestates of the objects a loop may change, as sets of the states of their class. The states at the loop head
+ * start as the states before the loop and grow with the states at the end of each checked iteration until they stop
+ * changing. A class has finitely many states, so they stop changing after at most one growth per state.
+ *
+ *
+ * While the body is checked, the states where control leaves the iteration are recorded: where it may exit the loop
+ * (the condition, a break), and where it goes on to the next iteration (the end of the body, a continue).
+ */
+class LoopTypestates {
+
+ private final RefinementTypeChecker checker;
+ private final CtLoop loop;
+ private final Map
+ * A statement that a labelled {@code break} leaves completes normally, since execution goes on after it.
+ *
+ *
* Not currently handled (treated conservatively as completing normally): {@code switch} where every case exits,
- * labeled {@code break}/{@code continue} targets, {@code try}/{@code catch}/{@code finally} flow, and infinite
- * loops such as {@code while (true)}. Extending this list only tightens precision.
+ * {@code try}/{@code catch}/{@code finally} flow, and infinite loops such as {@code while (true)}. Extending this
+ * list only tightens precision.
*/
boolean canCompleteNormally(CtStatement statement) {
- if (statement == null)
+ if (statement == null || isLeftByLabelledBreak(statement))
return true;
if (statement instanceof CtReturn> || statement instanceof CtThrow || statement instanceof CtBreak
|| statement instanceof CtContinue)
@@ -652,6 +667,11 @@ boolean canCompleteNormally(CtStatement statement) {
return true;
}
+ private static boolean isLeftByLabelledBreak(CtStatement statement) {
+ return statement.getLabel() != null && statement.getElements(new TypeFilter<>(CtBreak.class)).stream()
+ .anyMatch(jump -> jump.getTargetLabel() != null && jump.getLabelledStatement() == statement);
+ }
+
@Override
public void visitCtTry(CtTry tryBlock) {
tryChecker.visitTry(tryBlock);
@@ -664,27 +684,22 @@ public void visitCtTryWithResource(CtTryWithResource tryWithResource) {
@Override
public void visitCtWhile(CtWhile whileLoop) {
- visitLoop(whileLoop, () -> {
+ visitLoop(whileLoop, states -> {
scan(whileLoop.getLoopingExpression());
+ recordExit(states, whileLoop.getLoopingExpression());
assumeLoopCondition(whileLoop.getLoopingExpression());
- scan(whileLoop.getBody());
+ scanBody(whileLoop, states);
});
}
@Override
public void visitCtFor(CtFor forLoop) {
scan(forLoop.getForInit());
- visitLoop(forLoop, () -> {
+ visitLoop(forLoop, states -> {
scan(forLoop.getExpression());
+ recordExit(states, forLoop.getExpression());
assumeLoopCondition(forLoop.getExpression());
- List
+ * An earlier check starts from fewer states than the last one, so an error that stops it is also an error of the
+ * last one. Only the last check keeps the errors recorded without stopping it (e.g. in a lambda).
*/
- private void havocChangedIn(CtLoop loop) {
+ private void checkUntilFixpoint(LoopTypestates states, Consumer