Skip to content

Commit 6a3baee

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 f44048f commit 6a3baee

23 files changed

Lines changed: 353 additions & 113 deletions

‎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/AliasExpansion.java‎

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
import liquidjava.rj_language.ast.AliasInvocation;
1212
import liquidjava.rj_language.ast.Expression;
1313
import liquidjava.rj_language.ast.Var;
14+
import liquidjava.rj_language.ast.formatter.DisplayNames;
15+
import liquidjava.rj_language.ast.formatter.ExpressionFormatter;
1416
import spoon.reflect.cu.SourcePosition;
1517

1618
/**
@@ -40,19 +42,19 @@ public String declaration() {
4042

4143
/**
4244
* The expansions of the alias invocations in an expression, each invocation once in order of appearance, followed
43-
* by those of the aliases their definitions use
45+
* by those of the aliases their definitions use, naming the variables as the diagnostic does
4446
*/
45-
public static List<AliasExpansion> of(Expression expression, List<AliasWrapper> aliases) {
47+
public static List<AliasExpansion> of(Expression expression, List<AliasWrapper> aliases, DisplayNames names) {
4648
Map<String, AliasWrapper> byName = new LinkedHashMap<>();
4749
for (AliasWrapper alias : aliases)
4850
byName.put(alias.getName(), alias);
4951
Map<String, AliasExpansion> expansions = new LinkedHashMap<>();
50-
collect(expression, byName, expansions, new HashSet<>());
52+
collect(expression, byName, names, expansions, new HashSet<>());
5153
return new ArrayList<>(expansions.values());
5254
}
5355

5456
// Expands the invocations under expression, skipping aliases already being expanded so recursive ones end
55-
private static void collect(Expression expression, Map<String, AliasWrapper> aliases,
57+
private static void collect(Expression expression, Map<String, AliasWrapper> aliases, DisplayNames names,
5658
Map<String, AliasExpansion> expansions, Set<String> expanding) {
5759
List<AliasInvocation> invocations = new ArrayList<>();
5860
invocationsOf(expression, invocations);
@@ -64,14 +66,16 @@ private static void collect(Expression expression, Map<String, AliasWrapper> ali
6466
if (alias == null || expanding.contains(alias.getName()) || expansions.containsKey(key))
6567
continue;
6668
Expression body = substituteArguments(alias, invocation.getArgs());
67-
expansions.put(key, new AliasExpansion(invocation.toDisplayString(), body.toDisplayString(),
68-
alias.getName(), alias.getVarNames(), alias.getDefinition(), alias.getPosition()));
69+
expansions.put(key,
70+
new AliasExpansion(ExpressionFormatter.format(invocation, names),
71+
ExpressionFormatter.format(body, names), alias.getName(), alias.getVarNames(),
72+
alias.getDefinition(), alias.getPosition()));
6973
bodies.put(key, body);
7074
}
7175
for (Map.Entry<String, Expression> body : bodies.entrySet()) {
7276
Set<String> inner = new HashSet<>(expanding);
7377
inner.add(expansions.get(body.getKey()).alias());
74-
collect(body.getValue(), aliases, expansions, inner);
78+
collect(body.getValue(), aliases, names, expansions, inner);
7579
}
7680
}
7781

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package liquidjava.diagnostics.errors;
2+
3+
import static liquidjava.rj_language.opt.VCSimplificationUtils.linkedToExpected;
4+
5+
import java.util.ArrayList;
6+
import java.util.List;
7+
import java.util.function.Predicate;
8+
9+
import liquidjava.diagnostics.TranslationTable;
10+
import liquidjava.processor.context.PlacementInCode;
11+
import liquidjava.rj_language.ast.Expression;
12+
import liquidjava.rj_language.ast.formatter.DisplayNames;
13+
import liquidjava.rj_language.ast.formatter.ExpressionFormatter;
14+
import liquidjava.rj_language.opt.VCSimplificationResult;
15+
16+
/** The message of an error that found does not prove expected, and the names it gives the variables of both */
17+
record ErrorMessage(String text, DisplayNames names) {
18+
19+
/**
20+
* Writes the facts of found about the values expected constrains and expected in frame, or only expected when
21+
* showsFound rejects those facts; path conditions about other values stay in the derivation
22+
*/
23+
static ErrorMessage of(Expression expected, VCSimplificationResult found, TranslationTable translationTable,
24+
Predicate<Expression> showsFound, String frame) {
25+
Expression shown = linkedToExpected(found.getImplication().toPredicate().getExpression(), expected);
26+
// the names of the message first, then those only the original found has, each with the line of its value
27+
List<String> message = new ArrayList<>();
28+
shown.getVariableNames(message);
29+
expected.getVariableNames(message);
30+
VCSimplificationResult original = found.getOriginal();
31+
List<String> others = original.getBinders();
32+
original.getImplication().toPredicate().getExpression().getVariableNames(others);
33+
DisplayNames names = DisplayNames.of(message, others, name -> {
34+
PlacementInCode placement = translationTable == null ? null : translationTable.get(name);
35+
return placement == null ? null : placement.getLine();
36+
});
37+
String expectedText = ExpressionFormatter.format(expected, names);
38+
String text = showsFound.test(shown)
39+
? String.format(frame, ExpressionFormatter.format(shown, names), expectedText)
40+
: String.format("expected %s but could not prove it", expectedText);
41+
return new ErrorMessage(text, names);
42+
}
43+
}

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import liquidjava.diagnostics.LJDiagnostic;
44
import liquidjava.diagnostics.TranslationTable;
55
import liquidjava.diagnostics.Colors;
6+
import liquidjava.rj_language.ast.formatter.DisplayNames;
67
import spoon.reflect.cu.SourcePosition;
78

89
/**
@@ -11,18 +12,30 @@
1112
public abstract class LJError extends LJDiagnostic {
1213

1314
private final TranslationTable translationTable;
15+
private final DisplayNames displayNames;
1416

1517
public LJError(String title, String message, SourcePosition pos, TranslationTable translationTable) {
1618
this(title, message, pos, translationTable, null);
1719
}
1820

1921
public LJError(String title, String message, SourcePosition pos, TranslationTable translationTable,
2022
String customMessage) {
21-
super(title, message, pos, Colors.BOLD_RED, customMessage);
23+
this(title, new ErrorMessage(message, null), pos, translationTable, customMessage);
24+
}
25+
26+
LJError(String title, ErrorMessage message, SourcePosition pos, TranslationTable translationTable,
27+
String customMessage) {
28+
super(title, message.text(), pos, Colors.BOLD_RED, customMessage);
2229
this.translationTable = translationTable != null ? translationTable : new TranslationTable();
30+
this.displayNames = message.names();
2331
}
2432

2533
public TranslationTable getTranslationTable() {
2634
return translationTable;
2735
}
36+
37+
/** The names the error gives its variables, for every view of it to show them alike, or null when it has none */
38+
public DisplayNames getDisplayNames() {
39+
return displayNames;
40+
}
2841
}

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

Lines changed: 8 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
import liquidjava.rj_language.ast.Enum;
2020
import liquidjava.rj_language.ast.Expression;
2121
import liquidjava.rj_language.ast.Var;
22-
import liquidjava.rj_language.ast.formatter.VariableFormatter;
2322
import liquidjava.rj_language.opt.VCSimplificationResult;
2423
import liquidjava.smt.Counterexample;
2524
import liquidjava.utils.GeneratedNames;
@@ -52,19 +51,20 @@ public class RefinementError extends LJError {
5251
public RefinementError(SourcePosition position, SourcePosition declarationPosition, Predicate expected,
5352
VCSimplificationResult found, TranslationTable translationTable, Counterexample counterexample,
5453
String customMessage, List<AliasWrapper> aliases) {
55-
super("Refinement Error", message(expected, found), position, translationTable, customMessage);
54+
super("Refinement Error", ErrorMessage.of(expected.getExpression(), found, translationTable,
55+
shown -> !isTrue(shown), "%s is not a subtype of %s"), position, translationTable, customMessage);
5656
this.expected = expected;
5757
this.found = found;
5858
Expression foundExpression = found.getImplication().toPredicate().getExpression();
59-
this.foundAliases = AliasExpansion.of(foundExpression, aliases);
60-
this.expectedAliases = AliasExpansion.of(expected.getExpression(), aliases);
59+
this.foundAliases = AliasExpansion.of(foundExpression, aliases, getDisplayNames());
60+
this.expectedAliases = AliasExpansion.of(expected.getExpression(), aliases, getDisplayNames());
6161
this.aliasDeclarations = aliasDeclarations(linkedToExpected(foundExpression, expected.getExpression()),
6262
expected.getExpression(), aliases);
6363
this.counterexample = filterCounterexample(counterexample);
6464
this.declarationPosition = declarationPosition;
6565
if (!this.counterexample.isEmpty()) {
6666
String counterexampleString = this.counterexample.assignments().stream()
67-
.map(a -> VariableFormatter.format(a.first()) + " == " + a.second())
67+
.map(a -> getDisplayNames().get(a.first()) + " == " + a.second())
6868
.collect(Collectors.joining(" && "));
6969
setCounterexampleStr("Counterexample: " + counterexampleString);
7070
}
@@ -74,23 +74,11 @@ public RefinementError(SourcePosition position, SourcePosition declarationPositi
7474
"Not enough information to prove the expected refinement. Add a refinement or condition to constrain it."));
7575
}
7676

77-
// Only the facts of found about the values expected constrains; path conditions about other values stay in the
78-
// derivation
79-
private static String message(Predicate expected, VCSimplificationResult found) {
80-
Expression shown = linkedToExpected(found.getImplication().toPredicate().getExpression(),
81-
expected.getExpression());
82-
if (isTrue(shown))
83-
return String.format("expected %s but could not prove it", expected.getExpression().toDisplayString());
84-
return String.format("%s is not a subtype of %s", shown.toDisplayString(),
85-
expected.getExpression().toDisplayString());
86-
}
87-
8877
// Each alias the message uses once, with those their definitions use, as declared
89-
private static List<String> aliasDeclarations(Expression shownFound, Expression expected,
90-
List<AliasWrapper> aliases) {
78+
private List<String> aliasDeclarations(Expression shownFound, Expression expected, List<AliasWrapper> aliases) {
9179
Map<String, String> declarations = new LinkedHashMap<>();
9280
for (Expression shown : List.of(shownFound, expected))
93-
for (AliasExpansion expansion : AliasExpansion.of(shown, aliases))
81+
for (AliasExpansion expansion : AliasExpansion.of(shown, aliases, getDisplayNames()))
9482
declarations.putIfAbsent(expansion.alias(), expansion.declaration());
9583
return new ArrayList<>(declarations.values());
9684
}
@@ -182,10 +170,7 @@ private static boolean states(Expression term, Expression value, Pair<String, St
182170

183171
// Binders of the VC chain before simplification, which keeps the variables in the order they were introduced
184172
private List<String> originalBinders() {
185-
VCSimplificationResult original = getFound();
186-
while (original.getOrigin() != null)
187-
original = original.getOrigin();
188-
return original.getBinders();
173+
return getFound().getOriginal().getBinders();
189174
}
190175

191176
// 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: 3 additions & 11 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

@@ -30,7 +29,9 @@ public class StateRefinementError extends LJError {
3029
public StateRefinementError(SourcePosition position, SourcePosition declarationPosition, Predicate expected,
3130
VCSimplificationResult found, TranslationTable translationTable, String customMessage, List<Hint> hints,
3231
ErrorCategory category, StateErrorContext context) {
33-
super("State Refinement Error", message(expected, found), position, translationTable, customMessage);
32+
super("State Refinement Error", ErrorMessage.of(expected.getExpression(), found, translationTable,
33+
StateRefinementError::appliesGhost, "found %s but expected %s"), position, translationTable,
34+
customMessage);
3435
this.declarationPosition = declarationPosition;
3536
this.expected = expected;
3637
this.found = found;
@@ -44,15 +45,6 @@ public StateRefinementError(SourcePosition position, SourcePosition declarationP
4445

4546
// Only the facts of found about the objects of expected, and only when one of them tells a state: an alias or a
4647
// 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());
50-
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());
54-
}
55-
5648
private static boolean appliesGhost(Expression expression) {
5749
return expression instanceof FunctionInvocation
5850
|| expression.getChildren().stream().anyMatch(StateRefinementError::appliesGhost);

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
import java.util.HashMap;
44
import java.util.HashSet;
55
import java.util.Map;
6+
import java.util.Objects;
67
import java.util.Set;
8+
import java.util.stream.Stream;
79

810
import liquidjava.api.CommandLineLauncher;
911
import liquidjava.utils.Utils;
@@ -96,4 +98,11 @@ public Set<RefinedFunction> getMethods() {
9698
public Map<String, Set<String>> getFileScopes() {
9799
return fileScopes;
98100
}
101+
102+
/** The line where the value of a saved variable comes from, or null when unknown */
103+
public Integer lineOf(String name) {
104+
return Stream.concat(localVars.stream(), globalVars.stream()).filter(v -> v.getName().equals(name))
105+
.map(RefinedVariable::getPlacementInCode).filter(Objects::nonNull).map(PlacementInCode::getLine)
106+
.filter(Objects::nonNull).findFirst().orElse(null);
107+
}
99108
}

‎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: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
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 display name of a variable, or of a ghost application over variables such as {@code size(#s_2)}
48+
*/
49+
public String get(String name) {
50+
return VariableFormatter.format(name, variable -> tags.getOrDefault(variable, ""));
51+
}
52+
53+
private void tag(List<String> alike, Collection<String> shown, Function<String, Integer> lineOf) {
54+
String plain = plainOf(alike, shown);
55+
Map<String, List<String>> byLine = alike.stream().filter(name -> !name.equals(plain))
56+
.sorted(Comparator.comparingInt(VariableFormatter::counterOf))
57+
.collect(Collectors.groupingBy(name -> lineTag(lineOf.apply(name))));
58+
byLine.forEach((line, variables) -> {
59+
for (int i = 0; i < variables.size(); i++)
60+
tags.put(variables.get(i), tagFor(line, i, variables.size()));
61+
});
62+
}
63+
64+
// The declared variable, or else the latest instance, preferring those shown
65+
private static String plainOf(List<String> alike, Collection<String> shown) {
66+
List<String> candidates = alike.stream().filter(shown::contains).toList();
67+
return (candidates.isEmpty() ? alike : candidates).stream()
68+
.max(Comparator.comparing((String name) -> VariableFormatter.counterOf(name) < 0)
69+
.thenComparing(VariableFormatter::counterOf))
70+
.orElseThrow();
71+
}
72+
73+
// The line alone tells apart a single value from it; values that share a line, or have none, are numbered
74+
private static String tagFor(String line, int index, int count) {
75+
return count == 1 && !line.isEmpty() ? line : line + "#" + (index + 1);
76+
}
77+
78+
private static String lineTag(Integer line) {
79+
return line == null ? "" : "@" + line;
80+
}
81+
}

0 commit comments

Comments
 (0)