diff --git a/src/org/sosy_lab/java_smt/api/StringFormulaManager.java b/src/org/sosy_lab/java_smt/api/StringFormulaManager.java index 8766c874af..42c4450ab0 100644 --- a/src/org/sosy_lab/java_smt/api/StringFormulaManager.java +++ b/src/org/sosy_lab/java_smt/api/StringFormulaManager.java @@ -8,7 +8,6 @@ package org.sosy_lab.java_smt.api; -import com.google.common.base.Preconditions; import java.util.Arrays; import java.util.List; import org.sosy_lab.java_smt.api.NumeralFormula.IntegerFormula; @@ -155,13 +154,6 @@ default StringFormula concat(StringFormula... parts) { * @see #range(StringFormula, StringFormula) */ default RegexFormula range(char start, char end) { - Preconditions.checkArgument( - start <= end, - "Range from start '%s' (%s) to end '%s' (%s) is empty.", - start, - (int) start, - end, - (int) end); return range(makeString(String.valueOf(start)), makeString(String.valueOf(end))); } diff --git a/src/org/sosy_lab/java_smt/solvers/cvc4/CVC4StringFormulaManager.java b/src/org/sosy_lab/java_smt/solvers/cvc4/CVC4StringFormulaManager.java index 0e167b418a..d61227aa5d 100644 --- a/src/org/sosy_lab/java_smt/solvers/cvc4/CVC4StringFormulaManager.java +++ b/src/org/sosy_lab/java_smt/solvers/cvc4/CVC4StringFormulaManager.java @@ -143,9 +143,33 @@ protected Expr allCharImpl() { return exprManager.mkExpr(Kind.REGEXP_SIGMA, new vectorExpr()); } + /** + * Check if the String only contains printable US ASCII characters. + * + * <p>We use this function to check if the String contains any characters that would have to be + * escaped in SMTLIB. + */ + private boolean isAsciiString(String str) { + return str.codePoints().allMatch(c -> c >= 0x20 && c <= 0x7E); + } + @Override protected Expr range(Expr start, Expr end) { - return exprManager.mkExpr(Kind.REGEXP_RANGE, start, end); + Preconditions.checkArgument( + start.isConst() && end.isConst(), "CVC4 does not support variables as bounds"); + + String lower = (String) formulaCreator.convertValue(start, start); + String upper = (String) formulaCreator.convertValue(end, end); + + Preconditions.checkArgument( + isAsciiString(lower) && isAsciiString(upper), + "CVC4 only allows printable US ASCII characters as bounds"); + + // Return the empty language if the bounds are not single character Strings, or if the upper + // bound is smaller than the lower bound + return lower.length() != 1 || upper.length() != 1 || upper.compareTo(lower) < 0 + ? noneImpl() + : exprManager.mkExpr(Kind.REGEXP_RANGE, start, end); } @Override diff --git a/src/org/sosy_lab/java_smt/solvers/cvc5/CVC5StringFormulaManager.java b/src/org/sosy_lab/java_smt/solvers/cvc5/CVC5StringFormulaManager.java index 500aea06d2..db123b0a57 100644 --- a/src/org/sosy_lab/java_smt/solvers/cvc5/CVC5StringFormulaManager.java +++ b/src/org/sosy_lab/java_smt/solvers/cvc5/CVC5StringFormulaManager.java @@ -140,7 +140,16 @@ protected Term allCharImpl() { @Override protected Term range(Term start, Term end) { - return solver.mkTerm(Kind.REGEXP_RANGE, start, end); + // Precondition: Both bounds must be single character Strings + // CVC5 already checks that the lower bound is smaller than the upper bound and returns the + // empty language otherwise. + Term one = solver.mkInteger(1); + Term cond = + solver.mkTerm( + Kind.AND, + solver.mkTerm(Kind.EQUAL, length(start), one), + solver.mkTerm(Kind.EQUAL, length(end), one)); + return solver.mkTerm(Kind.ITE, cond, solver.mkTerm(Kind.REGEXP_RANGE, start, end), noneImpl()); } @Override diff --git a/src/org/sosy_lab/java_smt/solvers/princess/PrincessEnvironment.java b/src/org/sosy_lab/java_smt/solvers/princess/PrincessEnvironment.java index 29f2032780..61a42339c3 100644 --- a/src/org/sosy_lab/java_smt/solvers/princess/PrincessEnvironment.java +++ b/src/org/sosy_lab/java_smt/solvers/princess/PrincessEnvironment.java @@ -605,6 +605,9 @@ static FormulaType<?> getFormulaType(IExpression pFormula) { * @throws IllegalArgumentException for any other type. */ private static FormulaType<?> mergeFormulaTypes(FormulaType<?> type1, FormulaType<?> type2) { + if (type1.equals(type2)) { + return type1; + } if ((type1.isIntegerType() || type1.isRationalType()) && (type2.isIntegerType() || type2.isRationalType())) { return type1.isRationalType() ? type1 : type2; diff --git a/src/org/sosy_lab/java_smt/solvers/princess/PrincessStringFormulaManager.java b/src/org/sosy_lab/java_smt/solvers/princess/PrincessStringFormulaManager.java index c919679b08..68535404e1 100644 --- a/src/org/sosy_lab/java_smt/solvers/princess/PrincessStringFormulaManager.java +++ b/src/org/sosy_lab/java_smt/solvers/princess/PrincessStringFormulaManager.java @@ -11,11 +11,16 @@ import static com.google.common.base.Preconditions.checkArgument; import static org.sosy_lab.java_smt.solvers.princess.PrincessEnvironment.toITermSeq; +import ap.basetypes.IdealInt; import ap.parser.IAtom; +import ap.parser.IBinFormula; +import ap.parser.IBinJunctor; import ap.parser.IExpression; import ap.parser.IFormula; import ap.parser.IFunApp; +import ap.parser.IIntLit; import ap.parser.ITerm; +import ap.parser.ITermITE; import ap.types.Sort; import com.google.common.collect.ImmutableList; import java.util.List; @@ -166,7 +171,17 @@ protected IExpression allCharImpl() { @Override protected ITerm range(IExpression start, IExpression end) { - return new IFunApp(PrincessEnvironment.stringTheory.re_range(), toITermSeq()); + // Precondition: Both bounds must be single character Strings + // Princess already checks that the lower bound is smaller than the upper bound and returns the + // empty language otherwise. + ITerm one = new IIntLit(IdealInt.apply(1)); + IFormula cond = + new IBinFormula( + IBinJunctor.And(), length(start).$eq$eq$eq(one), length(end).$eq$eq$eq(one)); + return new ITermITE( + cond, + new IFunApp(PrincessEnvironment.stringTheory.re_range(), toITermSeq(start, end)), + noneImpl()); } @Override diff --git a/src/org/sosy_lab/java_smt/test/StringFormulaManagerTest.java b/src/org/sosy_lab/java_smt/test/StringFormulaManagerTest.java index ca7cfc7985..4c437964f3 100644 --- a/src/org/sosy_lab/java_smt/test/StringFormulaManagerTest.java +++ b/src/org/sosy_lab/java_smt/test/StringFormulaManagerTest.java @@ -260,6 +260,75 @@ public void testStringConcatEmpty() throws SolverException, InterruptedException assertEqual(empty, smgr.concat(ImmutableList.of(empty, empty, empty, empty))); } + @Test + public void testStringRange() throws SolverException, InterruptedException { + StringFormula var = smgr.makeVariable("str"); + + // Try something simple: + // StringFormulaManager.range("a","b") should not be empty + assertThatFormula(smgr.in(var, smgr.range(smgr.makeString("a"), smgr.makeString("b")))) + .isSatisfiable(); + assertThatFormula(smgr.in(var, smgr.range('a', 'b'))).isSatisfiable(); + + // Check again with a single element interval: + // StringFormulaManager.range("a","a") should not be empty + assertThatFormula(smgr.in(var, smgr.range(smgr.makeString("a"), smgr.makeString("a")))) + .isSatisfiable(); + assertThatFormula(smgr.in(var, smgr.range('a', 'a'))).isSatisfiable(); + + // Check again for Unicode characters: + // StringFormulaManager.range("ꯍ","ꯎ") should not be empty + if (solver != Solvers.CVC4) { + // FIXME CVC4 only support ASCII characters for range() + assertThatFormula(smgr.in(var, smgr.range(smgr.makeString("ꯍ"), smgr.makeString("ꯎ")))) + .isSatisfiable(); + assertThatFormula(smgr.in(var, smgr.range('ꯍ', 'ꯎ'))).isSatisfiable(); + } + + // And once more with Unicode characters that are not in the BMP: + // StringFormulaManager.range("𠃋","𠃋") should not be empty + if (solver != Solvers.PRINCESS && solver != Solvers.CVC4) { + // FIXME CVC4 only support ASCII characters for range() + // FIXME Princess can't handle surrogate pairs + assertThatFormula(smgr.in(var, smgr.range(smgr.makeString("𠃋"), smgr.makeString("𠃋")))) + .isSatisfiable(); + } + + // Check some corner cases: + // StringFormulaManager.range("b", "a") should be empty + assertThatFormula(smgr.in(var, smgr.range(smgr.makeString("b"), smgr.makeString("a")))) + .isUnsatisfiable(); + assertThatFormula(smgr.in(var, smgr.range('b', 'a'))).isUnsatisfiable(); + + // Only 'singleton' Strings (= Strings with one character) are allowed: + // StringFormulaManager.range("", "a") should be empty + assertThatFormula(smgr.in(var, smgr.range(smgr.makeString(""), smgr.makeString("a")))) + .isUnsatisfiable(); + + // Try again with two characters: + // StringFormulaManager.range("aa", "ab") should be empty + assertThatFormula(smgr.in(var, smgr.range(smgr.makeString("aa"), smgr.makeString("ab")))) + .isUnsatisfiable(); + + // Now use variables for the bounds: + // StringFormulaManager.range(lower, "b") should be empty iff "b" < lower + StringFormula lower = smgr.makeVariable("lower"); + if (solver != Solvers.PRINCESS && solver != Solvers.CVC4) { + // FIXME CVC4 only supports String constants as bounds and will fail for variables + // FIXME Princess will crash when using variables as bounds + assertThatFormula( + bmgr.and( + smgr.equal(lower, smgr.makeString("a")), + smgr.in(var, smgr.range(lower, smgr.makeString("b"))))) + .isSatisfiable(); + assertThatFormula( + bmgr.and( + smgr.equal(lower, smgr.makeString("c")), + smgr.in(var, smgr.range(lower, smgr.makeString("b"))))) + .isUnsatisfiable(); + } + } + @Test public void testStringPrefixSuffixConcat() throws SolverException, InterruptedException { // FIXME: Princess will timeout on this test