z3-z3-4.13.0.examples.python.mini_quip.py Maven / Gradle / Ivy
The newest version!
from z3 import *
import heapq
import numpy
import time
import random
verbose = True
# Simplistic (and fragile) converter from
# a class of Horn clauses corresponding to
# a transition system into a transition system
# representation as
# It assumes it is given three Horn clauses
# of the form:
# init(x) => Invariant(x)
# Invariant(x) and trans(x,x') => Invariant(x')
# Invariant(x) and goal(x) => Goal(x)
# where Invariant and Goal are uninterpreted predicates
class Horn2Transitions:
def __init__(self):
self.trans = True
self.init = True
self.inputs = []
self.goal = True
self.index = 0
def parse(self, file):
fp = Fixedpoint()
goals = fp.parse_file(file)
for r in fp.get_rules():
if not is_quantifier(r):
continue
b = r.body()
if not is_implies(b):
continue
f = b.arg(0)
g = b.arg(1)
if self.is_goal(f, g):
continue
if self.is_transition(f, g):
continue
if self.is_init(f, g):
continue
def is_pred(self, p, name):
return is_app(p) and p.decl().name() == name
def is_goal(self, body, head):
if not self.is_pred(head, "Goal"):
return False
pred, inv = self.is_body(body)
if pred is None:
return False
self.goal = self.subst_vars("x", inv, pred)
self.goal = self.subst_vars("i", self.goal, self.goal)
self.inputs += self.vars
self.inputs = list(set(self.inputs))
return True
def is_body(self, body):
if not is_and(body):
return None, None
fmls = [f for f in body.children() if self.is_inv(f) is None]
inv = None
for f in body.children():
if self.is_inv(f) is not None:
inv = f;
break
return And(fmls), inv
def is_inv(self, f):
if self.is_pred(f, "Invariant"):
return f
return None
def is_transition(self, body, head):
pred, inv0 = self.is_body(body)
if pred is None:
return False
inv1 = self.is_inv(head)
if inv1 is None:
return False
pred = self.subst_vars("x", inv0, pred)
self.xs = self.vars
pred = self.subst_vars("xn", inv1, pred)
self.xns = self.vars
pred = self.subst_vars("i", pred, pred)
self.inputs += self.vars
self.inputs = list(set(self.inputs))
self.trans = pred
return True
def is_init(self, body, head):
for f in body.children():
if self.is_inv(f) is not None:
return False
inv = self.is_inv(head)
if inv is None:
return False
self.init = self.subst_vars("x", inv, body)
return True
def subst_vars(self, prefix, inv, fml):
subst = self.mk_subst(prefix, inv)
self.vars = [ v for (k,v) in subst ]
return substitute(fml, subst)
def mk_subst(self, prefix, inv):
self.index = 0
if self.is_inv(inv) is not None:
return [(f, self.mk_bool(prefix)) for f in inv.children()]
else:
vars = self.get_vars(inv)
return [(f, self.mk_bool(prefix)) for f in vars]
def mk_bool(self, prefix):
self.index += 1
return Bool("%s%d" % (prefix, self.index))
def get_vars(self, f, rs=[]):
if is_var(f):
return z3util.vset(rs + [f], str)
else:
for f_ in f.children():
rs = self.get_vars(f_, rs)
return z3util.vset(rs, str)
# Produce a finite domain solver.
# The theory QF_FD covers bit-vector formulas
# and pseudo-Boolean constraints.
# By default cardinality and pseudo-Boolean
# constraints are converted to clauses. To override
# this default for cardinality constraints
# we set sat.cardinality.solver to True
def fd_solver():
s = SolverFor("QF_FD")
s.set("sat.cardinality.solver", True)
return s
# negate, avoid double negation
def negate(f):
if is_not(f):
return f.arg(0)
else:
return Not(f)
def cube2clause(cube):
return Or([negate(f) for f in cube])
class State:
def __init__(self, s):
self.R = set([])
self.solver = s
def add(self, clause):
if clause not in self.R:
self.R |= { clause }
self.solver.add(clause)
def is_seq(f):
return isinstance(f, list) or isinstance(f, tuple) or isinstance(f, AstVector)
# Check if the initial state is bad
def check_disjoint(a, b):
s = fd_solver()
s.add(a)
s.add(b)
return unsat == s.check()
# Remove clauses that are subsumed
def prune(R):
removed = set([])
s = fd_solver()
for f1 in R:
s.push()
for f2 in R:
if f2 not in removed:
s.add(Not(f2) if f1.eq(f2) else f2)
if s.check() == unsat:
removed |= { f1 }
s.pop()
return R - removed
# Quip variant of IC3
must = True
may = False
class QLemma:
def __init__(self, c):
self.cube = c
self.clause = cube2clause(c)
self.bad = False
def __hash__(self):
return hash(tuple(set(self.cube)))
def __eq__(self, qlemma2):
if set(self.cube) == set(qlemma2.cube) and self.bad == qlemma2.bad:
return True
else:
return False
def __ne__():
if not self.__eq__(self, qlemma2):
return True
else:
return False
class QGoal:
def __init__(self, cube, parent, level, must, encounter):
self.level = level
self.cube = cube
self.parent = parent
self.must = must
def __lt__(self, other):
return self.level < other.level
class QReach:
# it is assumed that there is a single initial state
# with all latches set to 0 in hardware design, so
# here init will always give a state where all variable are set to 0
def __init__(self, init, xs):
self.xs = xs
self.constant_xs = [Not(x) for x in self.xs]
s = fd_solver()
s.add(init)
is_sat = s.check()
assert is_sat == sat
m = s.model()
# xs is a list, "for" will keep the order when iterating
self.states = numpy.array([[False for x in self.xs]]) # all set to False
assert not numpy.max(self.states) # since all element is False, so maximum should be False
# check if new state exists
def is_exist(self, state):
if state in self.states:
return True
return False
def enumerate(self, i, state_b, state):
while i < len(state) and state[i] not in self.xs:
i += 1
if i >= len(state):
if state_b.tolist() not in self.states.tolist():
self.states = numpy.append(self.states, [state_b], axis = 0)
return state_b
else:
return None
state_b[i] = False
if self.enumerate(i+1, state_b, state) is not None:
return state_b
else:
state_b[i] = True
return self.enumerate(i+1, state_b, state)
def is_full_state(self, state):
for i in range(len(self.xs)):
if state[i] in self.xs:
return False
return True
def add(self, cube):
state = self.cube2partial_state(cube)
assert len(state) == len(self.xs)
if not self.is_exist(state):
return None
if self.is_full_state(state):
self.states = numpy.append(self.states, [state], axis = 0)
else:
# state[i] is instance, state_b[i] is boolean
state_b = numpy.array(state)
for i in range(len(state)): # state is of same length as self.xs
# i-th literal in state hasn't been assigned value
# init un-assigned literals in state_b as True
# make state_b only contain boolean value
if state[i] in self.xs:
state_b[i] = True
else:
state_b[i] = is_true(state[i])
if self.enumerate(0, state_b, state) is not None:
lits_to_remove = set([negate(f) for f in list(set(cube) - set(self.constant_xs))])
self.constant_xs = list(set(self.constant_xs) - lits_to_remove)
return state
return None
def cube2partial_state(self, cube):
s = fd_solver()
s.add(And(cube))
is_sat = s.check()
assert is_sat == sat
m = s.model()
state = numpy.array([m.eval(x) for x in self.xs])
return state
def state2cube(self, s):
result = copy.deepcopy(self.xs) # x1, x2, ...
for i in range(len(self.xs)):
if not s[i]:
result[i] = Not(result[i])
return result
def intersect(self, cube):
state = self.cube2partial_state(cube)
mask = True
for i in range(len(self.xs)):
if is_true(state[i]) or is_false(state[i]):
mask = (self.states[:, i] == state[i]) & mask
intersects = numpy.reshape(self.states[mask], (-1, len(self.xs)))
if intersects.size > 0:
return And(self.state2cube(intersects[0])) # only need to return one single intersect
return None
class Quip:
def __init__(self, init, trans, goal, x0, inputs, xn):
self.x0 = x0
self.inputs = inputs
self.xn = xn
self.init = init
self.bad = goal
self.trans = trans
self.min_cube_solver = fd_solver()
self.min_cube_solver.add(Not(trans))
self.goals = []
s = State(fd_solver())
s.add(init)
s.solver.add(trans) # check if a bad state can be reached in one step from current level
self.states = [s]
self.s_bad = fd_solver()
self.s_good = fd_solver()
self.s_bad.add(self.bad)
self.s_good.add(Not(self.bad))
self.reachable = QReach(self.init, x0)
self.frames = [] # frames is a 2d list, each row (representing level) is a set containing several (clause, bad) pairs
self.count_may = 0
def next(self, f):
if is_seq(f):
return [self.next(f1) for f1 in f]
return substitute(f, zip(self.x0, self.xn))
def prev(self, f):
if is_seq(f):
return [self.prev(f1) for f1 in f]
return substitute(f, zip(self.xn, self.x0))
def add_solver(self):
s = fd_solver()
s.add(self.trans)
self.states += [State(s)]
def R(self, i):
return And(self.states[i].R)
def value2literal(self, m, x):
value = m.eval(x)
if is_true(value):
return x
if is_false(value):
return Not(x)
return None
def values2literals(self, m, xs):
p = [self.value2literal(m, x) for x in xs]
return [x for x in p if x is not None]
def project0(self, m):
return self.values2literals(m, self.x0)
def projectI(self, m):
return self.values2literals(m, self.inputs)
def projectN(self, m):
return self.values2literals(m, self.xn)
# Block a cube by asserting the clause corresponding to its negation
def block_cube(self, i, cube):
self.assert_clause(i, cube2clause(cube))
# Add a clause to levels 1 until i
def assert_clause(self, i, clause):
for j in range(1, i + 1):
self.states[j].add(clause)
assert str(self.states[j].solver) != str([False])
# minimize cube that is core of Dual solver.
# this assumes that props & cube => Trans
# which means props & cube can only give us a Tr in Trans,
# and it will never make !Trans sat
def minimize_cube(self, cube, inputs, lits):
# min_cube_solver has !Trans (min_cube.solver.add(!Trans))
is_sat = self.min_cube_solver.check(lits + [c for c in cube] + [i for i in inputs])
assert is_sat == unsat
# unsat_core gives us some lits which make Tr sat,
# so that we can ignore other lits and include more states
core = self.min_cube_solver.unsat_core()
assert core
return [c for c in core if c in set(cube)]
# push a goal on a heap
def push_heap(self, goal):
heapq.heappush(self.goals, (goal.level, goal))
# make sure cube to be blocked excludes all reachable states
def check_reachable(self, cube):
s = fd_solver()
for state in self.reachable.states:
s.push()
r = self.reachable.state2cube(state)
s.add(And(self.prev(r)))
s.add(self.prev(cube))
is_sat = s.check()
s.pop()
if is_sat == sat:
# if sat, it means the cube to be blocked contains reachable states
# so it is an invalid cube
return False
# if all fail, is_sat will be unsat
return True
# Rudimentary generalization:
# If the cube is already unsat with respect to transition relation
# extract a core (not necessarily minimal)
# otherwise, just return the cube.
def generalize(self, cube, f):
s = self.states[f - 1].solver
if unsat == s.check(cube):
core = s.unsat_core()
if self.check_reachable(core):
return core, f
return cube, f
def valid_reachable(self, level):
s = fd_solver()
s.add(self.init)
for i in range(level):
s.add(self.trans)
for state in self.reachable.states:
s.push()
s.add(And(self.next(self.reachable.state2cube(state))))
print self.reachable.state2cube(state)
print s.check()
s.pop()
def lemmas(self, level):
return [(l.clause, l.bad) for l in self.frames[level]]
# whenever a new reachable state is found, we use it to mark some existing lemmas as bad lemmas
def mark_bad_lemmas(self, new):
s = fd_solver()
reset = False
for frame in self.frames:
for lemma in frame:
s.push()
s.add(lemma.clause)
is_sat = s.check(new)
if is_sat == unsat:
reset = True
lemma.bad = True
s.pop()
if reset:
self.states = [self.states[0]]
for i in range(1, len(self.frames)):
self.add_solver()
for lemma in self.frames[i]:
if not lemma.bad:
self.states[i].add(lemma.clause)
# prev & tras -> r', such that r' intersects with cube
def add_reachable(self, prev, cube):
s = fd_solver()
s.add(self.trans)
s.add(prev)
s.add(self.next(And(cube)))
is_sat = s.check()
assert is_sat == sat
m = s.model()
new = self.projectN(m)
state = self.reachable.add(self.prev(new)) # always add as non-primed
if state is not None: # if self.states do not have new state yet
self.mark_bad_lemmas(self.prev(new))
# Check if the negation of cube is inductive at level f
def is_inductive(self, f, cube):
s = self.states[f - 1].solver
s.push()
s.add(self.prev(Not(And(cube))))
is_sat = s.check(cube)
if is_sat == sat:
m = s.model()
s.pop()
if is_sat == sat:
cube = self.next(self.minimize_cube(self.project0(m), self.projectI(m), self.projectN(m)))
elif is_sat == unsat:
cube, f = self.generalize(cube, f)
cube = self.next(cube)
return cube, f, is_sat
# Determine if there is a cube for the current state
# that is potentially reachable.
def unfold(self, level):
core = []
self.s_bad.push()
R = self.R(level)
self.s_bad.add(R) # check if current frame intersects with bad states, no trans
is_sat = self.s_bad.check()
if is_sat == sat:
m = self.s_bad.model()
cube = self.project0(m)
props = cube + self.projectI(m)
self.s_good.push()
self.s_good.add(R)
is_sat2 = self.s_good.check(props)
assert is_sat2 == unsat
core = self.s_good.unsat_core()
assert core
core = [c for c in core if c in set(cube)]
self.s_good.pop()
self.s_bad.pop()
return is_sat, core
# A state s0 and level f0 such that
# not(s0) is f0-1 inductive
def quip_blocked(self, s0, f0):
self.push_heap(QGoal(self.next(s0), None, f0, must, 0))
while self.goals:
f, g = heapq.heappop(self.goals)
sys.stdout.write("%d." % f)
if not g.must:
self.count_may -= 1
sys.stdout.flush()
if f == 0:
if g.must:
s = fd_solver()
s.add(self.init)
s.add(self.prev(g.cube))
# since init is a complete assignment, so g.cube must equal to init in sat solver
assert is_sat == s.check()
if verbose:
print("")
return g
self.add_reachable(self.init, g.parent.cube)
continue
r0 = self.reachable.intersect(self.prev(g.cube))
if r0 is not None:
if g.must:
if verbose:
print ""
s = fd_solver()
s.add(self.trans)
# make it as a concrete reachable state
# intersect returns an And(...), so use children to get cube list
g.cube = r0.children()
while True:
is_sat = s.check(self.next(g.cube))
assert is_sat == sat
r = self.next(self.project0(s.model()))
r = self.reachable.intersect(self.prev(r))
child = QGoal(self.next(r.children()), g, 0, g.must, 0)
g = child
if not check_disjoint(self.init, self.prev(g.cube)):
# g is init, break the loop
break
init = g
while g.parent is not None:
g.parent.level = g.level + 1
g = g.parent
return init
if g.parent is not None:
self.add_reachable(r0, g.parent.cube)
continue
cube = None
is_sat = sat
f_1 = len(self.frames) - 1
while f_1 >= f:
for l in self.frames[f_1]:
if not l.bad and len(l.cube) > 0 and set(l.cube).issubset(g.cube):
cube = l.cube
is_sat == unsat
break
f_1 -= 1
if cube is None:
cube, f_1, is_sat = self.is_inductive(f, g.cube)
if is_sat == unsat:
self.frames[f_1].add(QLemma(self.prev(cube)))
self.block_cube(f_1, self.prev(cube))
if f_1 < f0:
# learned clause might also be able to block same bad states in higher level
if set(list(cube)) != set(list(g.cube)):
self.push_heap(QGoal(cube, None, f_1 + 1, may, 0))
self.count_may += 1
else:
# re-queue g.cube in higher level, here g.parent is simply for tracking down the trace when output.
self.push_heap(QGoal(g.cube, g.parent, f_1 + 1, g.must, 0))
if not g.must:
self.count_may += 1
else:
# qcube is a predecessor of g
qcube = QGoal(cube, g, f_1 - 1, g.must, 0)
if not g.must:
self.count_may += 1
self.push_heap(qcube)
if verbose:
print("")
return None
# Check if there are two states next to each other that have the same clauses.
def is_valid(self):
i = 1
inv = None
while True:
# self.states[].R contains full lemmas
# self.frames[] contains delta-encoded lemmas
while len(self.states) <= i+1:
self.add_solver()
while len(self.frames) <= i+1:
self.frames.append(set())
duplicates = set([])
for l in self.frames[i+1]:
if l in self.frames[i]:
duplicates |= {l}
self.frames[i] = self.frames[i] - duplicates
pushed = set([])
for l in (self.frames[i] - self.frames[i+1]):
if not l.bad:
s = self.states[i].solver
s.push()
s.add(self.next(Not(l.clause)))
s.add(l.clause)
is_sat = s.check()
s.pop()
if is_sat == unsat:
self.frames[i+1].add(l)
self.states[i+1].add(l.clause)
pushed |= {l}
self.frames[i] = self.frames[i] - pushed
if (not (self.states[i].R - self.states[i+1].R)
and len(self.states[i].R) != 0):
inv = prune(self.states[i].R)
F_inf = self.frames[i]
j = i + 1
while j < len(self.states):
for l in F_inf:
self.states[j].add(l.clause)
j += 1
self.frames[len(self.states)-1] = F_inf
self.frames[i] = set([])
break
elif (len(self.states[i].R) == 0
and len(self.states[i+1].R) == 0):
break
i += 1
if inv is not None:
self.s_bad.push()
self.s_bad.add(And(inv))
is_sat = self.s_bad.check()
if is_sat == unsat:
self.s_bad.pop()
return And(inv)
self.s_bad.pop()
return None
def run(self):
if not check_disjoint(self.init, self.bad):
return "goal is reached in initial state"
level = 0
while True:
inv = self.is_valid() # self.add_solver() here
if inv is not None:
return inv
is_sat, cube = self.unfold(level)
if is_sat == unsat:
level += 1
if verbose:
print("Unfold %d" % level)
sys.stdout.flush()
elif is_sat == sat:
cex = self.quip_blocked(cube, level)
if cex is not None:
return cex
else:
return is_sat
def test(file):
h2t = Horn2Transitions()
h2t.parse(file)
if verbose:
print("Test file: %s") % file
mp = Quip(h2t.init, h2t.trans, h2t.goal, h2t.xs, h2t.inputs, h2t.xns)
start_time = time.time()
result = mp.run()
end_time = time.time()
if isinstance(result, QGoal):
g = result
if verbose:
print("Trace")
while g:
if verbose:
print(g.level, g.cube)
g = g.parent
print("--- used %.3f seconds ---" % (end_time - start_time))
validate(mp, result, mp.trans)
return
if isinstance(result, ExprRef):
if verbose:
print("Invariant:\n%s " % result)
print("--- used %.3f seconds ---" % (end_time - start_time))
validate(mp, result, mp.trans)
return
print(result)
def validate(var, result, trans):
if isinstance(result, QGoal):
g = result
s = fd_solver()
s.add(trans)
while g.parent is not None:
s.push()
s.add(var.prev(g.cube))
s.add(var.next(g.parent.cube))
assert sat == s.check()
s.pop()
g = g.parent
if verbose:
print "--- validation succeed ----"
return
if isinstance(result, ExprRef):
inv = result
s = fd_solver()
s.add(trans)
s.push()
s.add(var.prev(inv))
s.add(Not(var.next(inv)))
assert unsat == s.check()
s.pop()
cube = var.prev(var.init)
step = 0
while True:
step += 1
# too many steps to reach invariant
if step > 1000:
if verbose:
print "--- validation failed --"
return
if not check_disjoint(var.prev(cube), var.prev(inv)):
# reach invariant
break
s.push()
s.add(cube)
assert s.check() == sat
cube = var.projectN(s.model())
s.pop()
if verbose:
print "--- validation succeed ----"
return
test("data/horn1.smt2")
test("data/horn2.smt2")
test("data/horn3.smt2")
test("data/horn4.smt2")
test("data/horn5.smt2")
# test("data/horn6.smt2") # not able to finish