Skip to content

Commit 3dffd6e

Browse files
authored
Merge pull request #627 from SRI-CSL/fix-iss-626-mcsat-ceil
Fix iss 626 mcsat ceil
2 parents cf9f5be + d88f8a9 commit 3dffd6e

4 files changed

Lines changed: 94 additions & 5 deletions

File tree

src/mcsat/preprocessor.c

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2284,17 +2284,26 @@ term_t preprocessor_apply(preprocessor_t* pre, term_t t, ivector_t* out, bool is
22842284
break;
22852285
}
22862286

2287-
case ARITH_CEIL: // floor: purify, but its interpreted
2287+
case ARITH_CEIL: // ceil: purify, but its interpreted
22882288
{
22892289
term_t child = arith_ceil_arg(terms, current);
22902290
term_t child_pre = preprocessor_get(pre, child);
22912291

22922292
if (child_pre != NULL_TERM) {
2293-
child_pre = preprocessor_purify(pre, child_pre, out);
2294-
if (child_pre != child) {
2295-
current_pre = arith_floor(terms, child_pre);
2293+
if (term_kind(terms, child_pre) == ARITH_CONSTANT) {
2294+
rational_t ceil;
2295+
q_init(&ceil);
2296+
q_set(&ceil, rational_term_desc(terms, child_pre));
2297+
q_ceil(&ceil);
2298+
current_pre = arith_constant(terms, &ceil);
2299+
q_clear(&ceil);
22962300
} else {
2297-
current_pre = current;
2301+
child_pre = preprocessor_purify(pre, child_pre, out);
2302+
if (child_pre != child) {
2303+
current_pre = arith_ceil(terms, child_pre);
2304+
} else {
2305+
current_pre = current;
2306+
}
22982307
}
22992308
} else {
23002309
ivector_push(pre_stack, child);
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
;; Regression for issue #626: the mcsat preprocessor's ARITH_CEIL arm
2+
;; was a copy-paste of the ARITH_FLOOR arm and silently rewrote
3+
;; (ceil x) to (floor x) when x had been substituted to a non-integer
4+
;; rational. Each (check) below would have produced the wrong answer
5+
;; pre-fix.
6+
7+
(define x::real)
8+
9+
;; Test 1: ceil(7/2) = 4 must be sat (true value of ceil).
10+
;; Pre-fix: rewritten to (floor x) = 4, which is unsat for x = 7/2.
11+
(push)
12+
(assert (= x (/ 7 2)))
13+
(assert (= (ceil x) 4))
14+
(check)
15+
(pop)
16+
17+
;; Test 2: ceil(7/2) = 3 must be unsat (3 is the floor, not the ceil).
18+
;; Pre-fix: rewritten to (floor x) = 3, which is sat for x = 7/2.
19+
(push)
20+
(assert (= x (/ 7 2)))
21+
(assert (= (ceil x) 3))
22+
(check)
23+
(pop)
24+
25+
;; Test 3: tautology ceil(x) >= x must hold, so its negation is unsat.
26+
;; Pre-fix: rewritten to floor(x) >= x, which is sat for any non-integer
27+
;; value of x (here x = 7/2 makes floor(x) = 3 < x).
28+
(push)
29+
(assert (= x (/ 7 2)))
30+
(assert (not (>= (ceil x) x)))
31+
(check)
32+
(pop)
33+
34+
;; Test 4: pin the non-constant ARITH_CEIL path. The argument is the
35+
;; polynomial (+ x 1/2), and x is bounded but not equality-substituted
36+
;; to a constant, so the preprocessor's ARITH_CEIL arm finds child_pre
37+
;; non-constant, calls preprocessor_purify (introducing a fresh proxy p
38+
;; with p = x + 1/2), and rebuilds (ceil p). Pre-fix that rebuild used
39+
;; arith_floor instead of arith_ceil. With x in (5/2, 3], the polynomial
40+
;; lies in (3, 7/2]: the correct ceil is always 4, the buggy floor is
41+
;; always 3.
42+
(push)
43+
(assert (> x (/ 5 2)))
44+
(assert (<= x 3))
45+
(assert (= (ceil (+ x (/ 1 2))) 4))
46+
(check)
47+
(pop)
48+
49+
;; Test 5: same non-constant ARITH_CEIL path, opposite truth value.
50+
;; Asserting (= (ceil (+ x 1/2)) 3) over the same x range must be
51+
;; unsat under the correct ceil (which is uniformly 4 there) and sat
52+
;; under the buggy floor rewrite (which is uniformly 3).
53+
(push)
54+
(assert (> x (/ 5 2)))
55+
(assert (<= x 3))
56+
(assert (= (ceil (+ x (/ 1 2))) 3))
57+
(check)
58+
(pop)
59+
60+
;; Test 6: pin the "argument unchanged after purify" arm of the
61+
;; non-constant ARITH_CEIL path -- (ceil x) where x is uninterpreted
62+
;; and bounded by inequalities only. preprocessor_purify is the
63+
;; identity on UNINTERPRETED_TERM, so child_pre == child and the arm
64+
;; takes the inner else: current_pre = current. (ceil x) >= x is a
65+
;; tautology over the reals, so its negation is unsat. Pre-fix this
66+
;; would have rebuilt the outer term as (>= x x) via the spurious
67+
;; floor rewrite for x in (0, 1) and reported sat.
68+
(push)
69+
(assert (>= x 0))
70+
(assert (<= x 1))
71+
(assert (not (>= (ceil x) x)))
72+
(check)
73+
(pop)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
sat
2+
unsat
3+
unsat
4+
sat
5+
unsat
6+
unsat
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
--mcsat

0 commit comments

Comments
 (0)