Skip to content

Commit 11c748c

Browse files
GUIpspclaude
andcommitted
Model Java integral types as fixed-width BitVectors
Migrate int/short/char/byte -> 32-bit BitVec and long -> 64-bit BitVec in the SMT translation. Z3 BitVec ops match Java exactly: mkBVAdd/Sub/Mul wrap (two's-complement overflow), mkBVSDiv truncates toward zero, mkBVSRem takes the sign of the dividend, signed comparisons; int<->long mixing sign-extends, and BV<->real/FP coercions go through mkBV2Int/getLong. ExpressionFolding (Java-int constant folding) is now consistent with the solver. Closes three holes at once: ErrorIntOverflowUnsound (46341*46341 wraps to -2147479015), ErrorNegativeModuloUnsound (-7 % 3 == -1), and ErrorLongAsRealDivisionUnsound (7L/2L == 3). Sound fixed-width semantics also exposed six Correct tests that were themselves latently unsound (they asserted mathematically-true but Java-false postconditions that overflow): CorrectSimpleIfElse (-a at MIN_VALUE), CorrectSpecificFunctionInvocation / CorrectLongUsage / CorrectFunctionsTutorial / CorrectIfThen / CorrectOperatorAssignments (+,-,* overflow). Each now carries overflow-guard preconditions/bounds so its postcondition genuinely holds (CorrectLongUsage widens to long before multiplying). ErrorLongUsage gains a third expected-error marker for the now-correctly-rejected overflow in its own body. Whole verifier suite is green (304 tests, 0 failures); all 11 Error*Unsound holes are now rejected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent a6c5b85 commit 11c748c

9 files changed

Lines changed: 188 additions & 32 deletions

File tree

liquidjava-example/src/main/java/testSuite/CorrectFunctionsTutorial.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,22 @@
44

55
public class CorrectFunctionsTutorial {
66

7-
@Refinement("_ >= 0 && _ >= n")
8-
public static int sum(int n) {
7+
// n + t1 overflows once the running sum exceeds Integer.MAX_VALUE. Bounding n to [0, 46340] keeps the
8+
// sum below 46340^2 < Integer.MAX_VALUE; the inductive ceiling _ <= n * 46340 (since n(n+1)/2 <= 46340*n
9+
// for n <= 46340) lets the modular check prove the addition cannot overflow at any recursion depth.
10+
@Refinement("_ >= 0 && _ >= n && _ <= n * 46340")
11+
public static int sum(@Refinement("0 <= n && n <= 46340") int n) {
912
if (n <= 0) return 0;
1013
else {
1114
int t1 = sum(n - 1);
1215
return n + t1;
1316
}
1417
}
1518

19+
// 0 - n overflows back to Integer.MIN_VALUE (still negative) when n == Integer.MIN_VALUE, so exclude it;
20+
// for every other n the magnitude is non-negative and at least n.
1621
@Refinement("_ >= 0 && _ >= n")
17-
public static int absolute(int n) {
22+
public static int absolute(@Refinement("n > -2147483648") int n) {
1823
if (0 <= n) return n;
1924
else return 0 - n;
2025
}

liquidjava-example/src/main/java/testSuite/CorrectIfThen.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
@SuppressWarnings("unused")
66
public class CorrectIfThen {
77

8-
public void have2(int a, int b) {
8+
// a - b overflows when b is very negative (a - b exceeds Integer.MAX_VALUE). Requiring b >= 0 keeps
9+
// a - b in [1, a] (both operands non-negative with a > b), so it stays positive without overflowing.
10+
public void have2(int a, @Refinement("b >= 0") int b) {
911
@Refinement("pos > 0")
1012
int pos = 10;
1113
if (a > 0) {

liquidjava-example/src/main/java/testSuite/CorrectLongUsage.java

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,18 @@
55
@SuppressWarnings("unused")
66
public class CorrectLongUsage {
77

8-
@Refinement("_ > 10")
8+
// `a * 2` would be 32-bit int arithmetic (overflowing at a >= 2^30) BEFORE the widening to long. Widening
9+
// the operand to long first makes the multiply 64-bit and overflow-free. For int a > 10 the result is in
10+
// (20, 2*Integer.MAX_VALUE], hence > 10 and bounded by 4294967294.
11+
@Refinement("_ > 10 && _ <= 4294967294")
912
public static long doubleBiggerThanTen(@Refinement("a > 10") int a) {
10-
return a * 2;
13+
long widened = a; // widen to 64-bit before multiplying, so a * 2 cannot overflow as 32-bit int
14+
return widened * 2;
1115
}
1216

17+
// a * 2 overflows in 64-bit once a exceeds Long.MAX_VALUE / 2, so bound a to keep 2*a in range and > 40.
1318
@Refinement("_ > 40")
14-
public static long doubleBiggerThanTwenty(@Refinement("a > 20") long a) {
19+
public static long doubleBiggerThanTwenty(@Refinement("a > 20 && a <= 4611686018427387903") long a) {
1520
return a * 2;
1621
}
1722

@@ -27,7 +32,9 @@ public static void main(String[] args) {
2732
long c = -a;
2833
}
2934

30-
@Refinement("d > 10")
35+
// Carry doubleBiggerThanTen's upper bound on d so that d * 2 below provably stays > 20 and in range
36+
// for doubleBiggerThanTwenty's a <= 4611686018427387903 precondition (and cannot itself overflow).
37+
@Refinement("d > 10 && d <= 4294967294")
3138
long d = doubleBiggerThanTen(100);
3239

3340
@Refinement("e > 10")

liquidjava-example/src/main/java/testSuite/CorrectOperatorAssignments.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,16 @@
44

55
public class CorrectOperatorAssignments {
66

7+
// x + 1 overflows to Integer.MIN_VALUE when x == Integer.MAX_VALUE, so exclude that boundary.
78
@Refinement("_ > 0")
8-
int plus(@Refinement("_ >= 0") int x) {
9+
int plus(@Refinement("_ >= 0 && _ < 2147483647") int x) {
910
x += 1; // x is now > 0
1011
return x;
1112
}
1213

14+
// x + 1 overflows when x == Integer.MAX_VALUE, so exclude that boundary.
1315
@Refinement("_ > 0")
14-
int plusInvocation(@Refinement("_ >= 0") int x) {
16+
int plusInvocation(@Refinement("_ >= 0 && _ < 2147483647") int x) {
1517
x += one(); // x is now > 0
1618
return x;
1719
}
@@ -21,14 +23,16 @@ int one() {
2123
return 1;
2224
}
2325

26+
// x - 1 overflows to Integer.MAX_VALUE when x == Integer.MIN_VALUE, so exclude that boundary.
2427
@Refinement("_ < 0")
25-
int minus(@Refinement("_ <= 0") int x) {
28+
int minus(@Refinement("_ <= 0 && _ > -2147483648") int x) {
2629
x -= 1; // x is now < 0
2730
return x;
2831
}
2932

33+
// x * x overflows (and can wrap negative) once |x| exceeds 46340, so bound |x| to keep the square non-negative.
3034
@Refinement("_ >= 0")
31-
int multiply(int x) {
35+
int multiply(@Refinement("_ >= -46340 && _ <= 46340") int x) {
3236
x *= x; // x is now >= 0
3337
return x;
3438
}

liquidjava-example/src/main/java/testSuite/CorrectSimpleIfElse.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@
55
@SuppressWarnings("unused")
66
public class CorrectSimpleIfElse {
77

8-
@Refinement("_ > 0")
9-
public static int toPositive(@Refinement("a < 0") int a) {
8+
// -Integer.MIN_VALUE overflows back to Integer.MIN_VALUE (still negative), so the negation is only
9+
// provably positive away from that boundary. We bound the input to a small negative window so the
10+
// result is a small positive value (also keeping the caller's `toPositive(ex_a) * 10` overflow-free).
11+
@Refinement("_ > 0 && _ < 1000")
12+
public static int toPositive(@Refinement("a < 0 && a > -1000") int a) {
1013
return -a;
1114
}
1215

@@ -28,7 +31,8 @@ public static void main(String[] args) {
2831
}
2932

3033
// EXAMPLE 2
31-
@Refinement("_ < 10")
34+
// Bound ex_a to the small negative window toPositive accepts (and keep the *10 below in range).
35+
@Refinement("_ < 10 && _ > -1000")
3236
int ex_a = 5;
3337
if (ex_a < 0) {
3438
@Refinement("_ >= 10")

liquidjava-example/src/main/java/testSuite/CorrectSpecificFunctionInvocation.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44

55
@SuppressWarnings("unused")
66
public class CorrectSpecificFunctionInvocation {
7+
// a * 2 overflows (wraps negative) once a reaches 2^30, so bound a below 2^30 for the result to stay positive.
78
@Refinement(" _ > 0")
8-
public static int doubleBiggerThanTen(@Refinement("a > 10") int a) {
9+
public static int doubleBiggerThanTen(@Refinement("a > 10 && a <= 1073741823") int a) {
910
return a * 2;
1011
}
1112

1213
public static void main(String[] args) {
13-
@Refinement("a > 0")
14+
// Upper bound so the call below provably satisfies doubleBiggerThanTen's a <= 1073741823 precondition.
15+
@Refinement("a > 0 && a <= 1073741823")
1416
int a = 50;
1517
int b = doubleBiggerThanTen(a);
1618
}

liquidjava-example/src/main/java/testSuite/ErrorLongUsage.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
public class ErrorLongUsage {
77
@Refinement(" _ > 40")
88
public static long doubleBiggerThanTwenty(@Refinement("a > 20") long a) {
9-
return a * 2;
9+
return a * 2; // Refinement Error
1010
}
1111

1212
public static void longUsage1() {

liquidjava-verifier/src/main/java/liquidjava/smt/TranslatorContextToZ3.java

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -90,15 +90,18 @@ private static Expr<?> getExpr(Context z3, String name, CtTypeReference<?> type)
9090
String typeName = type.getQualifiedName();
9191

9292
return switch (typeName) {
93-
case "int", "short", "char", "java.lang.Integer", "java.lang.Short", "java.lang.Character" -> z3
94-
.mkIntConst(name);
93+
// Java integral types are fixed-width two's-complement values, so they are modeled as Z3 BitVectors
94+
// (32-bit for int/short/char/byte and their boxed forms, 64-bit for long). This makes overflow,
95+
// signed division/remainder, and signed comparison faithful to the JLS instead of unbounded integers.
96+
case "int", "short", "char", "byte", "java.lang.Integer", "java.lang.Short", "java.lang.Character", "java.lang.Byte" -> z3
97+
.mkBVConst(name, 32);
9598
case "boolean", "java.lang.Boolean" -> z3.mkBoolConst(name);
96-
case "long", "java.lang.Long" -> z3.mkRealConst(name);
99+
case "long", "java.lang.Long" -> z3.mkBVConst(name, 64);
97100
// Java `float` is binary32 and `double` is binary64; modeling float as FP64 would lose single-precision
98101
// rounding. Mixed-precision expressions are reconciled via Java numeric promotion in TranslatorToZ3.
99102
case "float", "java.lang.Float" -> (FPExpr) z3.mkConst(name, z3.mkFPSort32());
100103
case "double", "java.lang.Double" -> (FPExpr) z3.mkConst(name, z3.mkFPSort64());
101-
case "int[]" -> z3.mkArrayConst(name, z3.mkIntSort(), z3.mkIntSort());
104+
case "int[]" -> z3.mkArrayConst(name, z3.mkBitVecSort(32), z3.mkBitVecSort(32));
102105
default -> z3.mkConst(name, z3.mkUninterpretedSort(typeName));
103106
};
104107
}
@@ -133,12 +136,14 @@ private static void addBuiltinFunctions(Context z3, Map<String, FuncDecl<?>> fun
133136

134137
static Sort getSort(Context z3, String sort) {
135138
return switch (sort) {
136-
case "int", "short", "char", "java.lang.Integer", "java.lang.Short", "java.lang.Character" -> z3.getIntSort();
139+
// Integral types are fixed-width BitVectors (32-bit for int-family, 64-bit for long); see getExpr.
140+
case "int", "short", "char", "byte", "java.lang.Integer", "java.lang.Short", "java.lang.Character", "java.lang.Byte" -> z3
141+
.mkBitVecSort(32);
137142
case "boolean", "java.lang.Boolean" -> z3.getBoolSort();
138-
case "long", "java.lang.Long" -> z3.getRealSort();
143+
case "long", "java.lang.Long" -> z3.mkBitVecSort(64);
139144
case "float", "java.lang.Float" -> z3.mkFPSort32();
140145
case "double", "java.lang.Double" -> z3.mkFPSortDouble();
141-
case "int[]" -> z3.mkArraySort(z3.mkIntSort(), z3.mkIntSort());
146+
case "int[]" -> z3.mkArraySort(z3.mkBitVecSort(32), z3.mkBitVecSort(32));
142147
case "String" -> z3.getStringSort();
143148
case "void" -> z3.mkUninterpretedSort("void");
144149
default -> z3.mkUninterpretedSort(sort);

0 commit comments

Comments
 (0)