Skip to content

Commit c732fd3

Browse files
Show variables by their name, with a line only to tell instances apart
Errors and context refinements wrote every variable instance with its verifier-wide counter in superscript digits (box¹⁵), which matched nothing in the code and mixed two Unicode blocks that many fonts draw unevenly. Each error, and each context refinement, now names its variables in one scope: a variable written alike to no other keeps its plain name; when several are, the declared variable or the latest instance shown keeps it and each other one gets the line its value comes from (x@12), numbered only when they share a line or have none (x@12#1). The message, counterexample and Found steps share the error's names, exposed with getDisplayNames(); ContextHistory.format(refinement) does the same for the context view. Verification, internal names and the translation table are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 327a0d1 commit c732fd3

21 files changed

Lines changed: 354 additions & 98 deletions

File tree

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,4 +78,11 @@ static int count() {
7878
int pickByCount() {
7979
return random.nextInt(count()); // Expect: Refinement Error
8080
}
81+
82+
// the earlier values of a variable are written with the line they come from
83+
void negate() {
84+
@Refinement("_ > 0")
85+
int b = 8;
86+
b = -b; // Expect: Refinement Error
87+
}
8188
}

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

Lines changed: 53 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
import liquidjava.rj_language.ast.Enum;
1616
import liquidjava.rj_language.ast.Expression;
1717
import liquidjava.rj_language.ast.Var;
18-
import liquidjava.rj_language.ast.formatter.VariableFormatter;
18+
import liquidjava.processor.context.PlacementInCode;
19+
import liquidjava.rj_language.ast.formatter.DisplayNames;
20+
import liquidjava.rj_language.ast.formatter.ExpressionFormatter;
1921
import liquidjava.rj_language.opt.VCSimplificationResult;
2022
import liquidjava.smt.Counterexample;
2123
import liquidjava.utils.GeneratedNames;
@@ -41,19 +43,27 @@ public class RefinementError extends LJError {
4143
private final VCSimplificationResult found;
4244
private final Counterexample counterexample;
4345
private final SourcePosition declarationPosition;
46+
private final DisplayNames displayNames;
4447

4548
public RefinementError(SourcePosition position, SourcePosition declarationPosition, Predicate expected,
4649
VCSimplificationResult found, TranslationTable translationTable, Counterexample counterexample,
4750
String customMessage) {
48-
super("Refinement Error", message(expected, found), position, translationTable, customMessage);
51+
this(position, declarationPosition, expected, found, translationTable, counterexample, customMessage,
52+
displayNames(expected, found, translationTable));
53+
}
54+
55+
private RefinementError(SourcePosition position, SourcePosition declarationPosition, Predicate expected,
56+
VCSimplificationResult found, TranslationTable translationTable, Counterexample counterexample,
57+
String customMessage, DisplayNames displayNames) {
58+
super("Refinement Error", message(expected, found, displayNames), position, translationTable, customMessage);
4959
this.expected = expected;
5060
this.found = found;
5161
this.counterexample = filterCounterexample(counterexample);
5262
this.declarationPosition = declarationPosition;
63+
this.displayNames = displayNames;
5364
if (!this.counterexample.isEmpty()) {
5465
String counterexampleString = this.counterexample.assignments().stream()
55-
.map(a -> VariableFormatter.format(a.first()) + " == " + a.second())
56-
.collect(Collectors.joining(" && "));
66+
.map(a -> displayNames.get(a.first()) + " == " + a.second()).collect(Collectors.joining(" && "));
5767
setCounterexampleStr("Counterexample: " + counterexampleString);
5868
}
5969
setCategory(categoryOf(expected, found));
@@ -64,13 +74,35 @@ public RefinementError(SourcePosition position, SourcePosition declarationPositi
6474

6575
// Only the facts of found about the values expected constrains; path conditions about other values stay in the
6676
// derivation
67-
private static String message(Predicate expected, VCSimplificationResult found) {
68-
Expression shown = linkedToExpected(found.getImplication().toPredicate().getExpression(),
69-
expected.getExpression());
77+
private static String message(Predicate expected, VCSimplificationResult found, DisplayNames names) {
78+
Expression shown = shownFound(expected, found);
79+
String expectedString = ExpressionFormatter.format(expected.getExpression(), names);
7080
if (isTrue(shown))
71-
return String.format("expected %s but could not prove it", expected.getExpression().toDisplayString());
72-
return String.format("%s is not a subtype of %s", shown.toDisplayString(),
73-
expected.getExpression().toDisplayString());
81+
return String.format("expected %s but could not prove it", expectedString);
82+
return String.format("%s is not a subtype of %s", ExpressionFormatter.format(shown, names), expectedString);
83+
}
84+
85+
/** The facts of found that the message shows */
86+
static Expression shownFound(Predicate expected, VCSimplificationResult found) {
87+
return linkedToExpected(found.getImplication().toPredicate().getExpression(), expected.getExpression());
88+
}
89+
90+
/**
91+
* The names of an error's variables: those of its message come first, then those only the steps of found show, each
92+
* with the line its value comes from
93+
*/
94+
static DisplayNames displayNames(Predicate expected, VCSimplificationResult found,
95+
TranslationTable translationTable) {
96+
List<String> shown = new ArrayList<>();
97+
shownFound(expected, found).getVariableNames(shown);
98+
expected.getExpression().getVariableNames(shown);
99+
VCSimplificationResult original = original(found);
100+
List<String> others = original.getBinders();
101+
original.getImplication().toPredicate().getExpression().getVariableNames(others);
102+
return DisplayNames.of(shown, others, name -> {
103+
PlacementInCode placement = translationTable == null ? null : translationTable.get(name);
104+
return placement == null ? null : placement.getLine();
105+
});
74106
}
75107

76108
/** Unknown when what was found says nothing about the values the expected refinement constrains */
@@ -97,6 +129,11 @@ public VCSimplificationResult getFound() {
97129
return found;
98130
}
99131

132+
/** The names this error gives its variables, for every view of it to show them alike */
133+
public DisplayNames getDisplayNames() {
134+
return displayNames;
135+
}
136+
100137
// Keeps the counterexample assignments to the terms of expected and of the found facts linked to them, except
101138
// values found already states, so there is none when found fixes every value expected depends on. The terms of
102139
// expected come first, those found says nothing about before the rest, then the linked terms in the order of the
@@ -144,10 +181,14 @@ private static boolean states(Expression term, Expression value, Pair<String, St
144181

145182
// Binders of the VC chain before simplification, which keeps the variables in the order they were introduced
146183
private List<String> originalBinders() {
147-
VCSimplificationResult original = getFound();
184+
return original(getFound()).getBinders();
185+
}
186+
187+
private static VCSimplificationResult original(VCSimplificationResult found) {
188+
VCSimplificationResult original = found;
148189
while (original.getOrigin() != null)
149190
original = original.getOrigin();
150-
return original.getBinders();
191+
return original;
151192
}
152193

153194
// 0 for terms of expected found says nothing about, whose values alone break it, 1 for the other terms of

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

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package liquidjava.diagnostics.errors;
22

33
import static liquidjava.rj_language.opt.VCSimplificationUtils.isTrue;
4-
import static liquidjava.rj_language.opt.VCSimplificationUtils.linkedToExpected;
54

65
import java.util.List;
76

@@ -12,6 +11,8 @@
1211
import liquidjava.rj_language.Predicate;
1312
import liquidjava.rj_language.ast.Expression;
1413
import liquidjava.rj_language.ast.FunctionInvocation;
14+
import liquidjava.rj_language.ast.formatter.DisplayNames;
15+
import liquidjava.rj_language.ast.formatter.ExpressionFormatter;
1516
import liquidjava.rj_language.opt.VCSimplificationResult;
1617
import spoon.reflect.cu.SourcePosition;
1718

@@ -26,11 +27,21 @@ public class StateRefinementError extends LJError {
2627
private final VCSimplificationResult found;
2728
private final SourcePosition declarationPosition;
2829
private final StateErrorContext context;
30+
private final DisplayNames displayNames;
2931

3032
public StateRefinementError(SourcePosition position, SourcePosition declarationPosition, Predicate expected,
3133
VCSimplificationResult found, TranslationTable translationTable, String customMessage, List<Hint> hints,
3234
ErrorCategory category, StateErrorContext context) {
33-
super("State Refinement Error", message(expected, found), position, translationTable, customMessage);
35+
this(position, declarationPosition, expected, found, translationTable, customMessage, hints, category, context,
36+
RefinementError.displayNames(expected, found, translationTable));
37+
}
38+
39+
private StateRefinementError(SourcePosition position, SourcePosition declarationPosition, Predicate expected,
40+
VCSimplificationResult found, TranslationTable translationTable, String customMessage, List<Hint> hints,
41+
ErrorCategory category, StateErrorContext context, DisplayNames displayNames) {
42+
super("State Refinement Error", message(expected, found, displayNames), position, translationTable,
43+
customMessage);
44+
this.displayNames = displayNames;
3445
this.declarationPosition = declarationPosition;
3546
this.expected = expected;
3647
this.found = found;
@@ -44,13 +55,12 @@ public StateRefinementError(SourcePosition position, SourcePosition declarationP
4455

4556
// Only the facts of found about the objects of expected, and only when one of them tells a state: an alias or a
4657
// guard on another value says nothing about the state expected needs
47-
private static String message(Predicate expected, VCSimplificationResult found) {
48-
Expression shown = linkedToExpected(found.getImplication().toPredicate().getExpression(),
49-
expected.getExpression());
58+
private static String message(Predicate expected, VCSimplificationResult found, DisplayNames names) {
59+
Expression shown = RefinementError.shownFound(expected, found);
60+
String expectedString = ExpressionFormatter.format(expected.getExpression(), names);
5061
if (!appliesGhost(shown))
51-
return String.format("expected %s but could not prove it", expected.getExpression().toDisplayString());
52-
return String.format("found %s but expected %s", shown.toDisplayString(),
53-
expected.getExpression().toDisplayString());
62+
return String.format("expected %s but could not prove it", expectedString);
63+
return String.format("found %s but expected %s", ExpressionFormatter.format(shown, names), expectedString);
5464
}
5565

5666
private static boolean appliesGhost(Expression expression) {
@@ -75,6 +85,11 @@ public VCSimplificationResult getFoundSimplification() {
7585
return found;
7686
}
7787

88+
/** The names this error gives its variables, for every view of it to show them alike */
89+
public DisplayNames getDisplayNames() {
90+
return displayNames;
91+
}
92+
7893
/** What is known about the receiver's states, or null if they could not be computed */
7994
public StateErrorContext getContext() {
8095
return context;

‎liquidjava-verifier/src/main/java/liquidjava/processor/context/ContextHistory.java‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,12 @@
44
import java.util.HashSet;
55
import java.util.Map;
66
import java.util.Set;
7+
import java.util.stream.Stream;
78

89
import liquidjava.api.CommandLineLauncher;
10+
import liquidjava.rj_language.Predicate;
11+
import liquidjava.rj_language.ast.formatter.DisplayNames;
12+
import liquidjava.rj_language.ast.formatter.ExpressionFormatter;
913
import liquidjava.utils.Utils;
1014
import spoon.reflect.cu.SourcePosition;
1115
import spoon.reflect.declaration.CtElement;
@@ -96,4 +100,19 @@ public Set<RefinedFunction> getMethods() {
96100
public Map<String, Set<String>> getFileScopes() {
97101
return fileScopes;
98102
}
103+
104+
/**
105+
* A refinement as the context shows it, its variables named as in {@link DisplayNames} with the lines of the
106+
* variables saved here
107+
*/
108+
public String format(Predicate refinement) {
109+
DisplayNames names = DisplayNames.of(refinement.getVariableNames(), Set.of(), this::lineOf);
110+
return ExpressionFormatter.format(refinement.getExpression(), names);
111+
}
112+
113+
private Integer lineOf(String name) {
114+
return Stream.concat(localVars.stream(), globalVars.stream()).filter(v -> v.getName().equals(name))
115+
.map(RefinedVariable::getPlacementInCode).filter(p -> p != null).map(PlacementInCode::getLine)
116+
.findFirst().orElse(null);
117+
}
99118
}

‎liquidjava-verifier/src/main/java/liquidjava/processor/context/PlacementInCode.java‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ public SourcePosition getAnnotationPosition() {
3131
return annotationPosition;
3232
}
3333

34+
/** The line of this placement, or null when it is not in code */
35+
public Integer getLine() {
36+
return position == null || !position.isValidPosition() ? null : position.getLine();
37+
}
38+
3439
public static PlacementInCode createPlacement(CtElement elem) {
3540
CtElement elemCopy = elem.clone();
3641
// cleanup annotations
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package liquidjava.rj_language.ast.formatter;
2+
3+
import java.util.Collection;
4+
import java.util.Comparator;
5+
import java.util.HashMap;
6+
import java.util.LinkedHashSet;
7+
import java.util.List;
8+
import java.util.Map;
9+
import java.util.Set;
10+
import java.util.function.Function;
11+
import java.util.stream.Collectors;
12+
13+
/**
14+
* The names of the variables of one scope, such as an error or a refinement, as they are shown. A name is written as in
15+
* code; when the scope has several variables written alike, one keeps the plain name and each other one gets the line
16+
* its value comes from, {@code x@12}, numbered when they share a line or have none, {@code x@12#1} and {@code x@12#2}.
17+
* The plain name goes to the declared variable, or else to its latest instance, preferring those shown first
18+
*/
19+
public final class DisplayNames {
20+
private final Map<String, String> tags = new HashMap<>();
21+
22+
private DisplayNames() {
23+
}
24+
25+
/**
26+
* @param shown
27+
* the names shown first, such as those of an error message
28+
* @param others
29+
* the other names of the scope
30+
* @param lineOf
31+
* the line where the value of a variable comes from, or null when unknown
32+
*/
33+
public static DisplayNames of(Collection<String> shown, Collection<String> others,
34+
Function<String, Integer> lineOf) {
35+
DisplayNames names = new DisplayNames();
36+
Set<String> all = new LinkedHashSet<>(shown);
37+
all.addAll(others);
38+
Map<String, List<String>> alike = all.stream().collect(Collectors.groupingBy(VariableFormatter::format));
39+
for (List<String> variables : alike.values()) {
40+
if (variables.size() > 1)
41+
names.tag(variables, shown, lineOf);
42+
}
43+
return names;
44+
}
45+
46+
/**
47+
* The names of an expression on its own, which has no lines to tell its variables apart
48+
*/
49+
static DisplayNames of(List<String> names) {
50+
return of(names, List.of(), name -> null);
51+
}
52+
53+
/**
54+
* The display name of a variable, or of a ghost application over variables such as {@code size(#s_2)}
55+
*/
56+
public String get(String name) {
57+
return VariableFormatter.format(name, variable -> tags.getOrDefault(variable, ""));
58+
}
59+
60+
private void tag(List<String> alike, Collection<String> shown, Function<String, Integer> lineOf) {
61+
List<String> candidates = alike.stream().filter(shown::contains).toList();
62+
String plain = (candidates.isEmpty() ? alike : candidates).stream()
63+
.max(Comparator.comparing((String name) -> VariableFormatter.counterOf(name) < 0)
64+
.thenComparing(VariableFormatter::counterOf))
65+
.orElseThrow();
66+
Map<String, List<String>> byLine = alike.stream().filter(name -> !name.equals(plain))
67+
.sorted(Comparator.comparingInt(VariableFormatter::counterOf))
68+
.collect(Collectors.groupingBy(name -> lineTag(lineOf.apply(name))));
69+
byLine.forEach((line, variables) -> {
70+
for (int i = 0; i < variables.size(); i++)
71+
tags.put(variables.get(i), variables.size() == 1 && !line.isEmpty() ? line : line + "#" + (i + 1));
72+
});
73+
}
74+
75+
private static String lineTag(Integer line) {
76+
return line == null ? "" : "@" + line;
77+
}
78+
}

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

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package liquidjava.rj_language.ast.formatter;
22

3+
import java.util.ArrayList;
34
import java.util.List;
45
import java.util.stream.Collectors;
56

@@ -28,16 +29,25 @@ public class ExpressionFormatter implements ExpressionVisitor<String> {
2829

2930
private final ExpressionNameResolver nameResolver;
3031

31-
private ExpressionFormatter(Expression expression) {
32-
this.nameResolver = ExpressionNameResolver.forExpression(expression);
32+
private ExpressionFormatter(Expression expression, DisplayNames names) {
33+
this.nameResolver = ExpressionNameResolver.forExpression(expression, names);
3334
}
3435

3536
public static String format(Predicate predicate) {
3637
return format(predicate.getExpression());
3738
}
3839

3940
public static String format(Expression expression) {
40-
return new ExpressionFormatter(expression).formatExpression(expression);
41+
List<String> variables = new ArrayList<>();
42+
expression.getVariableNames(variables);
43+
return format(expression, DisplayNames.of(variables));
44+
}
45+
46+
/**
47+
* Formats an expression of a scope, such as an error, naming its variables as the scope does
48+
*/
49+
public static String format(Expression expression, DisplayNames names) {
50+
return new ExpressionFormatter(expression, names).formatExpression(expression);
4151
}
4252

4353
private String formatExpression(Expression expression) {

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,20 @@
1313
final class ExpressionNameResolver {
1414
private final Set<String> variables = new HashSet<>();
1515
private final Set<String> functions = new HashSet<>();
16+
private final DisplayNames names;
1617

17-
public static ExpressionNameResolver forExpression(Expression expression) {
18-
ExpressionNameResolver resolver = new ExpressionNameResolver();
18+
private ExpressionNameResolver(DisplayNames names) {
19+
this.names = names;
20+
}
21+
22+
public static ExpressionNameResolver forExpression(Expression expression, DisplayNames names) {
23+
ExpressionNameResolver resolver = new ExpressionNameResolver(names);
1924
resolver.collect(expression);
2025
return resolver;
2126
}
2227

2328
public String resolveVariable(String name) {
24-
String formatted = VariableFormatter.format(name);
29+
String formatted = names.get(name);
2530
// a field or call argument is always written as in code, as its binder and the counterexample write it
2631
if (VariableFormatter.isField(name) || ArgumentDisplayNames.argumentCode(name) != null)
2732
return formatted;

0 commit comments

Comments
 (0)