Skip to content

Commit 0dfbcd5

Browse files
Ask the solver only about the state sets a refinement names
The verifier keeps the state of every set a refinement's to does not name, and the states of different sets are different ghost functions. So: - whether a from holds is asked once per state of the sets it names; - the states a call can lead to are only those that keep the other sets, and are asked once per state of the sets its to names for old(this); - a guard whose refinements name no state of the sets the failing call checks is not tried when the receiver can be in any combination of states. On the study's UndoManager this takes 1419 queries to 258, with the same hints. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 9f90bd5 commit 0dfbcd5

5 files changed

Lines changed: 219 additions & 19 deletions

File tree

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import liquidjava.processor.state_machine.DecidingArgument;
1919
import liquidjava.processor.state_machine.State;
2020
import liquidjava.processor.state_machine.StateMachine;
21+
import liquidjava.rj_language.Predicate;
2122
import liquidjava.utils.Utils;
2223
import liquidjava.utils.constants.Keys;
2324
import spoon.reflect.reference.CtExecutableReference;
@@ -141,7 +142,10 @@ private List<Hint> howToReach(Set<State> current, RefinedFunction call, String f
141142
*/
142143
private List<Hint> guard(Set<State> current, RefinedFunction call) {
143144
List<CallVariant> guards = new ArrayList<>();
145+
boolean anyCombination = anyCombination(current);
144146
for (CallVariant guard : machine.guards()) {
147+
if (anyCombination && !namesCheckedState(guard.method()))
148+
continue;
145149
if (!machine.allows(current, guard))
146150
continue;
147151
Set<State> reached = machine.after(current, guard);
@@ -158,6 +162,33 @@ private List<Hint> guard(Set<State> current, RefinedFunction call) {
158162
return List.of(new Hint(Kind.GUARD, message));
159163
}
160164

165+
/**
166+
* Whether the receiver can be in any combination of the states {@code current} has in each state set. Then a call
167+
* that names no state of the sets the failing call checks leaves them as they were, whatever it returns, so the
168+
* failing call stays disallowed after it
169+
*/
170+
private boolean anyCombination(Set<State> current) {
171+
long combinations = 1;
172+
for (int set = 0; set < current.iterator().next().names().size(); set++) {
173+
int index = set;
174+
combinations *= current.stream().map(s -> s.names().get(index)).distinct().count();
175+
}
176+
return combinations == current.size();
177+
}
178+
179+
/**
180+
* Whether a state refinement or the return refinement of {@code method} names a state of a set the failing call
181+
* checks, or an alias
182+
*/
183+
private boolean namesCheckedState(RefinedFunction method) {
184+
Set<String> checked = machine.states().stream().flatMap(s -> checkedSets.stream().map(s.names()::get))
185+
.collect(Collectors.toSet());
186+
Stream<Predicate> refinements = Stream.concat(
187+
method.getAllStates().stream().flatMap(o -> Stream.of(o.getFrom(), o.getTo())),
188+
Stream.ofNullable(method.getRefReturn()));
189+
return refinements.anyMatch(p -> StateMachine.mayName(p, checked));
190+
}
191+
161192
private Hint callSequence(Set<State> current, List<CallVariant> sequence, String from) {
162193
Set<State> reached = current;
163194
for (CallVariant step : sequence)

‎liquidjava-verifier/src/main/java/liquidjava/processor/state_machine/SolverEffects.java‎

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import java.util.ArrayList;
44
import java.util.HashMap;
5+
import java.util.HashSet;
6+
import java.util.LinkedHashSet;
57
import java.util.List;
68
import java.util.Map;
79
import java.util.Optional;
@@ -56,8 +58,14 @@ record Scope(TypeChecker tc, StateSolver solver, List<State> states, List<List<S
5658
/** The effects with any arguments, whose entries a condition on the result does not change, or null */
5759
private final CallVariant.Effects any;
5860

59-
private final Map<Integer, Map<State, CallVariant.Entry>> entries = new HashMap<>();
61+
/** By state refinement, then by the states before the call in the state sets its {@code from} names */
62+
private final Map<Integer, Map<List<String>, CallVariant.Entry>> entries = new HashMap<>();
6063
private final Map<Integer, Map<State, Optional<Set<State>>>> afters = new HashMap<>();
64+
/**
65+
* By state refinement, then by the states before the call in the sets its {@code to} names for {@code old(this)}:
66+
* the states it can lead to in the sets it names
67+
*/
68+
private final Map<Integer, Map<List<String>, Set<List<String>>>> reachedBefore = new HashMap<>();
6169

6270
/**
6371
* @param changes
@@ -99,13 +107,17 @@ public CallVariant.Entry entry(int index, State state) {
99107
return CallVariant.Entry.HOLDS; // no state refinement: allowed in every state
100108
if (onResult && any != null)
101109
return any.entry(index, state); // the result is not known before the call
102-
return entries.computeIfAbsent(index, i -> new HashMap<>()).computeIfAbsent(state, s -> {
110+
// the states of the other sets are about other ghost functions, so they cannot change the answer
111+
Predicate from = changes.get(index).getFrom();
112+
List<String> named = namedSets(from).stream().map(state.names()::get).toList();
113+
return entries.computeIfAbsent(index, i -> new HashMap<>()).computeIfAbsent(named, n -> {
103114
Optional<CallVariant.Entry> entry = withScope(() -> {
104-
Predicate before = before(s);
105-
Predicate from = changes.get(index).getFrom().substituteVariable(Keys.THIS, BEFORE);
106-
if (scope.solver().implies(before, from))
115+
Predicate before = before(state);
116+
Predicate fromBefore = from.substituteVariable(Keys.THIS, BEFORE);
117+
if (scope.solver().implies(before, fromBefore))
107118
return CallVariant.Entry.HOLDS;
108-
return scope.solver().compatible(before, from) ? CallVariant.Entry.UNDECIDED : CallVariant.Entry.FAILS;
119+
return scope.solver().compatible(before, fromBefore) ? CallVariant.Entry.UNDECIDED
120+
: CallVariant.Entry.FAILS;
109121
});
110122
return entry.orElse(CallVariant.Entry.UNDECIDED);
111123
});
@@ -133,11 +145,23 @@ private Optional<Set<State>> computeAfter(int index, State state, Set<State> can
133145
ObjectState change = changes.get(index);
134146
if (!change.hasTo())
135147
return Set.of(s);
136-
Predicate to = change.getTo().substituteVariable(Keys.THIS, AFTER).changeOldMentions(BEFORE, AFTER);
137-
Predicate known = Predicate.createConjunction(before(s), to);
138-
if (onResult)
139-
known = Predicate.createConjunction(known, condition);
140-
Set<State> next = possibleStates(candidates, known, AFTER);
148+
List<Integer> named = namedSets(change.getTo()).stream().sorted().toList();
149+
Set<State> kept = Set.copyOf(StateMachine.keeping(candidates, s, Set.copyOf(named)));
150+
// where it leads in the sets it names depends only on the state before in those it names for old(this)
151+
List<String> before = StateMachine.namedSetsBefore(change.getTo(), scope.stateSets()).stream().sorted()
152+
.map(s.names()::get).toList();
153+
Map<List<String>, Set<List<String>>> reached = reachedBefore.computeIfAbsent(index, i -> new HashMap<>());
154+
if (!reached.containsKey(before)) {
155+
Predicate to = change.getTo().substituteVariable(Keys.THIS, AFTER).changeOldMentions(BEFORE, AFTER);
156+
Predicate known = Predicate.createConjunction(before(s), to);
157+
if (onResult)
158+
known = Predicate.createConjunction(known, condition);
159+
reached.put(before, possibleStates(kept, known, AFTER).stream()
160+
.map(c -> named.stream().map(c.names()::get).toList()).collect(Collectors.toSet()));
161+
}
162+
Set<State> next = scope.states().stream().filter(kept::contains)
163+
.filter(c -> reached.get(before).contains(named.stream().map(c.names()::get).toList()))
164+
.collect(Collectors.toCollection(LinkedHashSet::new));
141165
// no state at all: the condition contradicts the refinement here, so nothing can be claimed
142166
return next.isEmpty() && !onResult ? null : next;
143167
});
@@ -171,6 +195,13 @@ private Set<State> possibleStates(Set<State> candidates, Predicate known, String
171195
return scope.solver().possibleStates(combined, known, variable);
172196
}
173197

198+
/** The state sets that {@code predicate} or this variant's condition may name a state of */
199+
private Set<Integer> namedSets(Predicate predicate) {
200+
Set<Integer> named = new HashSet<>(StateMachine.namedSets(predicate, scope.stateSets()));
201+
named.addAll(StateMachine.namedSets(condition, scope.stateSets()));
202+
return named;
203+
}
204+
174205
/** What is known before the call in {@code state} */
175206
private Predicate before(State state) {
176207
Predicate before = state.predicate().substituteVariable(Keys.THIS, BEFORE);

‎liquidjava-verifier/src/main/java/liquidjava/processor/state_machine/StateMachine.java‎

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,16 @@
1111
import java.util.Optional;
1212
import java.util.Set;
1313
import java.util.stream.Collectors;
14+
import java.util.stream.IntStream;
1415

1516
import liquidjava.processor.context.ObjectState;
1617
import liquidjava.processor.context.RefinedFunction;
1718
import liquidjava.rj_language.Predicate;
19+
import liquidjava.rj_language.ast.AliasInvocation;
20+
import liquidjava.rj_language.ast.Expression;
21+
import liquidjava.rj_language.ast.FunctionInvocation;
22+
import liquidjava.utils.Utils;
23+
import liquidjava.utils.constants.Keys;
1824

1925
/**
2026
* The typestate of a class as a finite state machine, built by {@link StateMachineBuilder}
@@ -256,6 +262,60 @@ public Set<State> ordered(Collection<State> subset) {
256262
return states.stream().filter(subset::contains).collect(Collectors.toCollection(LinkedHashSet::new));
257263
}
258264

265+
/**
266+
* Whether {@code predicate} names one of {@code states}, by its simple name, or calls an alias, which may name one
267+
* that cannot be seen without expanding it
268+
*/
269+
public static boolean mayName(Predicate predicate, Set<String> states) {
270+
return mayName(predicate.getExpression(), states, false);
271+
}
272+
273+
/**
274+
* The state sets, as indexes into {@link State#names()}, that {@code predicate} {@link #mayName may name} a state
275+
* of
276+
*
277+
* @param stateSets
278+
* the states of each state set on its own, in the order the sets are declared
279+
*/
280+
static Set<Integer> namedSets(Predicate predicate, List<List<State>> stateSets) {
281+
return namedSets(predicate, stateSets, false);
282+
}
283+
284+
/** Like {@link #namedSets}, but only the sets it may name a state of for {@code old(this)} */
285+
static Set<Integer> namedSetsBefore(Predicate predicate, List<List<State>> stateSets) {
286+
return namedSets(predicate, stateSets, true);
287+
}
288+
289+
private static Set<Integer> namedSets(Predicate predicate, List<List<State>> stateSets, boolean onOld) {
290+
return IntStream.range(0, stateSets.size())
291+
.filter(k -> mayName(predicate.getExpression(),
292+
stateSets.get(k).stream().map(s -> s.names().get(0)).collect(Collectors.toSet()), onOld))
293+
.boxed().collect(Collectors.toSet());
294+
}
295+
296+
/**
297+
* The {@code candidates} in the same state as {@code before} in every state set but those in {@code changed}: the
298+
* verifier keeps the state of a set that a state refinement's {@code to} does not name
299+
*/
300+
static List<State> keeping(Collection<State> candidates, State before, Set<Integer> changed) {
301+
return candidates.stream().filter(c -> IntStream.range(0, before.names().size())
302+
.allMatch(k -> changed.contains(k) || c.names().get(k).equals(before.names().get(k)))).toList();
303+
}
304+
305+
private static boolean mayName(Expression expression, Set<String> states, boolean onOld) {
306+
if (expression instanceof AliasInvocation)
307+
return true;
308+
if (expression instanceof FunctionInvocation f && states.contains(Utils.getSimpleName(f.getName()))
309+
&& (!onOld || invokesOld(f)))
310+
return true;
311+
return expression.getChildren().stream().anyMatch(e -> mayName(e, states, onOld));
312+
}
313+
314+
private static boolean invokesOld(Expression expression) {
315+
return expression instanceof FunctionInvocation f && Keys.OLD.equals(f.getName())
316+
|| expression.getChildren().stream().anyMatch(StateMachine::invokesOld);
317+
}
318+
259319
private Optional<Transition> transition(State from, RefinedFunction method) {
260320
return transitions.stream().filter(t -> t.from().equals(from) && t.method().equals(method)).findFirst();
261321
}

‎liquidjava-verifier/src/main/java/liquidjava/processor/state_machine/StateMachineBuilder.java‎

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
import java.util.ArrayDeque;
44
import java.util.ArrayList;
55
import java.util.Deque;
6+
import java.util.HashMap;
67
import java.util.HashSet;
78
import java.util.LinkedHashMap;
9+
import java.util.LinkedHashSet;
810
import java.util.List;
911
import java.util.Map;
1012
import java.util.Objects;
@@ -509,32 +511,68 @@ private Optional<List<Transition>> transitionsOf(RefinedFunction method) throws
509511
if (!canModel(method))
510512
return Optional.empty();
511513
List<Transition> transitions = new ArrayList<>();
514+
List<List<State>> sets = stateSets();
515+
List<ObjectState> changes = method.getAllStates();
516+
// whether a from holds depends only on the state sets it names: the others are other ghost functions
517+
List<Map<List<String>, CallVariant.Entry>> entries = changes.stream()
518+
.map(c -> new HashMap<List<String>, CallVariant.Entry>()).collect(Collectors.toList());
519+
List<Map<List<String>, Set<List<String>>>> afters = changes.stream()
520+
.map(c -> new HashMap<List<String>, Set<List<String>>>()).collect(Collectors.toList());
512521
for (State state : states) {
513522
Predicate current = state.predicate().substituteVariable(Keys.THIS, BEFORE);
514-
for (ObjectState change : method.getAllStates()) {
523+
for (int i = 0; i < changes.size(); i++) {
524+
ObjectState change = changes.get(i);
515525
if (!change.hasFrom())
516526
continue;
517-
Predicate from = change.getFrom().substituteVariable(Keys.THIS, BEFORE);
518-
if (solver.implies(current, from)) {
527+
List<String> named = StateMachine.namedSets(change.getFrom(), sets).stream().map(state.names()::get)
528+
.toList();
529+
CallVariant.Entry entry = entries.get(i).get(named);
530+
if (entry == null) {
531+
entry = entry(current, change.getFrom().substituteVariable(Keys.THIS, BEFORE));
532+
entries.get(i).put(named, entry);
533+
}
534+
if (entry == CallVariant.Entry.HOLDS) {
519535
// like the verifier, the first state change whose precondition holds is the one applied
520-
Set<State> next = statesAfter(change, state, current);
536+
Set<State> next = statesAfter(change, state, current, sets, afters.get(i));
521537
if (next.isEmpty())
522538
return Optional.empty();
523539
transitions.add(new Transition(state, method, change, next));
524540
break;
525541
}
526-
if (solver.compatible(current, from))
542+
if (entry == CallVariant.Entry.UNDECIDED)
527543
return Optional.empty(); // allowed only sometimes in this state
528544
}
529545
}
530546
return Optional.of(transitions);
531547
}
532548

533-
private Set<State> statesAfter(ObjectState change, State state, Predicate current) throws LJError {
549+
private CallVariant.Entry entry(Predicate current, Predicate from) throws LJError {
550+
if (solver.implies(current, from))
551+
return CallVariant.Entry.HOLDS;
552+
return solver.compatible(current, from) ? CallVariant.Entry.UNDECIDED : CallVariant.Entry.FAILS;
553+
}
554+
555+
/**
556+
* The states {@code change} can lead to from {@code state}. The verifier keeps the state of the sets its {@code to}
557+
* does not name, and the states it can lead to in the others depend only on the state before in the sets it names
558+
* for {@code old(this)}, so they are asked once for each and kept in {@code known}
559+
*/
560+
private Set<State> statesAfter(ObjectState change, State state, Predicate current, List<List<State>> sets,
561+
Map<List<String>, Set<List<String>>> known) throws LJError {
534562
if (!change.hasTo())
535563
return Set.of(state);
536-
Predicate to = change.getTo().substituteVariable(Keys.THIS, AFTER).changeOldMentions(BEFORE, AFTER);
537-
return solver.possibleStates(states, Predicate.createConjunction(current, to), AFTER);
564+
List<Integer> named = StateMachine.namedSets(change.getTo(), sets).stream().sorted().toList();
565+
List<State> candidates = StateMachine.keeping(states, state, Set.copyOf(named));
566+
List<String> before = StateMachine.namedSetsBefore(change.getTo(), sets).stream().sorted()
567+
.map(state.names()::get).toList();
568+
if (!known.containsKey(before)) {
569+
Predicate to = change.getTo().substituteVariable(Keys.THIS, AFTER).changeOldMentions(BEFORE, AFTER);
570+
known.put(before, solver.possibleStates(candidates, Predicate.createConjunction(current, to), AFTER)
571+
.stream().map(s -> named.stream().map(s.names()::get).toList()).collect(Collectors.toSet()));
572+
}
573+
Set<List<String>> after = known.get(before);
574+
return candidates.stream().filter(c -> after.contains(named.stream().map(c.names()::get).toList()))
575+
.collect(Collectors.toCollection(LinkedHashSet::new));
538576
}
539577

540578
/** The states {@code constructor} can create the object in, or nothing if they depend on its arguments */

‎liquidjava-verifier/src/test/java/liquidjava/processor/state_machine/StateMachineTest.java‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515

1616
import liquidjava.processor.context.RefinedFunction;
1717
import liquidjava.rj_language.Predicate;
18+
import liquidjava.rj_language.ast.AliasInvocation;
19+
import liquidjava.utils.constants.Keys;
1820

1921
class StateMachineTest {
2022

@@ -23,6 +25,9 @@ class StateMachineTest {
2325
private static final State OPEN_UNLOCKED = state("open", "unlocked");
2426
private static final State CLOSED_LOCKED = state("closed", "locked");
2527
private static final State CLOSED_UNLOCKED = state("closed", "unlocked");
28+
private static final List<List<State>> SETS = List.of(List.of(state("open"), state("closed")),
29+
List.of(state("locked"), state("unlocked")));
30+
private static final Predicate THIS = Predicate.createVar(Keys.THIS);
2631
private static final StateMachine MACHINE = machine(
2732
List.of(OPEN_LOCKED, OPEN_UNLOCKED, CLOSED_LOCKED, CLOSED_UNLOCKED));
2833

@@ -139,6 +144,41 @@ void callWhoseStatesAfterAreUnknownIsNotAllowed() {
139144
assertFalse(machine(ABC, call).allows(Set.of(A), call));
140145
}
141146

147+
@Test
148+
void setsNamedAreThoseWithAStateInThePredicate() {
149+
Predicate openAndPositive = Predicate.createConjunction(inState("open", THIS),
150+
Predicate.createInvocation("size", THIS));
151+
assertEquals(Set.of(0), StateMachine.namedSets(openAndPositive, SETS));
152+
assertEquals(Set.of(), StateMachine.namedSets(new Predicate(), SETS));
153+
}
154+
155+
@Test
156+
void aliasMayNameEverySet() {
157+
Predicate alias = new Predicate(new AliasInvocation("Ready", List.of(THIS.getExpression())));
158+
assertEquals(Set.of(0, 1), StateMachine.namedSets(alias, SETS));
159+
assertEquals(Set.of(0, 1), StateMachine.namedSetsBefore(alias, SETS));
160+
}
161+
162+
@Test
163+
void setsNamedBeforeAreThoseNamedForOld() {
164+
// locked(this) && open(old(this))
165+
Predicate to = Predicate.createConjunction(inState("locked", THIS),
166+
inState("open", Predicate.createInvocation(Keys.OLD, THIS)));
167+
assertEquals(Set.of(0, 1), StateMachine.namedSets(to, SETS));
168+
assertEquals(Set.of(0), StateMachine.namedSetsBefore(to, SETS));
169+
}
170+
171+
@Test
172+
void candidatesKeepTheStateOfSetsNotChanged() {
173+
assertEquals(List.of(OPEN_LOCKED, OPEN_UNLOCKED),
174+
StateMachine.keeping(MACHINE.states(), OPEN_LOCKED, Set.of(1)));
175+
assertEquals(List.of(OPEN_LOCKED), StateMachine.keeping(MACHINE.states(), OPEN_LOCKED, Set.of()));
176+
}
177+
178+
private static Predicate inState(String state, Predicate object) {
179+
return Predicate.createInvocation(state, object);
180+
}
181+
142182
/** Effects given by hand: the states where each refinement's from holds, and the states after the call from each */
143183
private record Effects(boolean changesState, Map<State, Set<State>> after, Set<State> undecided) {
144184
Effects(boolean changesState, Map<State, Set<State>> after) {

0 commit comments

Comments
 (0)