Skip to content

Commit d3e00be

Browse files
Point out the argument that decided a state a call does not allow (#419)
## Description Adds the **Argument picks the state** category (codebook C3.1) and a hint for it. These errors had no useful hint before: a transition that depends on an argument isn't part of the state machine, so the existing hints can't explain it. - Each object instance now records the last call that changed its state (`StateChange`). Method calls and constructors record their own call, and state-preserving calls keep the previous record. A join of branches records nothing, which ends the search below. - `DecidingArgument` 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, the SMT solver looks for values that would have allowed the failing call: the state before the call, the call's `to` with its parameters left free, and the failing call's `from`. The model gives the values for the hint. - When such values exist, the category comes before the other state categories. The error then gets only the deciding hint. If none exist, the error keeps its other category (for example `ErrorArgumentCannotPickTheState`). ## Example ``` State Refinement Error: found empty(buffer) but expected filled(buffer) Hints: - buffer.reset(boolean) on line 20 decides the state of buffer: with keep == true, this call would be allowed ``` Category: Argument picks the state (C3.1). ## Measured against the codebook Same runs as #418: - **Corpus:** 94 of 99 right (91 before), 1 wrong, 4 with no category. - **Exercises:** 42 of 52 right (34 before), 6 wrong, 4 with no category. The category now finds 12 of the 15 C3.1 cases. The ones it misses: - a guard in `from` that changes no state; - an earlier value that is the real cause; - a returned value at a join. ## Type of change - [x] New feature ## Checklist - [x] Added tests: - `ErrorArgumentPicksTheState`, `ErrorConstructorArgumentPicksTheState` and `ErrorArgumentPicksTheStateEarlier` (the decider is found past a call on another state set); - `ErrorArgumentCannotPickTheState` (no value helps, so no category change); - cases in `TestErrorCategories` and `TestStateHints`. - [x] `mvn test` passes locally 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 628a9ef commit d3e00be

16 files changed

Lines changed: 346 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
@@ -14,10 +14,12 @@
1414
import liquidjava.diagnostics.hints.Hint.Kind;
1515
import liquidjava.processor.context.RefinedFunction;
1616
import liquidjava.processor.context.Variable;
17+
import liquidjava.processor.state_machine.DecidingArgument;
1718
import liquidjava.processor.state_machine.State;
1819
import liquidjava.processor.state_machine.StateMachine;
1920
import liquidjava.processor.state_machine.Transition;
2021
import liquidjava.utils.Utils;
22+
import spoon.reflect.reference.CtExecutableReference;
2123
import spoon.reflect.reference.CtTypeReference;
2224

2325
/**
@@ -65,6 +67,18 @@ public static List<Hint> forCall(StateMachine machine, RefinedFunction call, Set
6567
hints.allowedCalls(current).stream()).toList();
6668
}
6769

70+
/** The earlier call whose arguments decided the receiver's state, and values that would allow the call */
71+
public static Hint decidingArgument(DecidingArgument deciding, String receiver) {
72+
RefinedFunction function = deciding.call().function();
73+
String call = CtExecutableReference.CONSTRUCTOR_NAME.equals(function.getName()) ? constructor(function)
74+
: receiver + "." + signature(function);
75+
String values = deciding.values().isEmpty() ? "other arguments" : deciding.values().entrySet().stream()
76+
.map(e -> e.getKey() + " == " + e.getValue()).collect(Collectors.joining(" and "));
77+
return new Hint(Kind.DECIDING_ARGUMENT,
78+
String.format("%s on line %d decides the state of %s: with %s, this call would be allowed", call,
79+
deciding.call().position().getLine(), receiver, values));
80+
}
81+
6882
/** Nothing is known about the receiver's state: list what leads to a state where {@code call} is allowed */
6983
private List<Hint> reachingCalls(RefinedFunction call, Set<State> goal) {
7084
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)