Skip to content

Commit b8cd48c

Browse files
Point out the argument that decided a state a call does not allow
Each object now remembers the last call that changed its state, through constructors and state-preserving calls. When a call fails, the verifier follows that chain back to the last call that changed a state set the failing call checks. If that call's transition depends on its arguments or result, and the SMT solver finds values for which the failing call would have been allowed, the error gets the "Argument picks the state" category and a hint with those values. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent c1673bb commit b8cd48c

16 files changed

Lines changed: 348 additions & 9 deletions

File tree

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package testSuite;
2+
3+
import liquidjava.specification.StateRefinement;
4+
import liquidjava.specification.StateSet;
5+
6+
@StateSet({"empty", "filled", "closed"})
7+
public class ErrorArgumentCannotPickTheState {
8+
9+
@StateRefinement(to = "empty(this)")
10+
public ErrorArgumentCannotPickTheState() {}
11+
12+
@StateRefinement(from = "!closed(this)", to = "keep ? filled(this) : empty(this)")
13+
public void reset(boolean keep) {}
14+
15+
@StateRefinement(from = "closed(this)")
16+
public void reopen() {}
17+
18+
// no value of keep closes the object, so the argument is not what went wrong
19+
public static void use() {
20+
ErrorArgumentCannotPickTheState buffer = new ErrorArgumentCannotPickTheState();
21+
buffer.reset(true);
22+
buffer.reopen(); // Expect: State Refinement Error
23+
}
24+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package testSuite;
2+
3+
import liquidjava.specification.StateRefinement;
4+
import liquidjava.specification.StateSet;
5+
6+
@StateSet({"empty", "filled", "closed"})
7+
public class ErrorArgumentPicksTheState {
8+
9+
@StateRefinement(to = "empty(this)")
10+
public ErrorArgumentPicksTheState() {}
11+
12+
@StateRefinement(from = "!closed(this)", to = "keep ? filled(this) : empty(this)")
13+
public void reset(boolean keep) {}
14+
15+
@StateRefinement(from = "filled(this)")
16+
public void read() {}
17+
18+
public static void use() {
19+
ErrorArgumentPicksTheState buffer = new ErrorArgumentPicksTheState();
20+
buffer.reset(false);
21+
buffer.read(); // Expect: State Refinement Error
22+
}
23+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package testSuite;
2+
3+
import liquidjava.specification.StateRefinement;
4+
import liquidjava.specification.StateSet;
5+
6+
@StateSet({"tilingOff", "tilingOn"})
7+
@StateSet({"uncompressed", "compressed"})
8+
public class ErrorArgumentPicksTheStateEarlier {
9+
10+
@StateRefinement(to = "tilingOff(this) && uncompressed(this)")
11+
public ErrorArgumentPicksTheStateEarlier() {}
12+
13+
@StateRefinement(to = "mode == 2 ? tilingOn(this) : tilingOff(this)")
14+
public void setTilingMode(int mode) {}
15+
16+
@StateRefinement(from = "uncompressed(this)", to = "compressed(this)")
17+
public void compress() {}
18+
19+
@StateRefinement(from = "tilingOn(this)")
20+
public void setTiling() {}
21+
22+
// compress() changes another state set, so the tiling mode is what decided the state
23+
public static void use() {
24+
ErrorArgumentPicksTheStateEarlier param = new ErrorArgumentPicksTheStateEarlier();
25+
param.setTilingMode(1);
26+
param.compress();
27+
param.setTiling(); // Expect: State Refinement Error
28+
}
29+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package testSuite;
2+
3+
import liquidjava.specification.StateRefinement;
4+
import liquidjava.specification.StateSet;
5+
6+
@StateSet({"readOnly", "writable"})
7+
public class ErrorConstructorArgumentPicksTheState {
8+
9+
@StateRefinement(to = "write ? writable(this) : readOnly(this)")
10+
public ErrorConstructorArgumentPicksTheState(boolean write) {}
11+
12+
@StateRefinement(from = "writable(this)")
13+
public void append() {}
14+
15+
public static void use() {
16+
ErrorConstructorArgumentPicksTheState file = new ErrorConstructorArgumentPicksTheState(false);
17+
file.append(); // Expect: State Refinement Error
18+
}
19+
}

‎liquidjava-verifier/src/main/java/liquidjava/diagnostics/categories/ErrorCategory.java‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ public enum ErrorCategory {
88

99
OUT_OF_ORDER("A1", "Out of order: the operation is legal for this object, but not right now"),
1010
OUT_OF_DOMAIN("A2", "Out of domain: the value is outside what is accepted"),
11+
ARGUMENT_PICKS_THE_STATE("C3.1", "Argument picks the state: the value passed decides where the object lands"),
1112
PATH_DEPENDENT_STATE("C4.1", "Path-dependent state: the state differs depending on which branch ran"),
1213
UNPROVABLE("C4.2", "Unprovable: nothing is known that could prove the requirement");
1314

‎liquidjava-verifier/src/main/java/liquidjava/diagnostics/hints/Hint.java‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ public record Hint(Kind kind, String message) {
1313
public enum Kind {
1414
/** A suggestion that does not come from a specific analysis */
1515
GENERAL,
16+
/** An earlier call whose arguments decided the state, and values that would allow the failing call */
17+
DECIDING_ARGUMENT,
1618
/** Calls to add before the failing call so the object reaches a state that allows it */
1719
CALL_SEQUENCE,
1820
/** The calls allowed in the object's current state */

‎liquidjava-verifier/src/main/java/liquidjava/diagnostics/hints/StateHints.java‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@
1313
import liquidjava.diagnostics.hints.Hint.Kind;
1414
import liquidjava.processor.context.RefinedFunction;
1515
import liquidjava.processor.context.Variable;
16+
import liquidjava.processor.state_machine.DecidingArgument;
1617
import liquidjava.processor.state_machine.State;
1718
import liquidjava.processor.state_machine.StateMachine;
1819
import liquidjava.processor.state_machine.Transition;
1920
import liquidjava.utils.Utils;
21+
import spoon.reflect.reference.CtExecutableReference;
2022
import spoon.reflect.reference.CtTypeReference;
2123

2224
/**
@@ -59,6 +61,18 @@ public static List<Hint> forCall(StateMachine machine, RefinedFunction call, Set
5961
.toList();
6062
}
6163

64+
/** The earlier call whose arguments decided the receiver's state, and values that would allow the call */
65+
public static Hint decidingArgument(DecidingArgument deciding, String receiver) {
66+
RefinedFunction function = deciding.call().function();
67+
String call = CtExecutableReference.CONSTRUCTOR_NAME.equals(function.getName()) ? constructor(function)
68+
: receiver + "." + signature(function);
69+
String values = deciding.values().isEmpty() ? "other arguments" : deciding.values().entrySet().stream()
70+
.map(e -> e.getKey() + " == " + e.getValue()).collect(Collectors.joining(" and "));
71+
return new Hint(Kind.DECIDING_ARGUMENT,
72+
String.format("%s on line %d decides the state of %s: with %s, this call would be allowed", call,
73+
deciding.call().position().getLine(), receiver, values));
74+
}
75+
6276
/** Nothing is known about the receiver's state: list what leads to a state where {@code call} is allowed */
6377
private List<Hint> reachingCalls(RefinedFunction call, Set<State> goal) {
6478
Map<RefinedFunction, List<State>> sources = new LinkedHashMap<>();
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package liquidjava.processor.context;
2+
3+
import java.util.Optional;
4+
5+
import spoon.reflect.cu.SourcePosition;
6+
7+
/**
8+
* The call that put an object in its current state
9+
*
10+
* @param function
11+
* the method or constructor called
12+
* @param change
13+
* the state refinement of {@code function} that applied
14+
* @param before
15+
* the object before the call, or {@code null} for a constructor
16+
* @param position
17+
* where the call is
18+
*/
19+
public record StateChange(RefinedFunction function, ObjectState change, VariableInstance before,
20+
SourcePosition position) {
21+
22+
public Optional<VariableInstance> previous() {
23+
return Optional.ofNullable(before);
24+
}
25+
}

‎liquidjava-verifier/src/main/java/liquidjava/processor/context/VariableInstance.java‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
public class VariableInstance extends RefinedVariable {
88

99
private Variable parent;
10+
private StateChange stateChange;
1011

1112
public VariableInstance(String name, CtTypeReference<?> type, Predicate c) {
1213
super(name, type, c);
@@ -33,6 +34,15 @@ public void setParent(Variable p) {
3334
parent = p;
3435
}
3536

37+
/** The last call that changed the state of this object, if it is known */
38+
public Optional<StateChange> getStateChange() {
39+
return Optional.ofNullable(stateChange);
40+
}
41+
42+
public void setStateChange(StateChange stateChange) {
43+
this.stateChange = stateChange;
44+
}
45+
3646
public Optional<Variable> getParent() {
3747
return parent == null ? Optional.empty() : Optional.of(parent);
3848
}

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -414,9 +414,13 @@ public boolean checkStateSMT(Predicate prevState, Predicate expectedState, Sourc
414414

415415
public boolean checkStateSMT(Predicate prevState, Predicate expectedState, SourcePosition p, boolean silent)
416416
throws LJError {
417-
SMTResult result = vcChecker.verifySMTSubtypeStates(prevState, expectedState, context.getGhostStates(), p,
418-
factory, silent);
419-
return result.isOk();
417+
return verifyStateSMT(prevState, expectedState, p, silent).isOk();
418+
}
419+
420+
/** Checks that {@code prevState} implies {@code expectedState}, with a counterexample when it does not */
421+
public SMTResult verifyStateSMT(Predicate prevState, Predicate expectedState, SourcePosition p, boolean silent)
422+
throws LJError {
423+
return vcChecker.verifySMTSubtypeStates(prevState, expectedState, context.getGhostStates(), p, factory, silent);
420424
}
421425

422426
public void throwRefinementError(SourcePosition position, SourcePosition declarationPosition,

0 commit comments

Comments
 (0)