Skip to content

Commit 920bf15

Browse files
From note: name an unrefined parameter as the unknown value's origin (#526)
Closes #522, part 1 of 5: **From note F3**, unrefined parameter. Design: #522 (comment) A refinement error whose Expected says nothing the verifier knows now gets a message note naming the cause, when the value is an unrefined parameter of the enclosing method. The CLI prints the note under the headline. `getMessageNotes()` carries it to the extension, which is meant to show it in the hover (vscode-liquidjava#188). ## When the note is printed All of these must hold, or there is no note: - the category is `UNKNOWN`; - every variable of Expected is a parameter of the enclosing method or constructor, by name. A reassigned parameter has an instance, which Expected would name instead; - each such parameter's context `Variable` has the declared refinement `true`. The note restates the declaration. It says nothing about callers and adds no fact to Found. ## A2_04, before and after (only study task that changes) ```diff Refinement Error: expected timeout >= 0 but could not prove it + From: timeout is a parameter of checkCimReachable(String, int) (line 94) with no refinement 103 | 104 | try (Socket socket = new Socket()) { 105 | socket.connect(new InetSocketAddress(host, port), timeout); ``` A2_10, A1_38, A1_01, A1_56, A1_09, practice and practice2 are byte-identical to main. Each task still gives exactly one error, on its `reportedLine`. ## Changes - `LJDiagnostic.addMessageNote`: `getMessageNotes()` returns these notes, and `RefinementError` puts them after its alias `where` lines. This is the hook the later From PRs use. - `FromNotes.unrefinedParameters`, called where `VCChecker` builds the refinement error. - Test: `TestFromNotes` on the existing fixture `ErrorUnknownArgument.java`. It fails without the change. ## Conflicts - May conflict with #520/#521 (another agent: headline and general hint in `RefinementError`). The overlap is only the `getMessageNotes` override, and it is a trivial rebase. - The later parts of #522 (F1/F2, F4) stack on this branch. Tests: `mvn test`, 729 run, 0 failures. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 98fa854 commit 920bf15

5 files changed

Lines changed: 136 additions & 4 deletions

File tree

‎liquidjava-verifier/src/main/java/liquidjava/diagnostics/LJDiagnostic.java‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public class LJDiagnostic extends RuntimeException {
2222
private String file;
2323
private SourcePosition position;
2424
private final List<Hint> hints = new ArrayList<>();
25+
private final List<String> messageNotes = new ArrayList<>();
2526
private String counterexample;
2627
private ErrorCategory category;
2728
private static final String PIPE = " | ";
@@ -78,7 +79,12 @@ public List<SourcePosition> getDeclarationPositions() {
7879

7980
/** Lines printed under the message that explain it, such as what the names it uses mean */
8081
public List<String> getMessageNotes() {
81-
return List.of();
82+
return Collections.unmodifiableList(messageNotes);
83+
}
84+
85+
/** Adds a line printed under the message, such as where the value it is about comes from */
86+
public void addMessageNote(String note) {
87+
messageNotes.add(note);
8288
}
8389

8490
/** Lines printed under the excerpt of where the refinement is declared that explain it */

‎liquidjava-verifier/src/main/java/liquidjava/diagnostics/errors/RefinementError.java‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,10 +116,13 @@ public List<AliasExpansion> getExpectedAliases() {
116116
return expectedAliases;
117117
}
118118

119-
/** A where line declaring each alias the message uses */
119+
/** A where line declaring each alias the message uses, then the other notes */
120120
@Override
121121
public List<String> getMessageNotes() {
122-
return aliasDeclarations.stream().map(declaration -> "where " + declaration).toList();
122+
List<String> notes = new ArrayList<>(
123+
aliasDeclarations.stream().map(declaration -> "where " + declaration).toList());
124+
notes.addAll(super.getMessageNotes());
125+
return notes;
123126
}
124127

125128
// Keeps the counterexample assignments to the terms of expected and of the found facts linked to them, except
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package liquidjava.processor.refinement_checker;
2+
3+
import java.util.ArrayList;
4+
import java.util.List;
5+
import java.util.Optional;
6+
import java.util.stream.Collectors;
7+
8+
import liquidjava.diagnostics.categories.ErrorCategory;
9+
import liquidjava.diagnostics.errors.RefinementError;
10+
import liquidjava.processor.context.Context;
11+
import liquidjava.processor.context.Variable;
12+
import spoon.reflect.declaration.CtConstructor;
13+
import spoon.reflect.declaration.CtElement;
14+
import spoon.reflect.declaration.CtExecutable;
15+
import spoon.reflect.declaration.CtMethod;
16+
import spoon.reflect.declaration.CtParameter;
17+
import spoon.reflect.declaration.CtType;
18+
import liquidjava.utils.Utils;
19+
20+
/**
21+
* The notes of a refinement error that say where the values its expected refinement is about come from. Each is given
22+
* only when it restates what the verifier used, and is left out otherwise
23+
*/
24+
final class FromNotes {
25+
26+
private FromNotes() {
27+
}
28+
29+
/**
30+
* When nothing is known about the values expected constrains and each is a parameter of the enclosing method that
31+
* is declared with no refinement, a note per parameter saying so. Nothing is said about the callers
32+
*/
33+
static List<String> unrefinedParameters(RefinementError error, CtElement checked, Context context) {
34+
Optional<CtExecutable<?>> method = enclosingMethod(checked);
35+
List<String> variables = error.getExpected().getVariableNames().stream().distinct().toList();
36+
if (error.getCategory().filter(ErrorCategory.UNKNOWN::equals).isEmpty() || method.isEmpty()
37+
|| variables.isEmpty())
38+
return List.of();
39+
40+
List<String> notes = new ArrayList<>();
41+
for (String variable : variables) {
42+
Optional<CtParameter<?>> parameter = method.get().getParameters().stream()
43+
.filter(p -> p.getSimpleName().equals(variable)).findFirst();
44+
// a written parameter has an instance, which expected would name instead
45+
if (parameter.isEmpty() || !parameter.get().getPosition().isValidPosition()
46+
|| !(context.getVariableByName(variable)instanceof Variable declared)
47+
|| !declared.getMainRefinement().isBooleanTrue() || isRefined(parameter.get(), context))
48+
return List.of();
49+
notes.add(String.format("From: %s is a parameter of %s (line %d) with no refinement",
50+
error.getDisplayNames().get(variable), signature(method.get()),
51+
parameter.get().getPosition().getLine()));
52+
}
53+
return notes;
54+
}
55+
56+
/**
57+
* Whether the parameter is written with a refinement, even {@code true}, or a specification of its method refines
58+
* it, such as an external one, which the method's body is not checked against
59+
*/
60+
private static boolean isRefined(CtParameter<?> parameter, Context context) {
61+
if (parameter.getAnnotations().stream()
62+
.anyMatch(a -> a.getAnnotationType().getQualifiedName().equals("liquidjava.specification.Refinement")))
63+
return true;
64+
CtExecutable<?> method = parameter.getParent();
65+
int index = method.getParameters().indexOf(parameter);
66+
String owner = method.getParent(CtType.class).getQualifiedName();
67+
String name = method instanceof CtConstructor<?> ? Utils.getSimpleName(owner) : method.getSimpleName();
68+
List<String> types = method.getParameters().stream().map(p -> p.getType().getQualifiedName()).toList();
69+
return context.getCtxFunctions().stream()
70+
.filter(f -> owner.equals(f.getTargetClass()) && name.equals(Utils.getSimpleName(f.getName()))
71+
&& types.equals(f.getArguments().stream()
72+
.map(a -> a.getType() == null ? null : a.getType().getQualifiedName()).toList()))
73+
.anyMatch(f -> !f.getArguments().get(index).getMainRefinement().isBooleanTrue());
74+
}
75+
76+
private static Optional<CtExecutable<?>> enclosingMethod(CtElement element) {
77+
CtElement parent = element.getParent();
78+
while (parent != null && !(parent instanceof CtMethod<?> || parent instanceof CtConstructor<?>))
79+
parent = parent.getParent();
80+
return Optional.ofNullable((CtExecutable<?>) parent);
81+
}
82+
83+
/** The method as {@code name(ParameterType, ...)}, a constructor by its class name */
84+
private static String signature(CtExecutable<?> method) {
85+
String name = method instanceof CtConstructor<?> constructor ? constructor.getDeclaringType().getSimpleName()
86+
: method.getSimpleName();
87+
return name + method.getParameters().stream().map(p -> p.getType().getSimpleName())
88+
.collect(Collectors.joining(", ", "(", ")"));
89+
}
90+
}

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,11 @@ public void processSubtyping(Predicate expectedType, List<GhostState> list, CtEl
8787
}
8888
DebugLog.smtResult(result);
8989
if (result.isError()) {
90-
throw new RefinementError(element.getPosition(), declarationPosition, expectedType,
90+
RefinementError error = new RefinementError(element.getPosition(), declarationPosition, expectedType,
9191
implBeforeChange.simplify(NeededVariables.forRefinementError(expectedType)), map,
9292
result.getCounterexample(), customMessage, context.getAliases());
93+
FromNotes.unrefinedParameters(error, element, context).forEach(error::addMessageNote);
94+
throw error;
9395
}
9496
}
9597

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package liquidjava.api.tests;
2+
3+
import static org.junit.jupiter.api.Assertions.*;
4+
5+
import java.util.List;
6+
7+
import org.junit.jupiter.api.Test;
8+
9+
import liquidjava.api.CommandLineLauncher;
10+
import liquidjava.diagnostics.Diagnostics;
11+
import liquidjava.diagnostics.errors.LJError;
12+
13+
class TestFromNotes {
14+
15+
private static final String TEST_SUITE = "../liquidjava-example/src/main/java/testSuite/";
16+
17+
@Test
18+
void unrefinedParameterIsNamedWithItsMethod() {
19+
LJError error = verify("ErrorUnknownArgument.java");
20+
assertEquals(List.of("From: amount is a parameter of use(int) (line 10) with no refinement"),
21+
error.getMessageNotes());
22+
assertTrue(error.toString().contains(error.getMessage() + "\n From: amount is a parameter"));
23+
}
24+
25+
private static LJError verify(String test) {
26+
CommandLineLauncher.launch(TEST_SUITE + test);
27+
List<LJError> errors = Diagnostics.getInstance().getErrors().stream().toList();
28+
assertEquals(1, errors.size(), "Expected exactly one error from " + test);
29+
return errors.get(0);
30+
}
31+
}

0 commit comments

Comments
 (0)