Search code examples
z3smt

Testing that a constant matches an interval


Given the following simple example:

s = Solver()

Z = IntSort()
a = Const('a', Z)

s.add(a >= 0)
s.add(a < 10)

print(s.check(a > 5)) # sat

Up until the last line, a has an implied range of 0 <= a < 10 - for which a > 5 does not satisfy. However, .check() tells z3 that "this is true" - which is not what I'm after.

Is there a way to ask z3 to test if a > 5 given the existing set of constraints, and have z3 interpret this as "make sure this is true given everything else we know about a"?


EDIT: Okay I think I figured it out. Since z3 tries to find a model that satisfies all constraints, I should instead check for the inverse to see if it finds a solution - in which case, my check would not hold.

In this case, my "test" function would become:

def test(s, expr):
    return s.check(Not(expr)) == unsat

thus...

s = Solver()

Z = IntSort()
a = Const('a', Z)

s.add(a >= 0)
s.add(a < 10)

print(s.check(a > 5)) # sat

print(s.check(Not(a > 5)) == unsat) # false, test failed
s.add(a > 8)

print(s.check(Not(a > 5)) == unsat) # true, test passed

Is that correct?


Solution

  • What you are doing is essentially correct, though not idiomatic. When you issue s.check(formula), you're telling z3 to show the satisfiability of all the other constraints you add'ed, along with formula; i.e., their conjunction. So, the way you set it up, it gives you the correct result.

    Here's some more detail. Since what you want to prove looks like:

    Implies(And(lower, upper), required)
    

    the typical way of doing this would be to assert its negation, and then check if it is satisfiable. That is, you'd check if:

    Not(Implies(And(lower, upper), required))
    

    is satisfiable. Recall that Implies(a, b) is equivalent to Or(Not(a), b), so a little bit of Boolean logic transforms the above to:

    And(And(lower, upper), Not(required))
    

    What you are doing is to add the first conjunct above (i.e., And(lower, upper)) to the solver itself, and then use the second conjunct Not(required) as an argument to check to see if you get unsat. If you do, then you get a "proof" that required always holds. Otherwise you get a counterexample.

    I should add, however, that this is not idiomatic usage of z3py and calls to check with a formula. The latter is usually used with a list of assumptions, and figuring out which subset of them was unsatisfiable with a call to get-unsat-assumptions. These applications typically come from model-checking problems. For details, see Section 4.2.5 of https://smtlib.cs.uiowa.edu/papers/smt-lib-reference-v2.6-r2021-05-12.pdf which discusses the semantics of check-sat-assuming, which is the SMTLib equivalent of calling check with extra assumptions.

    To solve your problem in more idiomatic z3, one would instead write something like:

    from z3 import *
    
    s = Solver()
    a = Int('a')
    
    lower    = a >= 0
    upper    = a < 10
    required = a > 5
    
    formula = Implies(And(lower, upper), required)
    
    # assert the negation
    s.add(Not(formula))
    r = s.check()
    if r == sat:
        print("Not valid, counterexample:")
        print(s.model())
    elif r == unsat:
        print("Valid!")
    else:
        print("Solver said:", r)
    

    This prints:

    Not valid, counterexample:
    [a = 0]
    

    And if you change to lower = a >= 8, it'll print:

    Valid!
    

    Bottom line, what you're doing is correct but not very idiomatic usage for check-sat-assuming. A simpler way is to simply assert the negation of the implication that your lower/upper bounds imply the required inequality and check if the result is unsat instead.