Skip to content

Commit 25cba58

Browse files
Return implies state (#212)
## Description Allow observer methods to give information about the object state. ## Example In refinements allow observers to mention the object state. ```java @stateset({"hasMore", "noMore"}) @ExternalRefinementsFor("java.util.Scanner") public interface IteratorRefinements { @refinement("_ == true --> hasMore(this)") boolean hasNext(); @StateRefinement(from = "hasMore(this)", to = "noMore(this)") // addition String next(); } ``` Client code: ```java public static void readOne(Scanner it) { if (it.hasNext()) { it.next(); // allow } } public static void nextNotInElse(Scanner it) { if (!it.hasNext()) { it.next(); // State Refinement Error } } ``` ## Type of change - [ ] Bug fix - [x] New feature - [ ] Documentation update - [ ] Code refactoring ## Checklist - [x] Added/updated tests under `liquidjava-example/src/main/java/testSuite/` (`Correct*` / `Error*`) - [x] `mvn test` passes locally - [ ] Updated docs/README if behavior or API changed
1 parent cd523df commit 25cba58

9 files changed

Lines changed: 353 additions & 7 deletions

File tree

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package testSuite.classes.iterator_observer_correct;
2+
3+
import liquidjava.specification.ExternalRefinementsFor;
4+
import liquidjava.specification.Refinement;
5+
import liquidjava.specification.StateRefinement;
6+
import liquidjava.specification.StateSet;
7+
8+
/**
9+
* Observer-style iteration over {@link java.util.Scanner} (an iterator-shaped JDK class):
10+
* {@code hasNext} is an informative check (no state change), and {@code next} consumes one
11+
* element, resetting state so a fresh check is required before the next consumption.
12+
*
13+
* <p>{@code java.util.Iterator} itself is an interface, and external refinements only attach to
14+
* classes — Scanner gives us the same API in a class form.
15+
*/
16+
@StateSet({"hasMore", "noMore"})
17+
@ExternalRefinementsFor("java.util.Scanner")
18+
public interface IteratorRefinements {
19+
20+
@Refinement("_ == true --> hasMore(this)")
21+
boolean hasNext();
22+
23+
@StateRefinement(from = "hasMore(this)", to = "noMore(this)")
24+
String next();
25+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package testSuite.classes.iterator_observer_correct;
2+
3+
import java.util.Scanner;
4+
5+
public class IteratorUse {
6+
7+
public static void readOne(Scanner it) {
8+
if (it.hasNext()) {
9+
it.next();
10+
}
11+
}
12+
13+
public static void readTwo(Scanner it) {
14+
if (it.hasNext()) {
15+
it.next();
16+
if (it.hasNext()) {
17+
it.next();
18+
}
19+
}
20+
}
21+
22+
public static void readInElse(Scanner it) {
23+
if (!it.hasNext()) {
24+
} else {
25+
it.next();
26+
}
27+
}
28+
29+
public static void recursive(Scanner it) {
30+
if (it.hasNext()) {
31+
String s = it.next();
32+
System.out.println(s);
33+
recursive(it);
34+
}
35+
}
36+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package testSuite.classes.iterator_observer_error;
2+
3+
import java.util.Scanner;
4+
5+
public class IteratorMisuse {
6+
7+
// No check at all: state of the parameter is unknown.
8+
public static void nextWithoutCheck(Scanner it) {
9+
it.next(); // State Refinement Error
10+
}
11+
12+
// Else branch of hasNext(): condition was false, so we know !hasMore.
13+
public static void nextInElseBranch(Scanner it) {
14+
if (it.hasNext()) {
15+
} else {
16+
it.next(); // State Refinement Error
17+
}
18+
}
19+
20+
// Negated check: !hasNext() true means hasNext returned false, so !hasMore.
21+
public static void nextNotInElse(Scanner it) {
22+
if (!it.hasNext()) {
23+
it.next(); // State Refinement Error
24+
}
25+
}
26+
27+
// After consuming with next(), state becomes noMore — a second next() in the same
28+
// then-branch must fail.
29+
public static void doubleNextInThen(Scanner it) {
30+
if (it.hasNext()) {
31+
it.next();
32+
it.next(); // State Refinement Error
33+
}
34+
}
35+
36+
// Empty if-then: the bug we fixed in visitCtIf would have masked this join, leaking the
37+
// path-variable's truthiness past the if and silently asserting hasMore.
38+
public static void nextAfterEmptyIf(Scanner it) {
39+
if (it.hasNext()) {
40+
}
41+
it.next(); // State Refinement Error
42+
}
43+
44+
// Sequential ifs: state is consumed by the first then-branch's next(), and the second if's
45+
// path-variable assertion must not leak past its join.
46+
public static void sequentialIfsLoseState(Scanner it) {
47+
if (it.hasNext()) {
48+
it.next();
49+
}
50+
if (it.hasNext()) {
51+
}
52+
it.next(); // State Refinement Error
53+
}
54+
55+
// Empty if + empty else: neither branch establishes hasMore.
56+
public static void nextAfterEmptyIfElse(Scanner it) {
57+
if (it.hasNext()) {
58+
} else {
59+
}
60+
it.next(); // State Refinement Error
61+
}
62+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package testSuite.classes.iterator_observer_error;
2+
3+
import liquidjava.specification.ExternalRefinementsFor;
4+
import liquidjava.specification.Refinement;
5+
import liquidjava.specification.StateRefinement;
6+
import liquidjava.specification.StateSet;
7+
8+
/**
9+
* Observer-style iteration over {@link java.util.Scanner} (an iterator-shaped JDK class):
10+
* {@code hasNext} is an informative check (no state change), and {@code next} consumes one
11+
* element, resetting state so a fresh check is required before the next consumption.
12+
*
13+
* <p>{@code java.util.Iterator} itself is an interface, and external refinements only attach to
14+
* classes — Scanner gives us the same API in a class form.
15+
*/
16+
@StateSet({"hasMore", "noMore"})
17+
@ExternalRefinementsFor("java.util.Scanner")
18+
public interface IteratorRefinements {
19+
20+
@Refinement("_ == true --> hasMore(this)")
21+
boolean hasNext();
22+
23+
@StateRefinement(from = "hasMore(this)", to = "noMore(this)")
24+
String next();
25+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package testSuite.classes.state_test_method_correct;
2+
3+
import liquidjava.specification.ExternalRefinementsFor;
4+
import liquidjava.specification.Refinement;
5+
import liquidjava.specification.StateRefinement;
6+
import liquidjava.specification.StateSet;
7+
8+
@StateSet({"aliveDone", "aliveNotDone", "notAlive"})
9+
@ExternalRefinementsFor("javax.swing.undo.AbstractUndoableEdit")
10+
public interface AbstractUndoableEditRefinements {
11+
12+
@StateRefinement(to = "aliveDone(this)")
13+
void AbstractUndoableEdit();
14+
15+
@StateRefinement(from = "aliveNotDone(this)", to = "aliveDone(this)")
16+
void redo();
17+
18+
@StateRefinement(from = "aliveDone(this)", to = "aliveNotDone(this)")
19+
void undo();
20+
21+
@StateRefinement(from = "!notAlive(this)", to = "notAlive(this)")
22+
void die();
23+
24+
@Refinement("_ == true --> aliveDone(this)")
25+
boolean canUndo();
26+
27+
@Refinement("_ == true --> aliveNotDone(this)")
28+
boolean canRedo();
29+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package testSuite.classes.state_test_method_correct;
2+
3+
import javax.swing.undo.AbstractUndoableEdit;
4+
5+
public class EditCycler {
6+
public static void undoIfPossible(AbstractUndoableEdit edit) {
7+
if (edit.canUndo()) {
8+
edit.undo();
9+
}
10+
}
11+
12+
public static void redoIfPossible(AbstractUndoableEdit edit) {
13+
if (edit.canRedo()) {
14+
edit.redo();
15+
}
16+
}
17+
18+
public static void redoIfPossible() {
19+
AbstractUndoableEdit edit = new AbstractUndoableEdit();
20+
edit.canRedo();
21+
edit.undo();
22+
}
23+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package testSuite.classes.state_test_method_error;
2+
3+
import liquidjava.specification.ExternalRefinementsFor;
4+
import liquidjava.specification.Refinement;
5+
import liquidjava.specification.StateRefinement;
6+
import liquidjava.specification.StateSet;
7+
8+
@StateSet({"aliveDone", "aliveNotDone", "notAlive"})
9+
@ExternalRefinementsFor("javax.swing.undo.AbstractUndoableEdit")
10+
public interface AbstractUndoableEditRefinements {
11+
12+
@StateRefinement(to = "aliveDone(this)")
13+
void AbstractUndoableEdit();
14+
15+
@StateRefinement(from = "aliveNotDone(this)", to = "aliveDone(this)")
16+
void redo();
17+
18+
@StateRefinement(from = "aliveDone(this)", to = "aliveNotDone(this)")
19+
void undo();
20+
21+
@Refinement("_ == true --> aliveDone(this)")
22+
boolean canUndo();
23+
24+
@Refinement("_ == true --> aliveNotDone(this)")
25+
boolean canRedo();
26+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package testSuite.classes.state_test_method_error;
2+
3+
import javax.swing.undo.AbstractUndoableEdit;
4+
5+
public class EditMisuse {
6+
7+
public static void undoInElseBranch(AbstractUndoableEdit edit) {
8+
if (edit.canUndo()) {
9+
} else {
10+
edit.undo(); // State Refinement Error
11+
}
12+
}
13+
14+
public static void undoNotInElse(AbstractUndoableEdit edit) {
15+
if (!edit.canUndo()) {
16+
edit.undo(); // State Refinement Error
17+
}
18+
}
19+
20+
public static void wrongTesterForRedo(AbstractUndoableEdit edit) {
21+
if (edit.canUndo()) {
22+
edit.redo(); // State Refinement Error
23+
}
24+
}
25+
26+
public static void wrongTester2() {
27+
AbstractUndoableEdit edit = new AbstractUndoableEdit();
28+
edit.undo();
29+
if (edit.canUndo()) {
30+
edit.undo();
31+
}
32+
edit.undo(); // State Refinement Error
33+
}
34+
35+
// Two undos in the same then-branch: condition forces aliveDone for the first, but the second
36+
// is in aliveNotDone.
37+
public static void doubleUndoInThen(AbstractUndoableEdit edit) {
38+
if (edit.canUndo()) {
39+
edit.undo();
40+
edit.undo(); // State Refinement Error
41+
}
42+
}
43+
44+
// Nested ifs: outer canUndo => aliveDone, inner canUndo => aliveDone (still), so calling
45+
// redo() in the inner then is illegal (redo needs aliveNotDone).
46+
public static void nestedIfRedoFromAliveDone(AbstractUndoableEdit edit) {
47+
if (edit.canUndo()) {
48+
if (edit.canUndo()) {
49+
edit.redo(); // State Refinement Error
50+
}
51+
}
52+
}
53+
54+
// Sequential guarded undos with no else: after the first if, state is aliveNotDone in BOTH
55+
// paths (then took canUndo=>aliveDone then undo=>aliveNotDone; else had canUndo=false meaning
56+
// !aliveDone, which can only be aliveNotDone given the state set). The second undo therefore
57+
// cannot succeed — but the verifier must not let the first if's truth assertion leak.
58+
public static void sequentialIfsLoseState() {
59+
AbstractUndoableEdit edit = new AbstractUndoableEdit();
60+
if (edit.canUndo()) {
61+
edit.undo();
62+
}
63+
if (edit.canUndo()) {
64+
}
65+
edit.undo(); // State Refinement Error
66+
}
67+
68+
// Wrong direction: canRedo() implies aliveNotDone, so calling undo() in that branch is illegal.
69+
public static void undoGuardedByCanRedo(AbstractUndoableEdit edit) {
70+
if (edit.canRedo()) {
71+
edit.undo(); // State Refinement Error
72+
}
73+
}
74+
75+
// Redo after undo inside the then: the inner state is aliveDone -> aliveNotDone -> aliveDone
76+
// after redo, so a second redo from aliveDone is illegal.
77+
public static void doubleRedoAfterUndo(AbstractUndoableEdit edit) {
78+
if (edit.canUndo()) {
79+
edit.undo();
80+
edit.redo();
81+
edit.redo(); // State Refinement Error
82+
}
83+
}
84+
85+
// Empty if / empty else: the bug we fixed would have let `pathVar == true` (or false) leak past
86+
// the join, masking violations. Here the constructor leaves edit in aliveDone, neither branch
87+
// changes it, so after the if the state is still aliveDone — calling redo() is illegal.
88+
public static void redoAfterEmptyIfElse() {
89+
AbstractUndoableEdit edit = new AbstractUndoableEdit();
90+
if (edit.canUndo()) {
91+
} else {
92+
}
93+
edit.redo(); // State Refinement Error
94+
}
95+
}

0 commit comments

Comments
 (0)