Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
package liquidjava.diagnostics.errors;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import liquidjava.diagnostics.TranslationTable;
import liquidjava.rj_language.Predicate;
import liquidjava.rj_language.ast.Expression;
import liquidjava.rj_language.ast.LiteralString;
import liquidjava.rj_language.ast.UnaryExpression;
import liquidjava.rj_language.ast.Var;
import liquidjava.rj_language.ast.formatter.VariableFormatter;
import liquidjava.rj_language.opt.VCSimplificationResult;
import liquidjava.rj_language.parsing.RefinementsParser;
import liquidjava.smt.Counterexample;
import liquidjava.utils.Pair;
import spoon.reflect.cu.SourcePosition;
Expand All @@ -23,27 +28,49 @@
public class RefinementError extends LJError {

private final Predicate expected;
private final Predicate finalExpected;
private final Predicate expectedWithWitness;
private final VCSimplificationResult found;
private final Counterexample counterexample;
private final SourcePosition declarationPosition;

public RefinementError(SourcePosition position, SourcePosition declarationPosition, Predicate expected,
VCSimplificationResult found, TranslationTable translationTable, Counterexample counterexample,
String customMessage) {
this(position, declarationPosition, expected, null, found, translationTable, counterexample, customMessage);
}

public RefinementError(SourcePosition position, SourcePosition declarationPosition, Predicate expected,
Predicate finalExpected, VCSimplificationResult found, TranslationTable translationTable,
Counterexample counterexample, String customMessage) {
super("Refinement Error",
String.format("%s is not a subtype of %s",
found.getImplication().toPredicate().getExpression().toDisplayString(),
expected.getExpression().toDisplayString()),
position, translationTable, customMessage);
this.expected = expected;
this.finalExpected = finalExpected;
this.found = found;
this.counterexample = filterCounterexample(counterexample);
this.expectedWithWitness = substituteWitness(finalExpected, this.counterexample);
this.declarationPosition = declarationPosition;
if (!this.counterexample.isEmpty()) {
String counterexampleString = this.counterexample.assignments().stream()
.map(a -> VariableFormatter.format(a.first()) + " == " + a.second())
.collect(Collectors.joining(" && "));
setCounterexampleStr("Counterexample: " + counterexampleString);
StringBuilder detail = new StringBuilder();
if (finalExpected != null) {
detail.append("Final expected: ").append(finalExpected.getExpression().toDisplayString()).append("\n");
}
detail.append("Counterexample: ").append(counterexampleString);
if (expectedWithWitness != null) {
detail.append("\nWith witness: ").append(expectedWithWitness.getExpression().toDisplayString());
List<String> remainingVariables = new ArrayList<>();
expectedWithWitness.getExpression().getVariableNames(remainingVariables);
if (remainingVariables.isEmpty())
detail.append(" ✗");
}
setCounterexampleStr(detail.toString());
}
if (isTrue(found.getImplication().toPredicate().getExpression()))
setHint("Not enough information to prove the expected refinement. Add a refinement or condition to constrain it.");
Expand All @@ -62,10 +89,48 @@ public Predicate getExpected() {
return expected;
}

public Predicate getFinalExpected() {
return finalExpected;
}

public Predicate getExpectedWithWitness() {
return expectedWithWitness;
}

public VCSimplificationResult getFound() {
return found;
}

private static Predicate substituteWitness(Predicate finalExpected, Counterexample counterexample) {
if (finalExpected == null || counterexample.isEmpty())
return null;
Expression expression = finalExpected.getExpression().clone();
boolean substituted = false;
for (Pair<String, String> assignment : counterexample.assignments()) {
List<String> variableNames = new ArrayList<>();
expression.getVariableNames(variableNames);
if (!variableNames.contains(assignment.first()))
continue;
try {
Expression value = RefinementsParser.createAST(assignment.second(), "");
if (!isLiteralValue(value))
continue;
expression = expression.substitute(new Var(assignment.first()), value);
substituted = true;
} catch (SyntaxError ignored) {
// Some SMT values cannot be represented in the refinement language.
}
Comment on lines +114 to +122

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Edge Case: Witness parsing only catches SyntaxError; other parse failures escape

substituteWitness now runs raw Z3 model values through RefinementsParser.createAST, but it only catches SyntaxError. The parser can throw other unchecked exceptions. For example, literalCreate calls Long.parseLong on any INT token, and a Z3 Int model value outside the long range (Z3 integers are unbounded) throws NumberFormatException. NotImplementedException and other LJError subclasses can also escape. Any of these would escape the RefinementError constructor, so the verifier would crash with a runtime exception instead of reporting the refinement error. The witness is best-effort decoration, so a failure to parse one value should skip it the same way a syntax error does.

Treat any parse failure of an SMT value as non-representable:

try {
    Expression value = RefinementsParser.createAST(assignment.second(), "");
    if (!isLiteralValue(value))
        continue;
    expression = expression.substitute(new Var(assignment.first()), value);
    substituted = true;
} catch (RuntimeException ignored) {
    // Some SMT values cannot be represented in the refinement language.
}
  • Apply fix

Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎

}
return substituted ? new Predicate(expression) : null;
}

private static boolean isLiteralValue(Expression value) {
if (value.isLiteral() || value instanceof LiteralString)
return true;
return value instanceof UnaryExpression unary && ("-".equals(unary.getOp()) || "+".equals(unary.getOp()))
&& isLiteralValue(unary.getExpression());
}

// Filters counterexample assignments only in found VC and sorts them in the order of its binders
private Counterexample filterCounterexample(Counterexample counterexample) {
if (counterexample == null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ public void processSubtyping(Predicate expectedType, List<GhostState> list, CtEl
}
DebugLog.smtResult(result);
if (result.isError()) {
throw new RefinementError(element.getPosition(), declarationPosition, expectedType,
throw new RefinementError(element.getPosition(), declarationPosition, expectedType, expected,
implBeforeChange.simplify(), map, result.getCounterexample(), customMessage);
}
}
Expand All @@ -96,8 +96,8 @@ public void processSubtyping(Predicate type, Predicate expectedType, List<GhostS
SourcePosition declarationPosition, Factory f) throws LJError {
SMTResult result = verifySMTSubtypeStates(type, expectedType, list, element.getPosition(), f);
if (result.isError())
throwRefinementError(element.getPosition(), declarationPosition, expectedType, type,
result.getCounterexample(), null);
throwRefinementError(element.getPosition(), declarationPosition, expectedType, result.getFinalExpected(),
type, result.getCounterexample(), null);
}

/**
Expand Down Expand Up @@ -183,7 +183,7 @@ public SMTResult verifySMTSubtypeStates(Predicate type, Predicate expectedType,
if (!silent) {
DebugLog.smtResult(result);
}
return result;
return result.withFinalExpected(expected);
}

/**
Expand Down Expand Up @@ -404,10 +404,16 @@ private VCImplication buildPremiseChain(TranslationTable map, Predicate... predi

protected void throwRefinementError(SourcePosition position, SourcePosition declarationPosition, Predicate expected,
Predicate found, Counterexample counterexample, String customMessage) throws RefinementError {
throwRefinementError(position, declarationPosition, expected, null, found, counterexample, customMessage);
}

protected void throwRefinementError(SourcePosition position, SourcePosition declarationPosition, Predicate expected,
Predicate finalExpected, Predicate found, Counterexample counterexample, String customMessage)
throws RefinementError {
TranslationTable map = new TranslationTable();
VCImplication premises = buildPremiseChain(map, expected, found);
throw new RefinementError(position, declarationPosition, expected, premises.simplify(), map, counterexample,
customMessage);
throw new RefinementError(position, declarationPosition, expected, finalExpected, premises.simplify(), map,
counterexample, customMessage);
}

protected void throwStateRefinementError(SourcePosition position, SourcePosition declarationPosition,
Expand Down
18 changes: 15 additions & 3 deletions liquidjava-verifier/src/main/java/liquidjava/smt/SMTResult.java
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
package liquidjava.smt;

import liquidjava.rj_language.Predicate;

public class SMTResult {
private final Counterexample counterexample;
private final Predicate finalExpected;

private SMTResult(Counterexample counterexample) {
private SMTResult(Counterexample counterexample, Predicate finalExpected) {
this.counterexample = counterexample;
this.finalExpected = finalExpected;
}

public static SMTResult ok() {
return new SMTResult(null);
return new SMTResult(null, null);
}

public static SMTResult error(Counterexample counterexample) {
return new SMTResult(counterexample);
return new SMTResult(counterexample, null);
}

public SMTResult withFinalExpected(Predicate expected) {
return new SMTResult(counterexample, expected);
}

public boolean isOk() {
Expand All @@ -26,4 +34,8 @@ public boolean isError() {
public Counterexample getCounterexample() {
return counterexample;
}

public Predicate getFinalExpected() {
return finalExpected;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,17 @@ void integerDivisionIncludesInputAndGeneratedReturn() {
void dependentUpperBoundIncludesBoundaryValuesInBinderOrder() {
RefinementError error = verify("ErrorDependentUpperBound.java");
assertAssignments(error, assignment("len", "1"), assignment("i", "0"), assignment("#ret", "1"));
assertNotNull(error.getFinalExpected());
assertNotNull(error.getExpectedWithWitness());
assertTrue(error.getCounterexampleStr().contains("With witness:"));
}

@Test
void aliasFailureRetainsOriginalAndExpandedExpectedPredicates() {
RefinementError error = verify("ErrorAliasSimple.java");
assertTrue(error.getExpected().getExpression().toDisplayString().contains("PtGrade"));
assertNotNull(error.getFinalExpected());
assertFalse(error.getFinalExpected().getExpression().toDisplayString().contains("PtGrade"));
}

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package liquidjava.diagnostics.errors;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.List;

import org.junit.jupiter.api.Test;

import liquidjava.diagnostics.TranslationTable;
import liquidjava.processor.VCImplication;
import liquidjava.rj_language.Predicate;
import liquidjava.rj_language.opt.VCSimplificationResult;
import liquidjava.rj_language.parsing.RefinementsParser;
import liquidjava.smt.Counterexample;
import liquidjava.utils.Pair;
import spoon.Launcher;
import spoon.reflect.factory.Factory;

class RefinementWitnessTest {

private static final Factory FACTORY = new Launcher().getFactory();

@Test
void expandsTheFinalExpectedPredicateBeforeShowingTheWitness() {
RefinementError error = error("Positive(buffered)", "buffered > 0", List.of("buffered"),
new Pair<>("buffered", "0"));

assertEquals("Positive(buffered)", error.getExpected().getExpression().toDisplayString());
assertEquals("buffered > 0", error.getFinalExpected().getExpression().toDisplayString());
assertEquals("0 > 0", error.getExpectedWithWitness().getExpression().toDisplayString());
assertTrue(error.getCounterexampleStr().contains("With witness: 0 > 0 ✗"));
}

@Test
void substitutesEveryAvailableValueAndEveryOccurrence() {
RefinementError error = error("x < y && x != 0", "x < y && x != 0", List.of("x", "y"), new Pair<>("x", "0"),
new Pair<>("y", "1"));

assertEquals("0 < 1 && 0 != 0", error.getExpectedWithWitness().getExpression().toDisplayString());
}

@Test
void leavesValuesThatAreNotSafeLiteralsUnchanged() {
RefinementError error = error("x < y", "x < y", List.of("x", "y"), new Pair<>("x", "-1"),
new Pair<>("y", "other"));

assertNotNull(error.getExpectedWithWitness());
assertEquals("-1 < y", error.getExpectedWithWitness().getExpression().toDisplayString());
assertTrue(error.getCounterexampleStr().contains("With witness: -1 < y"));
}

@SafeVarargs
private static RefinementError error(String original, String finalExpression, List<String> binders,
Pair<String, String>... assignments) {
VCImplication first = null;
VCImplication last = null;
for (String binder : binders) {
VCImplication current = new VCImplication(binder, FACTORY.Type().INTEGER_PRIMITIVE, new Predicate());
if (last != null)
last.setNext(current);
if (first == null)
first = current;
last = current;
}
assertNotNull(first);
return new RefinementError(null, null, predicate(original), predicate(finalExpression),
new VCSimplificationResult(first), new TranslationTable(), new Counterexample(List.of(assignments)),
null);
}

private static Predicate predicate(String source) {
return new Predicate(RefinementsParser.createAST(source, ""));
}
}
Loading