Skip to content

Commit 9aa2467

Browse files
Write a call argument as the caller wrote it in refinement errors (#486)
## Problem When a call argument is itself a call, the refinement error named its value after the callee's parameter (`bound`), which is not in the caller's code. ## Before / after Issue reproducer (`return nextInt(size());`): ``` before: bound³ >= 0 is not a subtype of bound³ > 0 Counterexample: bound³ == 0 after: size()³ >= 0 is not a subtype of size()³ > 0 Counterexample: size()³ == 0 ``` Real case, fixture `classes/found_binders_error` (`random.nextInt(items.size())`, as in chaos-http-proxy's `failures.size()`): ``` before: bound⁶ == size(this.items) && bound⁶ >= 0 is not a subtype of bound⁶ > 0 ∀bound⁶ bound⁶ == size(this.items) && bound⁶ >= 0 Counterexample: bound⁶ == 0 after: items.size()⁶ == size(this.items) && items.size()⁶ >= 0 is not a subtype of items.size()⁶ > 0 ∀items.size()⁶ items.size()⁶ == size(this.items) && items.size()⁶ >= 0 Counterexample: items.size()⁶ == 0 ``` Nested calls: `nextInt(dec(size()))` gives `size()¹⁹ >= 0 && dec(size())²¹ == size()¹⁹ - 1 is not a subtype of dec(size())²¹ > 0`. An unrefined call reads `plain()⁸ == #plain_result⁷` (result name from #483). The argument keeps its instance counter, like every other instance, so two calls written alike (`between(next(), next())`) stay apart. ## Unchanged Plain variables (`positive(t)`), field reads (`bound == this.f`), other expressions (`bound == this.f - 1`) and literals keep the parameter name: their refinement is `bound == <their code>`, so writing them as code would read as a tautology (`0 == 0`, which `TestGeneratedNames` would catch). ## How Same approach as `FieldDisplayNames` (#459): `MethodsFunctionsChecker` records the call's source text as written (Spoon's original source fragment, so `count()` is not printed as `Basket.count()`) for the argument instance in the new `ArgumentDisplayNames`. `VariableFormatter.format` writes it with the counter, and `ExpressionNameResolver` keeps it whole, as it does for fields (`items.size()` is not cut to `size()`). A call with no source position (built by Spoon, not read from a file) keeps the parameter name. `CommandLineLauncher` clears the record at the start of each run. The internal name and the SMT query are unchanged. ## Size +68 / −7 over 7 files (main code +48 / −2; the new `ArgumentDisplayNames` is 35 of those lines, most of them Javadoc). ## Tests - `TestFoundBinders`: the reproducer's rows (also in the original step of the history) now use `items.size()`; new `callArgumentIsWrittenAsTheCallerWroteIt` on a new fixture line `random.nextInt(count())`, checking the message and counterexample read `count()`, not `bound` or `Basket.count()`. - All of them fail on `main`, and the rows fail without the `ExpressionNameResolver` change. - `mvn test`: 693 tests, 0 failures. Closes #481 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 2a9b40d commit 9aa2467

7 files changed

Lines changed: 68 additions & 7 deletions

File tree

‎liquidjava-example/src/main/java/testSuite/classes/found_binders_error/Basket.java‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,4 +68,14 @@ void addUp(int first, int second) {
6868
void forward(int t) {
6969
positive(t); // Expect: Refinement Error
7070
}
71+
72+
@Refinement("_ >= 0")
73+
static int count() {
74+
return 0;
75+
}
76+
77+
// a call argument is written as the caller wrote it, not as the callee's parameter
78+
int pickByCount() {
79+
return random.nextInt(count()); // Expect: Refinement Error
80+
}
7181
}

‎liquidjava-verifier/src/main/java/liquidjava/api/CommandLineLauncher.java‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import liquidjava.diagnostics.warnings.CustomWarning;
1010
import liquidjava.processor.RefinementProcessor;
1111
import liquidjava.processor.context.ContextHistory;
12+
import liquidjava.rj_language.ast.formatter.ArgumentDisplayNames;
1213
import liquidjava.rj_language.ast.formatter.FieldDisplayNames;
1314
import liquidjava.specification.Refinement;
1415
import picocli.CommandLine;
@@ -59,6 +60,7 @@ public static void launch(String... paths) {
5960
diagnostics.clear();
6061
contextHistory.clearHistory();
6162
FieldDisplayNames.clear();
63+
ArgumentDisplayNames.clear();
6264
Launcher launcher = new Launcher();
6365
for (String path : paths) {
6466
if (!new File(path).exists()) {

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import liquidjava.processor.refinement_checker.object_checkers.AuxHierarchyRefinementsPassage;
1717
import liquidjava.processor.refinement_checker.object_checkers.AuxStateHandler;
1818
import liquidjava.rj_language.Predicate;
19+
import liquidjava.rj_language.ast.formatter.ArgumentDisplayNames;
1920
import liquidjava.utils.GeneratedNames;
2021
import liquidjava.utils.Utils;
2122
import liquidjava.utils.constants.Formats;
@@ -459,6 +460,10 @@ private String createVariableRepresentingArgument(CtExpression<?> iArg, Variable
459460
met = Predicate.createEquals(Predicate.createVar(Keys.WILDCARD), met);
460461
String nVar = String.format(Formats.INSTANCE, fArg.getName(), rtc.getContext().getCounter());
461462
rtc.getContext().addInstanceToContext(nVar, fArg.getType(), met.substituteVariable(Keys.WILDCARD, nVar), iArg);
463+
// a call in the source reads as written; any other argument is refined as equal to its code, which would read
464+
// as a tautology
465+
if (iArg instanceof CtInvocation && iArg.getPosition().isValidPosition())
466+
ArgumentDisplayNames.recordArgument(nVar, iArg.getOriginalSourceFragment().getSourceCode());
462467
return nVar;
463468
}
464469

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package liquidjava.rj_language.ast.formatter;
2+
3+
import java.util.Map;
4+
import java.util.concurrent.ConcurrentHashMap;
5+
6+
/**
7+
* The code of the call arguments that a verification names after the callee's parameter, so that
8+
* {@link VariableFormatter} can write such an argument as the caller wrote it ({@code items.size()} rather than
9+
* {@code bound}). Filled during a verification and cleared when the next one starts
10+
*/
11+
public final class ArgumentDisplayNames {
12+
private static final Map<String, String> ARGUMENTS = new ConcurrentHashMap<>();
13+
14+
private ArgumentDisplayNames() {
15+
}
16+
17+
/**
18+
* Records that an argument instance ({@code #<parameter>_<n>}) stands for the argument {@code code}. Only the
19+
* display changes
20+
*/
21+
public static void recordArgument(String instanceName, String code) {
22+
ARGUMENTS.put(instanceName, code);
23+
}
24+
25+
/**
26+
* Forgets what the previous verification recorded
27+
*/
28+
public static void clear() {
29+
ARGUMENTS.clear();
30+
}
31+
32+
static String argumentCode(String instanceName) {
33+
return ARGUMENTS.get(instanceName);
34+
}
35+
}

‎liquidjava-verifier/src/main/java/liquidjava/rj_language/ast/formatter/ExpressionNameResolver.java‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ public static ExpressionNameResolver forExpression(Expression expression) {
2222

2323
public String resolveVariable(String name) {
2424
String formatted = VariableFormatter.format(name);
25-
// a field is always written as in code, as its binder and the counterexample write it
26-
if (VariableFormatter.isField(name))
25+
// a field or call argument is always written as in code, as its binder and the counterexample write it
26+
if (VariableFormatter.isField(name) || ArgumentDisplayNames.argumentCode(name) != null)
2727
return formatted;
2828
return isAmbiguous(name, variables, ExpressionNameResolver::getVariableSimpleName) ? formatted
2929
: Utils.getSimpleName(formatted);

‎liquidjava-verifier/src/main/java/liquidjava/rj_language/ast/formatter/VariableFormatter.java‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ public static String format(String name) {
2525

2626
String baseName = matcher.group(1);
2727
String counter = matcher.group(2);
28+
// the counter tells apart calls written alike, which can return different values
29+
String argument = ArgumentDisplayNames.argumentCode(name);
30+
if (argument != null)
31+
return argument + toSuperscript(counter);
2832
String receiver = FieldDisplayNames.writeReceiver(name);
2933
if (receiver != null && isField(baseName))
3034
return (receiver.isEmpty() ? "" : receiver + ".") + fieldOf(baseName) + toSuperscript(counter);

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

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,16 +57,21 @@ static void verify() {
5757
@Test
5858
void issueReproducerDropsTheTrueFieldRow() {
5959
Shown shown = ERRORS.get(25);
60-
assertEquals(List.of("∀bound bound == size(this.items) && bound >= 0"), shown.rows());
61-
assertEquals("bound == size(this.items) && bound >= 0 is not a subtype of bound > 0", shown.message());
60+
assertEquals(List.of("∀items.size() items.size() == size(this.items) && items.size() >= 0"), shown.rows());
61+
}
62+
63+
@Test
64+
void callArgumentIsWrittenAsTheCallerWroteIt() {
65+
Shown shown = ERRORS.get(79);
66+
assertEquals("count() >= 0 is not a subtype of count() > 0", shown.message());
67+
assertEquals("Counterexample: count() == 0", shown.counterexample());
6268
}
6369

6470
@Test
6571
void steppingBackReachesTheOriginalWithTheDroppedRow() {
6672
Shown shown = ERRORS.get(25);
67-
assertEquals(
68-
List.of("∀this.items true", "∀items items == this.items", "∀bound bound == size(items) && bound >= 0"),
69-
shown.originalRows());
73+
assertEquals(List.of("∀this.items true", "∀items items == this.items",
74+
"∀items.size() items.size() == size(items) && items.size() >= 0"), shown.originalRows());
7075
assertEquals(List.of("Substitution", "Binder"), shown.steps());
7176
assertEquals(List.of("this#testSuite.classes.found_binders_error.Basket.items", "#items_", "#bound_"),
7277
shown.originalBinders());

0 commit comments

Comments
 (0)