Skip to content

Commit 0afdf6a

Browse files
Leave a guard's arguments for the caller and name parameters that conditions mention
A guard is written as if (l.tryLock(...)), with the declaration tryLock(long timeout) to fill in, since the callee's parameter names are not in scope where the call is made. Argument conditions and deciding arguments show the parameters' names too, so "with lo < hi" says which argument is which. The path search ranks paths that need no argument conditions first before capping the paths to each set of states. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 9f90bd5 commit 0afdf6a

7 files changed

Lines changed: 62 additions & 36 deletions

File tree

‎liquidjava-example/src/main/java/testSuite/ErrorStateHintGuards.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ static void negated() {
1313
c.send(); // Expect: State Refinement Error
1414
}
1515

16-
// known state with no sequence of calls to the needed state: the check, with its parameter
16+
// known state with no sequence of calls to the needed state: the check, with its parameters for the caller to fill in
1717
static void withParameter() {
1818
GuardLock l = new GuardLock();
1919
l.unlock(); // Expect: State Refinement Error

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

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,10 @@ public static List<Hint> forCall(StateMachine machine, RefinedFunction call, Set
8484
/** The earlier call whose arguments decided the receiver's state, and values that would allow the call */
8585
public static Hint decidingArgument(DecidingArgument deciding, String receiver) {
8686
RefinedFunction function = deciding.call().function();
87-
String call = CtExecutableReference.CONSTRUCTOR_NAME.equals(function.getName()) ? constructor(function)
88-
: receiver + "." + signature(function);
87+
// the values name the parameters, so the call shows their names
88+
String call = CtExecutableReference.CONSTRUCTOR_NAME.equals(function.getName())
89+
? "new " + Utils.getSimpleName(function.getTargetClass()) + namedParameters(function)
90+
: receiver + "." + declaration(function);
8991
String values = deciding.values().isEmpty() ? "other arguments" : deciding.values().entrySet().stream()
9092
.map(e -> e.getKey() + " == " + e.getValue()).collect(Collectors.joining(" and "));
9193
return new Hint(Kind.DECIDING_ARGUMENT,
@@ -152,9 +154,11 @@ private List<Hint> guard(Set<State> current, RefinedFunction call) {
152154
return List.of();
153155
CallVariant guard = guards.get(0);
154156
boolean negated = guard.negatedResult();
155-
String check = (negated ? "!" : "") + invocation(guard.method());
156-
String message = String.format("Guard this call with if (%s): when it returns %s, %s is %s", check, !negated,
157-
receiver, needed());
157+
String check = (negated ? "!" : "") + guardCall(guard.method());
158+
String arguments = guard.method().getArguments().isEmpty() ? ""
159+
: ", filling in the arguments of " + declaration(guard.method());
160+
String message = String.format("Guard this call with if (%s)%s: when it returns %s, %s is %s", check, arguments,
161+
!negated, receiver, needed());
158162
return List.of(new Hint(Kind.GUARD, message));
159163
}
160164

@@ -208,20 +212,32 @@ private String call(RefinedFunction method) {
208212
return receiver + "." + signature(method);
209213
}
210214

211-
/** A call as written in code, with the parameters' names as its arguments, e.g. {@code l.tryLock(timeout)} */
212-
private String invocation(RefinedFunction method) {
213-
return receiver + "." + Utils.getSimpleName(method.getName())
214-
+ method.getArguments().stream().map(Variable::getName).collect(Collectors.joining(", ", "(", ")"));
215+
/**
216+
* A call as written in code, with {@code ...} for arguments the caller must supply, e.g. {@code l.tryLock(...)}:
217+
* the parameters' names are not in scope where the call is made
218+
*/
219+
private String guardCall(RefinedFunction method) {
220+
String arguments = method.getArguments().isEmpty() ? "()" : "(...)";
221+
return receiver + "." + Utils.getSimpleName(method.getName()) + arguments;
222+
}
223+
224+
/**
225+
* A method with its parameters' types and names, e.g. {@code tryLock(long timeout)}, for hints that mention the
226+
* parameters by name
227+
*/
228+
private static String declaration(RefinedFunction method) {
229+
return Utils.getSimpleName(method.getName()) + namedParameters(method);
215230
}
216231

217232
/**
218233
* A step of a sequence, with the condition on its arguments if it has one, e.g.
219-
* {@code p.setMode(int) with mode == 2}
234+
* {@code p.setMode(int mode) with mode == 2}
220235
*/
221236
private String call(CallVariant step) {
222-
String condition = step.kind() == CallVariant.Kind.ARGUMENTS
223-
? " with " + step.condition().getExpression().toDisplayString() : "";
224-
return call(step.method()) + condition;
237+
if (step.kind() != CallVariant.Kind.ARGUMENTS)
238+
return call(step.method());
239+
return receiver + "." + declaration(step.method()) + " with "
240+
+ step.condition().getExpression().toDisplayString();
225241
}
226242

227243
/** A method as shown in hints, e.g. {@code open(int)} */
@@ -233,6 +249,11 @@ private static String constructor(RefinedFunction constructor) {
233249
return "new " + Utils.getSimpleName(constructor.getTargetClass()) + parameters(constructor);
234250
}
235251

252+
private static String namedParameters(RefinedFunction function) {
253+
return function.getArguments().stream().map(v -> v.getType().getSimpleName() + " " + v.getName())
254+
.collect(Collectors.joining(", ", "(", ")"));
255+
}
256+
236257
private static String parameters(RefinedFunction function) {
237258
return function.getArguments().stream().map(Variable::getType).map(CtTypeReference::getSimpleName)
238259
.collect(Collectors.joining(", ", "(", ")"));

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,7 @@ private static Optional<Map<String, String>> valuesAllowing(TypeChecker tc, Stat
9999
for (Variable parameter : function.getArguments())
100100
to = leaveFree(context, to, parameter.getName(), parameter.getType(), parameter.getName(), free, types,
101101
invocation);
102-
boolean returnsValue = function.getType() != null && !"void".equals(function.getType().toString());
103-
to = returnsValue
102+
to = StateMachineBuilder.returnsValue(function)
104103
? leaveFree(context, to, Keys.WILDCARD, function.getType(), RESULT, free, types, invocation)
105104
: to.substituteVariable(Keys.WILDCARD, OBJECT);
106105

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

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -124,17 +124,16 @@ public Set<State> after(int index, State state) {
124124

125125
private Optional<Set<State>> computeAfter(int index, State state, Set<State> candidates) {
126126
return withScope(() -> {
127-
State s = state;
128127
if (query != null) {
129-
Predicate known = Predicate.createConjunction(before(s),
128+
Predicate known = Predicate.createConjunction(before(state),
130129
Predicate.createConjunction(query.substituteVariable(Keys.THIS, BEFORE), condition));
131-
return possibleStates(Set.of(s), known, BEFORE);
130+
return possibleStates(Set.of(state), known, BEFORE);
132131
}
133132
ObjectState change = changes.get(index);
134133
if (!change.hasTo())
135-
return Set.of(s);
134+
return Set.of(state);
136135
Predicate to = change.getTo().substituteVariable(Keys.THIS, AFTER).changeOldMentions(BEFORE, AFTER);
137-
Predicate known = Predicate.createConjunction(before(s), to);
136+
Predicate known = Predicate.createConjunction(before(state), to);
138137
if (onResult)
139138
known = Predicate.createConjunction(known, condition);
140139
Set<State> next = possibleStates(candidates, known, AFTER);

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

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

33
import java.util.ArrayList;
44
import java.util.Collection;
5+
import java.util.Comparator;
56
import java.util.Collections;
67
import java.util.HashSet;
78
import java.util.LinkedHashMap;
@@ -197,15 +198,16 @@ public Optional<List<List<CallVariant>>> shortestPaths(Set<State> current, Refin
197198
if (visited.contains(reached))
198199
continue;
199200
List<List<CallVariant>> reachedPaths = next.computeIfAbsent(reached, k -> new ArrayList<>());
200-
paths.stream().limit(limit - reachedPaths.size()).map(path -> append(path, step))
201-
.forEach(reachedPaths::add);
201+
paths.stream().map(path -> append(path, step)).forEach(reachedPaths::add);
202202
}
203203
});
204+
// capped only once every path to a set is known, so that paths needing no arguments are the ones kept
205+
next.replaceAll((states, paths) -> paths.stream().sorted(Comparator.comparingLong(StateMachine::conditions))
206+
.limit(limit).toList());
204207
List<List<CallVariant>> found = next.entrySet().stream().filter(e -> allows(e.getKey(), call))
205208
.flatMap(e -> e.getValue().stream()).toList();
206209
if (!found.isEmpty()) {
207-
List<List<CallVariant>> unconditioned = found.stream()
208-
.filter(p -> p.stream().noneMatch(v -> v.kind() == CallVariant.Kind.ARGUMENTS)).toList();
210+
List<List<CallVariant>> unconditioned = found.stream().filter(p -> conditions(p) == 0).toList();
209211
return Optional.of((unconditioned.isEmpty() ? found : unconditioned).stream().limit(limit).toList());
210212
}
211213
visited.addAll(next.keySet());
@@ -260,6 +262,11 @@ private Optional<Transition> transition(State from, RefinedFunction method) {
260262
return transitions.stream().filter(t -> t.from().equals(from) && t.method().equals(method)).findFirst();
261263
}
262264

265+
/** The steps of {@code path} that need arguments meeting a condition */
266+
private static long conditions(List<CallVariant> path) {
267+
return path.stream().filter(v -> v.kind() == CallVariant.Kind.ARGUMENTS).count();
268+
}
269+
263270
private static List<CallVariant> append(List<CallVariant> path, CallVariant method) {
264271
List<CallVariant> extended = new ArrayList<>(path);
265272
extended.add(method);

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,7 @@ private static boolean returnsBoolean(RefinedFunction function) {
400400
return function.getType() != null && Types.BOOLEAN.equals(function.getType().toString());
401401
}
402402

403-
private static boolean returnsValue(RefinedFunction function) {
403+
static boolean returnsValue(RefinedFunction function) {
404404
return function.getType() != null && !"void".equals(function.getType().toString());
405405
}
406406

‎liquidjava-verifier/src/test/java/liquidjava/api/tests/TestStateHints.java‎

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ void finalStateSuggestsCreatingANewObject() {
5555
@Test
5656
void transitionDependingOnArgumentsIsSuggestedWithItsConditionButPreventsClaimingStateIsFinal() {
5757
assertHints("ErrorStateHintIncomplete.java", new Hint(Kind.CALL_SEQUENCE,
58-
"Call f.reopen(int) with attempts > 0 before this call to move f from closed to open"));
58+
"Call f.reopen(int attempts) with attempts > 0 before this call to move f from closed to open"));
5959
}
6060

6161
@Test
@@ -244,13 +244,13 @@ void onlyPublicInheritedMethodsAreSuggested() {
244244
@Test
245245
void argumentThatDecidedTheStateIsShownWithAValueThatWouldWork() {
246246
assertHints("ErrorArgumentPicksTheState.java", new Hint(Kind.DECIDING_ARGUMENT,
247-
"buffer.reset(boolean) on line 20 decides the state of buffer: with keep == true, this call would be allowed"));
247+
"buffer.reset(boolean keep) on line 20 decides the state of buffer: with keep == true, this call would be allowed"));
248248
}
249249

250250
@Test
251251
void deciderIsTheLastCallThatChangedTheCheckedStateSet() {
252252
assertHints("ErrorArgumentPicksTheStateEarlier.java", new Hint(Kind.DECIDING_ARGUMENT,
253-
"param.setTilingMode(int) on line 25 decides the state of param: with mode == 2, this call would be allowed"));
253+
"param.setTilingMode(int mode) on line 25 decides the state of param: with mode == 2, this call would be allowed"));
254254
}
255255

256256
@Test
@@ -262,7 +262,8 @@ void checksAreSuggestedOnlyWhenTheRefinementsProveThem() {
262262
+ "Objects created with new GuardChannel() start in such a state"),
263263
new Hint(Kind.GUARD, "Guard this call with if (!c.isClosed()): when it returns false, c is open"));
264264
assertHints(errors.get(1),
265-
new Hint(Kind.GUARD, "Guard this call with if (l.tryLock(timeout)): when it returns true, l is locked"),
265+
new Hint(Kind.GUARD, "Guard this call with if (l.tryLock(...)), "
266+
+ "filling in the arguments of tryLock(long timeout): when it returns true, l is locked"),
266267
new Hint(Kind.AVAILABLE_CALLS, "In state unlocked, l can call: tryLock(long) (and possibly others)"));
267268
assertHints(errors.get(2),
268269
new Hint(Kind.NEEDED_STATE, "The state of p is unknown here, and this call needs it to be ready"));
@@ -291,14 +292,13 @@ void argumentConditionsAreSuggestedOnlyWhenEveryArgumentMeetingThemWorks() {
291292
List<StateRefinementError> errors = errors("ErrorStateHintArgumentConditions.java", 7);
292293
assertHints(errors.get(0),
293294
new Hint(Kind.CALL_SEQUENCE,
294-
"Call r.set(int, int) with lo < hi before this call to move r from low to high"),
295+
"Call r.set(int lo, int hi) with lo < hi before this call to move r from low to high"),
295296
new Hint(Kind.AVAILABLE_CALLS, "In state low, r can call: set(int, int) (and possibly others)"));
296297
assertHints(errors.get(1), new Hint(Kind.CALL_SEQUENCE,
297-
"Call chain.pick(int) with k == 1, then chain.next() before this call to move chain from a to c"),
298+
"Call chain.pick(int k) with k == 1, then chain.next() before this call to move chain from a to c"),
298299
new Hint(Kind.AVAILABLE_CALLS, "In state a, chain can call: pick(int) (and possibly others)"));
299-
assertHints(errors.get(2),
300-
new Hint(Kind.CALL_SEQUENCE,
301-
"Call l.setLimit(int) with limit <= 0 before this call to move l from bounded to unbounded"),
300+
assertHints(errors.get(2), new Hint(Kind.CALL_SEQUENCE,
301+
"Call l.setLimit(int limit) with limit <= 0 before this call to move l from bounded to unbounded"),
302302
new Hint(Kind.AVAILABLE_CALLS, "In state bounded, l can call: setLimit(int) (and possibly others)"));
303303
// no condition, and no value of the earlier call's argument, is claimed to allow the call
304304
for (StateRefinementError error : errors.subList(3, 5))
@@ -313,7 +313,7 @@ void argumentConditionsAreSuggestedOnlyWhenEveryArgumentMeetingThemWorks() {
313313
@Test
314314
void callWhoseStateDependsOnItsArgumentIsSuggestedWithTheCondition() {
315315
assertHints("classes/state_hint_compression_mode_error", new Hint(Kind.CALL_SEQUENCE,
316-
"Call wParam.setCompressionMode(int) with mode == 2 before this call to move wParam from startCompression to compressionExplicit"),
316+
"Call wParam.setCompressionMode(int mode) with mode == 2 before this call to move wParam from startCompression to compressionExplicit"),
317317
new Hint(Kind.AVAILABLE_CALLS,
318318
"In state startCompression, wParam can call: setCompressionMode(int) (and possibly others)"));
319319
}

0 commit comments

Comments
 (0)