Skip to content

Commit 10efb8e

Browse files
Say what a call's return refinement guarantees about an argument it gives
A refinement error whose Expected is about a call argument that is a call to a refined method now has the note "From: <call> (<File>:<line>) guarantees only <refinement>", instantiated as the error writes it. An equality _ == g(...) defining a ghost is left out when the ghost is nowhere else in the refinement: then it constrains no value, so what is left is exactly what the refinement says about the value. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 3895e35 commit 10efb8e

4 files changed

Lines changed: 87 additions & 1 deletion

File tree

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

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,20 @@
1616
import spoon.reflect.declaration.CtParameter;
1717
import spoon.reflect.declaration.CtType;
1818
import liquidjava.utils.Utils;
19+
import liquidjava.utils.constants.Keys;
20+
import liquidjava.processor.context.CallResult;
21+
import liquidjava.processor.context.VariableInstance;
22+
import liquidjava.rj_language.ast.BinaryExpression;
23+
import liquidjava.rj_language.ast.Expression;
24+
import liquidjava.rj_language.ast.FunctionInvocation;
25+
import liquidjava.rj_language.ast.LiteralBoolean;
26+
import liquidjava.rj_language.ast.Var;
27+
import liquidjava.rj_language.ast.formatter.ExpressionFormatter;
28+
import spoon.reflect.cu.SourcePosition;
29+
30+
import static liquidjava.rj_language.opt.VCSimplificationUtils.containsExpression;
31+
import static liquidjava.rj_language.opt.VCSimplificationUtils.factsOf;
32+
import static liquidjava.rj_language.opt.VCSimplificationUtils.isTrue;
1933

2034
/**
2135
* The notes of a refinement error that say where the values its expected refinement is about come from. Each is given
@@ -53,6 +67,64 @@ static List<String> unrefinedParameters(RefinementError error, CtElement checked
5367
return notes;
5468
}
5569

70+
/**
71+
* For each argument of expected that is a call to a refined method, what the method's return refinement guarantees
72+
* about it. An equality defining a ghost, {@code _ == size(this)}, is left out when the ghost is nowhere else in
73+
* the refinement: it then constrains no value, so what is left is all it says about the value
74+
*/
75+
static List<String> returnedValues(RefinementError error, Context context) {
76+
List<String> notes = new ArrayList<>();
77+
for (String variable : error.getExpected().getVariableNames().stream().distinct().toList()) {
78+
// a call argument, which has no variable of its own and is shown as the call
79+
if (!(context.getVariableByName(variable)instanceof VariableInstance instance)
80+
|| instance.getReturnedBy().isEmpty() || context.getVariableFromInstance(instance) != null)
81+
continue;
82+
CallResult call = instance.getReturnedBy().get();
83+
SourcePosition declared = call.function().getPlacementInCode() == null ? null
84+
: call.function().getPlacementInCode().getAnnotationPosition();
85+
if (declared == null || !declared.isValidPosition() || declared.getFile() == null)
86+
continue;
87+
Expression guaranteed = withoutGhostDefinitions(call.returned().getExpression());
88+
String value = ExpressionFormatter.format(new Var(variable), error.getDisplayNames());
89+
String says = isTrue(guaranteed) ? "guarantees nothing about " + value
90+
: "guarantees only " + ExpressionFormatter.format(
91+
guaranteed.substitute(new Var(Keys.WILDCARD), new Var(variable)), error.getDisplayNames());
92+
notes.add(String.format("From: %s (%s:%d) %s", value, declared.getFile().getName(), declared.getLine(),
93+
says));
94+
}
95+
return notes;
96+
}
97+
98+
private static Expression withoutGhostDefinitions(Expression refinement) {
99+
List<Expression> facts = factsOf(refinement);
100+
Expression kept = new LiteralBoolean(true);
101+
for (Expression fact : facts) {
102+
boolean definesGhost = ghostDefined(fact).filter(
103+
ghost -> facts.stream().noneMatch(other -> other != fact && containsExpression(other, ghost)))
104+
.isPresent();
105+
if (!definesGhost)
106+
kept = isTrue(kept) ? fact : new BinaryExpression(kept, "&&", fact);
107+
}
108+
return kept;
109+
}
110+
111+
/** The ghost application {@code g(...)} of a fact {@code _ == g(...)} */
112+
private static Optional<Expression> ghostDefined(Expression fact) {
113+
if (!(fact instanceof BinaryExpression equality) || !"==".equals(equality.getOperator()))
114+
return Optional.empty();
115+
Expression left = equality.getFirstOperand();
116+
Expression right = equality.getSecondOperand();
117+
if (isWildcard(left) && right instanceof FunctionInvocation)
118+
return Optional.of(right);
119+
if (isWildcard(right) && left instanceof FunctionInvocation)
120+
return Optional.of(left);
121+
return Optional.empty();
122+
}
123+
124+
private static boolean isWildcard(Expression expression) {
125+
return expression instanceof Var var && Keys.WILDCARD.equals(var.getName());
126+
}
127+
56128
/**
57129
* Whether the parameter is written with a refinement, even {@code true}, or a specification of its method refines
58130
* it, such as an external one, which the method's body is not checked against

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ public void processSubtyping(Predicate expectedType, List<GhostState> list, CtEl
9191
implBeforeChange.simplify(NeededVariables.forRefinementError(expectedType)), map,
9292
result.getCounterexample(), customMessage, context.getAliases());
9393
FromNotes.unrefinedParameters(error, element, context).forEach(error::addMessageNote);
94+
FromNotes.returnedValues(error, context).forEach(error::addMessageNote);
9495
throw error;
9596
}
9697
}

‎liquidjava-verifier/src/main/java/liquidjava/processor/refinement_checker/general_checkers/MethodsFunctionsChecker.java‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -472,7 +472,11 @@ private String createVariableRepresentingArgument(CtExpression<?> iArg, Variable
472472
if (!unknown && !met.getVariableNames().contains(Keys.WILDCARD))
473473
met = Predicate.createEquals(Predicate.createVar(Keys.WILDCARD), met);
474474
String nVar = String.format(Formats.INSTANCE, fArg.getName(), rtc.getContext().getCounter());
475-
rtc.getContext().addInstanceToContext(nVar, fArg.getType(), met.substituteVariable(Keys.WILDCARD, nVar), iArg);
475+
RefinedVariable argument = rtc.getContext().addInstanceToContext(nVar, fArg.getType(),
476+
met.substituteVariable(Keys.WILDCARD, nVar), iArg);
477+
if (argument instanceof VariableInstance instance
478+
&& iArg.getMetadata(Keys.CALL_RESULT)instanceof CallResult call)
479+
instance.setReturnedBy(call);
476480
// a call in the source reads as written; any other argument is refined as equal to its code, which would read
477481
// as a tautology
478482
if (iArg instanceof CtInvocation && iArg.getPosition().isValidPosition())

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,15 @@ void stateOfAReceiverAnotherNameMayHoldIsNotTraced() {
4242
assertEquals(List.of(), captured.getMessageNotes());
4343
}
4444

45+
@Test
46+
void callArgumentIsTracedToWhatItsReturnRefinementGuaranteesAboutIt() {
47+
CommandLineLauncher.launch(TEST_SUITE + "classes/found_binders_error");
48+
LJError pick = Diagnostics.getInstance().getErrors().stream().filter(e -> e.getPosition().getLine() == 25)
49+
.findFirst().orElseThrow();
50+
assertEquals(List.of("From: items.size() (ArrayListRefinements.java:13) guarantees only items.size() >= 0"),
51+
pick.getMessageNotes());
52+
}
53+
4554
private static LJError verify(String test) {
4655
CommandLineLauncher.launch(TEST_SUITE + test);
4756
List<LJError> errors = Diagnostics.getInstance().getErrors().stream().toList();

0 commit comments

Comments
 (0)