Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
8 changes: 8 additions & 0 deletions liquidjava-example/src/main/java/testSuite/CorrectIfThen.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ public void have1(int a) {
int u = pos;
}

public void haveAnd(int a, @Refinement("b > 5")int b) {
@Refinement("pos > 0")
int pos = 10;
if (a > 0 && b > 0) {
if (a > b) pos = a - b;
}
}

public static void main(String[] args) {
@Refinement("_ < 10")
int a = 5;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package testSuite.classes.ts_bufferedreader_correct;
import java.io.IOException;
import java.io.Reader;
import java.util.stream.Stream;

import liquidjava.specification.ExternalRefinementsFor;
import liquidjava.specification.Refinement;
import liquidjava.specification.RefinementAlias;
import liquidjava.specification.StateRefinement;
import liquidjava.specification.StateSet;

@RefinementAlias("NonNegative(int v) { v >= 0 }")
@RefinementAlias("Positive(int v) { v > 0 }")
@StateSet({"open", "marked", "closed"})
@ExternalRefinementsFor("java.io.BufferedReader")
public interface BufferedReaderRefinementsExpert {

@StateRefinement(to="open(this)")
public void BufferedReader(Reader in);

@StateRefinement(to="open(this)")
public void BufferedReader(Reader in, @Refinement("Positive(_)") int sz);

@StateRefinement(from="open(this)")
@StateRefinement(from="marked(this)")
public int read();

@StateRefinement(from="open(this)")
@StateRefinement(from="marked(this)")
@Refinement(" _ >= -1")
public int read(char[] cbuf, @Refinement("NonNegative(_)") int off, @Refinement("NonNegative(_)") int len);

@StateRefinement(from="open(this)")
@StateRefinement(from="marked(this)")
public String readLine() throws IOException;

@StateRefinement(from="open(this)")
@StateRefinement(from="marked(this)")
public boolean ready() throws IOException;

@StateRefinement(from="open(this)")
@StateRefinement(from="marked(this)")
public boolean markSupported();

@StateRefinement(from="open(this)", to="marked(this)")
@StateRefinement(from="marked(this)")
public void mark(@Refinement("NonNegative(_)") int readAheadLimit);

@StateRefinement(from="marked(this)", to="open(this)")
public void reset();

@StateRefinement(from="!closed(this)", to="closed(this)")
public void close();

@StateRefinement(from="open(this)")
@StateRefinement(from="marked(this)")
public Stream<String> lines();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package testSuite.classes.ts_bufferedreader_correct;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

// Regression test for unrefined conditions: each branch reads (open/marked) before
// closing. The verifier must not assume the if-condition is unconditionally true,
// otherwise it would conclude the reader is always closed by line 24 and flag the
// readLine() call on line 31 as a state-refinement error.

class ConfigLoader {

String loadFirstSetting(String configPath, String defaultValue) throws IOException {
BufferedReader reader = new BufferedReader(new FileReader(configPath));

String header = reader.readLine();
if (header.isEmpty()) {
reader.close();
return defaultValue;
}

if (header.startsWith("#")) {
String value = reader.readLine();
reader.close();
return value;
}

reader.close();
return header;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package testSuite.classes.ts_bufferedreader_error;
import java.io.IOException;
import java.io.Reader;
import java.util.stream.Stream;

import liquidjava.specification.ExternalRefinementsFor;
import liquidjava.specification.Refinement;
import liquidjava.specification.RefinementAlias;
import liquidjava.specification.StateRefinement;
import liquidjava.specification.StateSet;

@RefinementAlias("NonNegative(int v) { v >= 0 }")
@RefinementAlias("Positive(int v) { v > 0 }")
@StateSet({"open", "marked", "closed"})
@ExternalRefinementsFor("java.io.BufferedReader")
public interface BufferedReaderRefinementsExpert {

@StateRefinement(to="open(this)")
public void BufferedReader(Reader in);

@StateRefinement(to="open(this)")
public void BufferedReader(Reader in, @Refinement("Positive(_)") int sz);

@StateRefinement(from="open(this)")
@StateRefinement(from="marked(this)")
public int read();

@StateRefinement(from="open(this)")
@StateRefinement(from="marked(this)")
@Refinement(" _ >= -1")
public int read(char[] cbuf, @Refinement("NonNegative(_)") int off, @Refinement("NonNegative(_)") int len);

@StateRefinement(from="open(this)")
@StateRefinement(from="marked(this)")
public String readLine() throws IOException;

@StateRefinement(from="open(this)")
@StateRefinement(from="marked(this)")
public boolean ready() throws IOException;

@StateRefinement(from="open(this)")
@StateRefinement(from="marked(this)")
public boolean markSupported();

@StateRefinement(from="open(this)", to="marked(this)")
@StateRefinement(from="marked(this)")
public void mark(@Refinement("NonNegative(_)") int readAheadLimit);

@StateRefinement(from="marked(this)", to="open(this)")
public void reset();

@StateRefinement(from="!closed(this)", to="closed(this)")
public void close();

@StateRefinement(from="open(this)")
@StateRefinement(from="marked(this)")
public Stream<String> lines();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package testSuite.classes.ts_bufferedreader_error;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

// (A) The model is co-located: BufferedReaderRefinementsExpert (de-packaged
// copy) declares states open/marked/closed on java.io.BufferedReader and
// requires open or marked for read methods.
//
// (B) The client: load the first non-comment setting from a config file.
// The author handles three branches — empty file, leading-comment header,
// plain header — and tries to close the reader as soon as it is no longer
// needed. In the leading-comment branch the close happens too early; the
// follow-up readLine runs against a closed reader.

class ConfigLoader {

String loadFirstSetting(String configPath, String defaultValue) throws IOException {
BufferedReader reader = new BufferedReader(new FileReader(configPath));

String header = reader.readLine();
if (header.isEmpty()) {
reader.close();
return defaultValue;
}

if (header.startsWith("#")) {
// Header was a comment — the real value is on the next line.
reader.close();
return reader.readLine(); // State Refinement Error
}

reader.close();
return header;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ void saveInstanceElse() {
Optional<VariableInstance> getIfInstanceCombination(int counter, Predicate cond) {
if (ifCombiner.isEmpty() || (!has(ifthenIndex) && !has(ifelseIndex) && !has(ifbeforeIndex)))
return Optional.empty();
if (has(ifbeforeIndex) && !has(ifthenIndex) && !has(ifelseIndex))
return Optional.empty();

String nName = String.format("#%s_%d", super.getName(), counter);
Predicate ref = new Predicate();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@
import spoon.reflect.code.CtArrayWrite;
import spoon.reflect.code.CtAssignment;
import spoon.reflect.code.CtBinaryOperator;
import spoon.reflect.code.CtBlock;
import spoon.reflect.code.CtBreak;
import spoon.reflect.code.CtConditional;
import spoon.reflect.code.CtContinue;
import spoon.reflect.code.CtConstructorCall;
import spoon.reflect.code.CtExpression;
import spoon.reflect.code.CtFieldRead;
Expand All @@ -36,7 +39,9 @@
import spoon.reflect.code.CtNewArray;
import spoon.reflect.code.CtNewClass;
import spoon.reflect.code.CtReturn;
import spoon.reflect.code.CtStatement;
import spoon.reflect.code.CtThisAccess;
import spoon.reflect.code.CtThrow;
import spoon.reflect.code.CtUnaryOperator;
import spoon.reflect.code.CtVariableAccess;
import spoon.reflect.code.CtVariableRead;
Expand Down Expand Up @@ -336,18 +341,26 @@ public void visitCtIf(CtIf ifElement) {
CtExpression<Boolean> exp = ifElement.getCondition();
Predicate expRefs = getExpressionRefinements(exp);

String freshVarName = String.format(Formats.FRESH, context.getCounter());
expRefs = expRefs.substituteVariable(Keys.WILDCARD, freshVarName);
Predicate lastExpRefs = substituteAllVariablesForLastInstance(expRefs);
expRefs = Predicate.createConjunction(expRefs, lastExpRefs);
String pathVarName = String.format(Formats.FRESH, context.getCounter());
RefinedVariable freshRV;
if (isUninformativeCondition(expRefs, exp)) {
// No refinement means the condition is unknown, not true: model it as a fresh
// boolean so the SMT solver may pick either truth value for each branch.
expRefs = Predicate.createVar(pathVarName);
freshRV = context.addInstanceToContext(pathVarName, factory.Type().BOOLEAN_PRIMITIVE, new Predicate(), exp);
} else {
expRefs = expRefs.substituteVariable(Keys.WILDCARD, pathVarName);
Predicate lastExpRefs = substituteAllVariablesForLastInstance(expRefs);
expRefs = Predicate.createConjunction(expRefs, lastExpRefs);

freshRV = context.addInstanceToContext(pathVarName, factory.Type().INTEGER_PRIMITIVE, expRefs, exp);
}

// TODO Change in future
if (expRefs.getVariableNames().contains("null")) {
expRefs = new Predicate();
}

RefinedVariable freshRV = context.addInstanceToContext(freshVarName, factory.Type().INTEGER_PRIMITIVE, expRefs,
exp);
vcChecker.addPathVariable(freshRV);

context.variablesNewIfCombination();
Expand All @@ -357,19 +370,21 @@ public void visitCtIf(CtIf ifElement) {
// VISIT THEN
context.enterContext();
visitCtBlock(ifElement.getThenStatement());
context.variablesSetThenIf();
if (canCompleteNormally(ifElement.getThenStatement())) {
context.variablesSetThenIf();
}
contextHistory.saveContext(ifElement.getThenStatement(), context);
context.exitContext();

// VISIT ELSE
if (ifElement.getElseStatement() != null) {
context.getVariableByName(freshVarName);
// expRefs = expRefs.negate();
context.newRefinementToVariableInContext(freshVarName, expRefs.negate());
context.newRefinementToVariableInContext(pathVarName, expRefs.negate());

context.enterContext();
visitCtBlock(ifElement.getElseStatement());
context.variablesSetElseIf();
if (canCompleteNormally(ifElement.getElseStatement())) {
context.variablesSetElseIf();
}
contextHistory.saveContext(ifElement.getElseStatement(), context);
context.exitContext();
}
Expand All @@ -380,6 +395,47 @@ public void visitCtIf(CtIf ifElement) {
context.variablesFinishIfCombination();
}

/**
* A condition is uninformative when its refinement is the trivial {@code true} predicate yet the expression itself
* is not a boolean literal — i.e. the verifier has no symbolic information to relate the branch to. Treating such a
* condition as {@code true} would force every if-then to be taken, producing spurious state-refinement errors.
*/
private boolean isUninformativeCondition(Predicate conditionRefinement, CtExpression<Boolean> condition) {
if (!conditionRefinement.isBooleanTrue())
return false;
return !(condition instanceof CtLiteral<?> literal && literal.getValue() instanceof Boolean);
}

/**
* Best-effort normal-completion check (JLS §14.21): branches that always {@code return}, {@code throw},
* {@code break} or {@code continue} cannot contribute state to code following the {@code if}, so their post-context
* must be discarded at the join.
*
* <p>
* Not currently handled (treated conservatively as completing normally): {@code switch} where every case exits,
* labeled {@code break}/{@code continue} targets, {@code try}/{@code catch}/{@code finally} flow, and infinite
* loops such as {@code while (true)}. Extending this list only tightens precision.
*/
private boolean canCompleteNormally(CtStatement statement) {
if (statement == null)
return true;
if (statement instanceof CtReturn<?> || statement instanceof CtThrow || statement instanceof CtBreak
|| statement instanceof CtContinue)
return false;
if (statement instanceof CtBlock<?> block) {
List<CtStatement> statements = block.getStatements();
return statements.isEmpty() || canCompleteNormally(statements.get(statements.size() - 1));
}
if (statement instanceof CtIf nestedIf) {
CtStatement elseStatement = nestedIf.getElseStatement();
// No else means the false path always falls through.
if (elseStatement == null)
return true;
return canCompleteNormally(nestedIf.getThenStatement()) || canCompleteNormally(elseStatement);
}
return true;
}

@Override
public <T> void visitCtArrayWrite(CtArrayWrite<T> arrayWrite) {
super.visitCtArrayWrite(arrayWrite);
Expand Down
Loading