Skip to content

Commit c1673bb

Browse files
Label errors with the kind of mistake they show
Each error now carries a category from the LiquidJava bug codebook, printed after its hints: - Out of order: a call in a single state that does not allow it - Out of domain: a value outside its refinement - Path-dependent state: a state that differs between branches, when the class has one state set - Unprovable: nothing is known about the object's state or the value Errors the rules cannot decide get no category. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 69895fa commit c1673bb

12 files changed

Lines changed: 270 additions & 20 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package testSuite;
2+
3+
import liquidjava.specification.Refinement;
4+
5+
public class ErrorOutOfDomainArgument {
6+
7+
static void take(@Refinement("_ > 0") int amount) {}
8+
9+
static void use() {
10+
take(-1); // Expect: Refinement Error
11+
}
12+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package testSuite;
2+
3+
import liquidjava.specification.StateRefinement;
4+
import liquidjava.specification.StateSet;
5+
6+
@StateSet({"closed", "opened"})
7+
@StateSet({"unlocked", "locked"})
8+
public class ErrorPathDependentOtherStateSet {
9+
10+
@StateRefinement(to = "closed(this) && unlocked(this)")
11+
public ErrorPathDependentOtherStateSet() {}
12+
13+
@StateRefinement(from = "closed(this)", to = "opened(this)")
14+
public void open() {}
15+
16+
@StateRefinement(from = "unlocked(this)", to = "locked(this)")
17+
public void lock() {}
18+
19+
@StateRefinement(from = "opened(this)")
20+
public void pass() {}
21+
22+
// the states differ only in the lock, which pass() does not check, so the path is not what is wrong
23+
public static void use(boolean secure) {
24+
ErrorPathDependentOtherStateSet d = new ErrorPathDependentOtherStateSet();
25+
if (secure)
26+
d.lock();
27+
d.pass(); // Expect: State Refinement Error
28+
}
29+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package testSuite;
2+
3+
import liquidjava.specification.Refinement;
4+
5+
public class ErrorUnprovableArgument {
6+
7+
static void take(@Refinement("_ > 0") int amount) {}
8+
9+
// nothing is known about amount, so its refinement can be neither violated nor proved
10+
static void use(int amount) {
11+
take(amount); // Expect: Refinement Error
12+
}
13+
}

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66
import java.util.Collections;
77
import java.util.List;
88
import java.util.Objects;
9+
import java.util.Optional;
910
import java.util.stream.Collectors;
1011

12+
import liquidjava.diagnostics.categories.ErrorCategory;
1113
import liquidjava.diagnostics.hints.Hint;
1214
import spoon.reflect.cu.SourcePosition;
1315

@@ -21,6 +23,7 @@ public class LJDiagnostic extends RuntimeException {
2123
private SourcePosition position;
2224
private final List<Hint> hints = new ArrayList<>();
2325
private String counterexample;
26+
private ErrorCategory category;
2427
private static final String PIPE = " | ";
2528

2629
public LJDiagnostic(String title, String message, SourcePosition pos, String accentColor, String customMessage) {
@@ -86,6 +89,14 @@ public void addHint(Hint hint) {
8689
hints.add(hint);
8790
}
8891

92+
public Optional<ErrorCategory> getCategory() {
93+
return Optional.ofNullable(category);
94+
}
95+
96+
public void setCategory(ErrorCategory category) {
97+
this.category = category;
98+
}
99+
89100
public void setCounterexampleStr(String counterexample) {
90101
this.counterexample = counterexample;
91102
}
@@ -116,6 +127,10 @@ public String toString() {
116127
sb.append(" --> ").append(String.join("\n ", hint.split("\n"))).append("\n");
117128
}
118129

130+
// category
131+
if (category != null)
132+
sb.append(" --> Category: ").append(category.title()).append("\n");
133+
119134
// location
120135
if (file != null && position != null) {
121136
sb.append("\n").append(file).append(":").append(position.getLine()).append(Colors.RESET).append("\n");
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package liquidjava.diagnostics.categories;
2+
3+
/**
4+
* The kind of mistake an error shows, named after the categories of the LiquidJava bug codebook. A category describes
5+
* the program according to its specification, so it cannot tell whether the specification itself is wrong.
6+
*/
7+
public enum ErrorCategory {
8+
9+
OUT_OF_ORDER("A1", "Out of order: the operation is legal for this object, but not right now"),
10+
OUT_OF_DOMAIN("A2", "Out of domain: the value is outside what is accepted"),
11+
PATH_DEPENDENT_STATE("C4.1", "Path-dependent state: the state differs depending on which branch ran"),
12+
UNPROVABLE("C4.2", "Unprovable: nothing is known that could prove the requirement");
13+
14+
private final String id;
15+
private final String title;
16+
17+
ErrorCategory(String id, String title) {
18+
this.id = id;
19+
this.title = title;
20+
}
21+
22+
/** The id of the category in the codebook */
23+
public String id() {
24+
return id;
25+
}
26+
27+
public String title() {
28+
return title;
29+
}
30+
}

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import java.util.stream.Collectors;
66

77
import liquidjava.diagnostics.TranslationTable;
8+
import liquidjava.diagnostics.categories.ErrorCategory;
89
import liquidjava.diagnostics.hints.Hint;
910
import liquidjava.rj_language.Predicate;
1011
import liquidjava.rj_language.ast.Expression;
@@ -46,11 +47,19 @@ public RefinementError(SourcePosition position, SourcePosition declarationPositi
4647
.collect(Collectors.joining(" && "));
4748
setCounterexampleStr("Counterexample: " + counterexampleString);
4849
}
50+
setCategory(categoryOf(expected, found));
4951
if (isTrue(found.getImplication().toPredicate().getExpression()))
5052
addHint(Hint.general(
5153
"Not enough information to prove the expected refinement. Add a refinement or condition to constrain it."));
5254
}
5355

56+
/** Unprovable when what was found says nothing about the values the expected refinement constrains */
57+
private static ErrorCategory categoryOf(Predicate expected, VCSimplificationResult found) {
58+
List<String> foundVariables = found.getImplication().toPredicate().getVariableNames();
59+
boolean constrained = expected.getVariableNames().stream().anyMatch(foundVariables::contains);
60+
return constrained ? ErrorCategory.OUT_OF_DOMAIN : ErrorCategory.UNPROVABLE;
61+
}
62+
5463
@Override
5564
public SourcePosition getDeclarationPosition() {
5665
return declarationPosition;

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import java.util.List;
66

77
import liquidjava.diagnostics.TranslationTable;
8+
import liquidjava.diagnostics.categories.ErrorCategory;
89
import liquidjava.diagnostics.hints.Hint;
910
import liquidjava.processor.VCImplication;
1011
import liquidjava.rj_language.Predicate;
@@ -23,7 +24,8 @@ public class StateRefinementError extends LJError {
2324
private final SourcePosition declarationPosition;
2425

2526
public StateRefinementError(SourcePosition position, SourcePosition declarationPosition, Predicate expected,
26-
VCSimplificationResult found, TranslationTable translationTable, String customMessage, List<Hint> hints) {
27+
VCSimplificationResult found, TranslationTable translationTable, String customMessage, List<Hint> hints,
28+
ErrorCategory category) {
2729
super("State Refinement Error",
2830
String.format("found %s but expected %s",
2931
found.getImplication().toPredicate().getExpression().toDisplayString(),
@@ -33,6 +35,7 @@ public StateRefinementError(SourcePosition position, SourcePosition declarationP
3335
this.expected = expected;
3436
this.found = found;
3537
hints.forEach(this::addHint);
38+
setCategory(category);
3639
if (hints.isEmpty() && isTrue(found.getImplication().toPredicate().getExpression()))
3740
addHint(Hint.general(
3841
"No state information is known for this object. Make sure its state is initialized and available at this point."));

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import liquidjava.diagnostics.Diagnostics;
1111
import liquidjava.diagnostics.errors.*;
12+
import liquidjava.diagnostics.categories.ErrorCategory;
1213
import liquidjava.diagnostics.hints.Hint;
1314
import liquidjava.diagnostics.warnings.UnsatisfiableRefinementWarning;
1415
import liquidjava.processor.context.AliasWrapper;
@@ -425,12 +426,13 @@ public void throwRefinementError(SourcePosition position, SourcePosition declara
425426

426427
public void throwStateRefinementError(SourcePosition position, SourcePosition declarationPosition, Predicate found,
427428
Predicate expected, String customMessage) throws LJError {
428-
throwStateRefinementError(position, declarationPosition, found, expected, customMessage, List.of());
429+
throwStateRefinementError(position, declarationPosition, found, expected, customMessage, List.of(), null);
429430
}
430431

431432
public void throwStateRefinementError(SourcePosition position, SourcePosition declarationPosition, Predicate found,
432-
Predicate expected, String customMessage, List<Hint> hints) throws LJError {
433-
vcChecker.throwStateRefinementError(position, declarationPosition, found, expected, customMessage, hints);
433+
Predicate expected, String customMessage, List<Hint> hints, ErrorCategory category) throws LJError {
434+
vcChecker.throwStateRefinementError(position, declarationPosition, found, expected, customMessage, hints,
435+
category);
434436
}
435437

436438
public void throwStateConflictError(SourcePosition position, Predicate expectedType) throws LJError {

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import liquidjava.diagnostics.errors.*;
1313
import liquidjava.diagnostics.DebugLog;
1414
import liquidjava.diagnostics.TranslationTable;
15+
import liquidjava.diagnostics.categories.ErrorCategory;
1516
import liquidjava.diagnostics.hints.Hint;
1617
import liquidjava.processor.VCImplication;
1718
import liquidjava.processor.context.*;
@@ -437,11 +438,12 @@ protected void throwRefinementError(SourcePosition position, SourcePosition decl
437438
}
438439

439440
protected void throwStateRefinementError(SourcePosition position, SourcePosition declarationPosition,
440-
Predicate found, Predicate expected, String customMessage, List<Hint> hints) throws StateRefinementError {
441+
Predicate found, Predicate expected, String customMessage, List<Hint> hints, ErrorCategory category)
442+
throws StateRefinementError {
441443
TranslationTable map = new TranslationTable();
442444
VCImplication foundState = buildPremiseChain(map, expected, found);
443445
throw new StateRefinementError(position, declarationPosition, expected, foundState.simplify(), map,
444-
customMessage, hints);
446+
customMessage, hints, category);
445447
}
446448

447449
protected void throwStateConflictError(SourcePosition position, Predicate expected) throws StateConflictError {

‎liquidjava-verifier/src/main/java/liquidjava/processor/refinement_checker/object_checkers/AuxStateHandler.java‎

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import java.util.stream.Collectors;
66

77
import liquidjava.diagnostics.DebugLog;
8+
import liquidjava.diagnostics.categories.ErrorCategory;
89
import liquidjava.diagnostics.errors.IllegalConstructorTransitionError;
910
import liquidjava.diagnostics.errors.InvalidRefinementError;
1011
import liquidjava.diagnostics.errors.LJError;
@@ -15,6 +16,7 @@
1516
import liquidjava.processor.refinement_checker.TypeChecker;
1617
import liquidjava.processor.refinement_checker.TypeCheckingUtils;
1718
import liquidjava.processor.state_machine.State;
19+
import liquidjava.processor.state_machine.StateErrorCategories;
1820
import liquidjava.processor.state_machine.StateMachine;
1921
import liquidjava.processor.state_machine.StateMachineBuilder;
2022
import liquidjava.processor.state_machine.StateSolver;
@@ -643,35 +645,47 @@ private static void changeState(TypeChecker tc, VariableInstance vi, RefinedFunc
643645
SourcePosition declarationPosition = stateChanges.stream().filter(ObjectState::hasFrom)
644646
.map(ObjectState::getFromPosition).filter(Objects::nonNull).findFirst()
645647
.orElse(function.getPlacementInCode().getPosition());
646-
List<Hint> hints = stateHints(tc, function, instanceName, prevState, invocation);
648+
Optional<ReceiverStates> states = receiverStates(tc, function, instanceName, prevState, invocation);
649+
List<Hint> hints = states.map(r -> stateHints(r, function, invocation)).orElse(List.of());
650+
ErrorCategory category = states.flatMap(r -> StateErrorCategories.categorize(r.machine(), r.current(),
651+
prevState, getGhostStatesFor(function.getTargetClass(), tc))).orElse(null);
647652
tc.throwStateRefinementError(invocation.getPosition(), declarationPosition, prevState,
648-
expectedStatesDisjunction, message, hints);
653+
expectedStatesDisjunction, message, hints, category);
649654
}
650655
}
651656

657+
/** The state machine of the receiver's class and the states the receiver can be in before a call */
658+
private record ReceiverStates(StateMachine machine, Set<State> current) {
659+
}
660+
652661
/**
653-
* Hints on how to fix a call to {@code function} made in a state that does not allow it. They are best effort: if
654-
* they cannot be computed, the error is reported without them
662+
* The receiver's states before a call to {@code function} that its state does not allow. Best effort: if they
663+
* cannot be computed, the error is reported without hints or category
655664
*/
656-
private static List<Hint> stateHints(TypeChecker tc, RefinedFunction function, String instanceName, Predicate state,
657-
CtElement invocation) {
658-
Optional<String> receiver = receiverOf(invocation);
659-
if (receiver.isEmpty() || !StateMachineBuilder.canModel(function))
660-
return List.of();
665+
private static Optional<ReceiverStates> receiverStates(TypeChecker tc, RefinedFunction function,
666+
String instanceName, Predicate state, CtElement invocation) {
661667
try {
662668
Optional<StateMachine> machine = tc.getStateMachine(function.getTargetClass(), invocation);
663669
if (machine.isEmpty())
664-
return List.of();
670+
return Optional.empty();
665671
Set<State> current = new StateSolver(tc, invocation.getPosition()).possibleStates(machine.get().states(),
666672
state, instanceName);
667-
return StateHints.forCall(machine.get(), function, current, receiver.get());
673+
return Optional.of(new ReceiverStates(machine.get(), current));
668674
} catch (RuntimeException e) {
669-
// a failure to compute hints must not replace the error being reported
670-
DebugLog.warn("Could not compute state hints: " + e.getMessage());
671-
return List.of();
675+
// a failure to analyse the states must not replace the error being reported
676+
DebugLog.warn("Could not compute the receiver's states: " + e.getMessage());
677+
return Optional.empty();
672678
}
673679
}
674680

681+
/** Hints on how to fix a call to {@code function} made in a state that does not allow it */
682+
private static List<Hint> stateHints(ReceiverStates states, RefinedFunction function, CtElement invocation) {
683+
Optional<String> receiver = receiverOf(invocation);
684+
if (receiver.isEmpty() || !StateMachineBuilder.canModel(function))
685+
return List.of();
686+
return StateHints.forCall(states.machine(), function, states.current(), receiver.get());
687+
}
688+
675689
/**
676690
* The receiver of {@code invocation} as written in the code, if it names an object that calls can be added to: a
677691
* variable, a field or {@code this}

0 commit comments

Comments
 (0)