Skip to content

Commit 3a6bce9

Browse files
Merge pull request #456 from sosy-lab/allow-empty-string-ranges
Allow empty String ranges
2 parents c98415e + 99cb995 commit 3a6bce9

File tree

6 files changed

+123
-11
lines changed

6 files changed

+123
-11
lines changed

src/org/sosy_lab/java_smt/api/StringFormulaManager.java

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88

99
package org.sosy_lab.java_smt.api;
1010

11-
import com.google.common.base.Preconditions;
1211
import java.util.Arrays;
1312
import java.util.List;
1413
import org.sosy_lab.java_smt.api.NumeralFormula.IntegerFormula;
@@ -155,13 +154,6 @@ default StringFormula concat(StringFormula... parts) {
155154
* @see #range(StringFormula, StringFormula)
156155
*/
157156
default RegexFormula range(char start, char end) {
158-
Preconditions.checkArgument(
159-
start <= end,
160-
"Range from start '%s' (%s) to end '%s' (%s) is empty.",
161-
start,
162-
(int) start,
163-
end,
164-
(int) end);
165157
return range(makeString(String.valueOf(start)), makeString(String.valueOf(end)));
166158
}
167159

src/org/sosy_lab/java_smt/solvers/cvc4/CVC4StringFormulaManager.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,9 +143,33 @@ protected Expr allCharImpl() {
143143
return exprManager.mkExpr(Kind.REGEXP_SIGMA, new vectorExpr());
144144
}
145145

146+
/**
147+
* Check if the String only contains printable US ASCII characters.
148+
*
149+
* <p>We use this function to check if the String contains any characters that would have to be
150+
* escaped in SMTLIB.
151+
*/
152+
private boolean isAsciiString(String str) {
153+
return str.codePoints().allMatch(c -> c >= 0x20 && c <= 0x7E);
154+
}
155+
146156
@Override
147157
protected Expr range(Expr start, Expr end) {
148-
return exprManager.mkExpr(Kind.REGEXP_RANGE, start, end);
158+
Preconditions.checkArgument(
159+
start.isConst() && end.isConst(), "CVC4 does not support variables as bounds");
160+
161+
String lower = (String) formulaCreator.convertValue(start, start);
162+
String upper = (String) formulaCreator.convertValue(end, end);
163+
164+
Preconditions.checkArgument(
165+
isAsciiString(lower) && isAsciiString(upper),
166+
"CVC4 only allows printable US ASCII characters as bounds");
167+
168+
// Return the empty language if the bounds are not single character Strings, or if the upper
169+
// bound is smaller than the lower bound
170+
return lower.length() != 1 || upper.length() != 1 || upper.compareTo(lower) < 0
171+
? noneImpl()
172+
: exprManager.mkExpr(Kind.REGEXP_RANGE, start, end);
149173
}
150174

151175
@Override

src/org/sosy_lab/java_smt/solvers/cvc5/CVC5StringFormulaManager.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,16 @@ protected Term allCharImpl() {
140140

141141
@Override
142142
protected Term range(Term start, Term end) {
143-
return solver.mkTerm(Kind.REGEXP_RANGE, start, end);
143+
// Precondition: Both bounds must be single character Strings
144+
// CVC5 already checks that the lower bound is smaller than the upper bound and returns the
145+
// empty language otherwise.
146+
Term one = solver.mkInteger(1);
147+
Term cond =
148+
solver.mkTerm(
149+
Kind.AND,
150+
solver.mkTerm(Kind.EQUAL, length(start), one),
151+
solver.mkTerm(Kind.EQUAL, length(end), one));
152+
return solver.mkTerm(Kind.ITE, cond, solver.mkTerm(Kind.REGEXP_RANGE, start, end), noneImpl());
144153
}
145154

146155
@Override

src/org/sosy_lab/java_smt/solvers/princess/PrincessEnvironment.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,6 +605,9 @@ static FormulaType<?> getFormulaType(IExpression pFormula) {
605605
* @throws IllegalArgumentException for any other type.
606606
*/
607607
private static FormulaType<?> mergeFormulaTypes(FormulaType<?> type1, FormulaType<?> type2) {
608+
if (type1.equals(type2)) {
609+
return type1;
610+
}
608611
if ((type1.isIntegerType() || type1.isRationalType())
609612
&& (type2.isIntegerType() || type2.isRationalType())) {
610613
return type1.isRationalType() ? type1 : type2;

src/org/sosy_lab/java_smt/solvers/princess/PrincessStringFormulaManager.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,16 @@
1111
import static com.google.common.base.Preconditions.checkArgument;
1212
import static org.sosy_lab.java_smt.solvers.princess.PrincessEnvironment.toITermSeq;
1313

14+
import ap.basetypes.IdealInt;
1415
import ap.parser.IAtom;
16+
import ap.parser.IBinFormula;
17+
import ap.parser.IBinJunctor;
1518
import ap.parser.IExpression;
1619
import ap.parser.IFormula;
1720
import ap.parser.IFunApp;
21+
import ap.parser.IIntLit;
1822
import ap.parser.ITerm;
23+
import ap.parser.ITermITE;
1924
import ap.types.Sort;
2025
import com.google.common.collect.ImmutableList;
2126
import java.util.List;
@@ -166,7 +171,17 @@ protected IExpression allCharImpl() {
166171

167172
@Override
168173
protected ITerm range(IExpression start, IExpression end) {
169-
return new IFunApp(PrincessEnvironment.stringTheory.re_range(), toITermSeq());
174+
// Precondition: Both bounds must be single character Strings
175+
// Princess already checks that the lower bound is smaller than the upper bound and returns the
176+
// empty language otherwise.
177+
ITerm one = new IIntLit(IdealInt.apply(1));
178+
IFormula cond =
179+
new IBinFormula(
180+
IBinJunctor.And(), length(start).$eq$eq$eq(one), length(end).$eq$eq$eq(one));
181+
return new ITermITE(
182+
cond,
183+
new IFunApp(PrincessEnvironment.stringTheory.re_range(), toITermSeq(start, end)),
184+
noneImpl());
170185
}
171186

172187
@Override

src/org/sosy_lab/java_smt/test/StringFormulaManagerTest.java

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,75 @@ public void testStringConcatEmpty() throws SolverException, InterruptedException
260260
assertEqual(empty, smgr.concat(ImmutableList.of(empty, empty, empty, empty)));
261261
}
262262

263+
@Test
264+
public void testStringRange() throws SolverException, InterruptedException {
265+
StringFormula var = smgr.makeVariable("str");
266+
267+
// Try something simple:
268+
// StringFormulaManager.range("a","b") should not be empty
269+
assertThatFormula(smgr.in(var, smgr.range(smgr.makeString("a"), smgr.makeString("b"))))
270+
.isSatisfiable();
271+
assertThatFormula(smgr.in(var, smgr.range('a', 'b'))).isSatisfiable();
272+
273+
// Check again with a single element interval:
274+
// StringFormulaManager.range("a","a") should not be empty
275+
assertThatFormula(smgr.in(var, smgr.range(smgr.makeString("a"), smgr.makeString("a"))))
276+
.isSatisfiable();
277+
assertThatFormula(smgr.in(var, smgr.range('a', 'a'))).isSatisfiable();
278+
279+
// Check again for Unicode characters:
280+
// StringFormulaManager.range("ꯍ","ꯎ") should not be empty
281+
if (solver != Solvers.CVC4) {
282+
// FIXME CVC4 only support ASCII characters for range()
283+
assertThatFormula(smgr.in(var, smgr.range(smgr.makeString("ꯍ"), smgr.makeString("ꯎ"))))
284+
.isSatisfiable();
285+
assertThatFormula(smgr.in(var, smgr.range('ꯍ', 'ꯎ'))).isSatisfiable();
286+
}
287+
288+
// And once more with Unicode characters that are not in the BMP:
289+
// StringFormulaManager.range("𠃋","𠃋") should not be empty
290+
if (solver != Solvers.PRINCESS && solver != Solvers.CVC4) {
291+
// FIXME CVC4 only support ASCII characters for range()
292+
// FIXME Princess can't handle surrogate pairs
293+
assertThatFormula(smgr.in(var, smgr.range(smgr.makeString("𠃋"), smgr.makeString("𠃋"))))
294+
.isSatisfiable();
295+
}
296+
297+
// Check some corner cases:
298+
// StringFormulaManager.range("b", "a") should be empty
299+
assertThatFormula(smgr.in(var, smgr.range(smgr.makeString("b"), smgr.makeString("a"))))
300+
.isUnsatisfiable();
301+
assertThatFormula(smgr.in(var, smgr.range('b', 'a'))).isUnsatisfiable();
302+
303+
// Only 'singleton' Strings (= Strings with one character) are allowed:
304+
// StringFormulaManager.range("", "a") should be empty
305+
assertThatFormula(smgr.in(var, smgr.range(smgr.makeString(""), smgr.makeString("a"))))
306+
.isUnsatisfiable();
307+
308+
// Try again with two characters:
309+
// StringFormulaManager.range("aa", "ab") should be empty
310+
assertThatFormula(smgr.in(var, smgr.range(smgr.makeString("aa"), smgr.makeString("ab"))))
311+
.isUnsatisfiable();
312+
313+
// Now use variables for the bounds:
314+
// StringFormulaManager.range(lower, "b") should be empty iff "b" < lower
315+
StringFormula lower = smgr.makeVariable("lower");
316+
if (solver != Solvers.PRINCESS && solver != Solvers.CVC4) {
317+
// FIXME CVC4 only supports String constants as bounds and will fail for variables
318+
// FIXME Princess will crash when using variables as bounds
319+
assertThatFormula(
320+
bmgr.and(
321+
smgr.equal(lower, smgr.makeString("a")),
322+
smgr.in(var, smgr.range(lower, smgr.makeString("b")))))
323+
.isSatisfiable();
324+
assertThatFormula(
325+
bmgr.and(
326+
smgr.equal(lower, smgr.makeString("c")),
327+
smgr.in(var, smgr.range(lower, smgr.makeString("b")))))
328+
.isUnsatisfiable();
329+
}
330+
}
331+
263332
@Test
264333
public void testStringPrefixSuffixConcat() throws SolverException, InterruptedException {
265334
// FIXME: Princess will timeout on this test

0 commit comments

Comments
 (0)