Skip to content

Commit be54c23

Browse files
authored
Adds proof support for the pythonic API (#99)
1 parent 10bec15 commit be54c23

File tree

4 files changed

+186
-0
lines changed

4 files changed

+186
-0
lines changed

cvc5_pythonic_api/cvc5_pythonic.py

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6212,6 +6212,42 @@ def model(self):
62126212
"""
62136213
return ModelRef(self)
62146214

6215+
def proof(self):
6216+
"""Return a proof for the last `check()`.
6217+
6218+
This function raises an exception if
6219+
a proof is not available (e.g., last `check()` does not return unsat).
6220+
6221+
>>> s = Solver()
6222+
>>> s.set('produce-proofs','true')
6223+
>>> a = Int('a')
6224+
>>> s.add(a + 2 == 0)
6225+
>>> s.check()
6226+
sat
6227+
>>> try:
6228+
... s.proof()
6229+
... except RuntimeError:
6230+
... print("failed to get proof (last `check()` must have returned unsat)")
6231+
failed to get proof (last `check()` must have returned unsat)
6232+
>>> s.add(a == 0)
6233+
>>> s.check()
6234+
unsat
6235+
>>> s.proof()
6236+
(SCOPE: Not(And(a + 2 == 0, a == 0)),
6237+
(SCOPE: Not(And(a + 2 == 0, a == 0)),
6238+
[a + 2 == 0, a == 0],
6239+
(EQ_RESOLVE: False,
6240+
(ASSUME: a == 0, [a == 0]),
6241+
(MACRO_SR_EQ_INTRO: (a == 0) == False,
6242+
[a == 0, 7, 12],
6243+
(EQ_RESOLVE: a == -2,
6244+
(ASSUME: a + 2 == 0, [a + 2 == 0]),
6245+
(MACRO_SR_EQ_INTRO: (a + 2 == 0) == (a == -2),
6246+
[a + 2 == 0, 7, 12]))))))
6247+
"""
6248+
p = self.solver.getProof()[0]
6249+
return ProofRef(self, p)
6250+
62156251
def assertions(self):
62166252
"""Return an AST vector containing all added constraints.
62176253
@@ -6760,6 +6796,98 @@ def evaluate(t):
67606796
return m[t]
67616797

67626798

6799+
class ProofRef:
6800+
"""A proof tree where every proof reference corresponds to the
6801+
root step of a proof. The branches of the root step are the
6802+
premises of the step."""
6803+
6804+
def __init__(self, solver, proof):
6805+
self.proof = proof
6806+
self.solver = solver
6807+
6808+
def __del__(self):
6809+
if self.solver is not None:
6810+
self.solver = None
6811+
6812+
def __repr__(self):
6813+
return obj_to_string(self)
6814+
6815+
def getRule(self):
6816+
"""Returns the proof rule used by the root step of the proof.
6817+
6818+
>>> s = Solver()
6819+
>>> s.set('produce-proofs','true')
6820+
>>> a = Int('a')
6821+
>>> s.add(a + 2 == 0, a == 0)
6822+
>>> s.check()
6823+
unsat
6824+
>>> p = s.proof()
6825+
>>> p.getRule()
6826+
<ProofRule.SCOPE: 1>
6827+
"""
6828+
return self.proof.getRule()
6829+
6830+
def getResult(self):
6831+
"""Returns the conclusion of the root step of the proof.
6832+
6833+
>>> s = Solver()
6834+
>>> s.set('produce-proofs','true')
6835+
>>> a = Int('a')
6836+
>>> s.add(a + 2 == 0, a == 0)
6837+
>>> s.check()
6838+
unsat
6839+
>>> p = s.proof()
6840+
>>> p.getResult()
6841+
Not(And(a + 2 == 0, a == 0))
6842+
"""
6843+
return _to_expr_ref(self.proof.getResult(), Context(self.solver))
6844+
6845+
def getChildren(self):
6846+
"""Returns the premises, i.e., proofs themselvels, of the root step of
6847+
the proof.
6848+
6849+
>>> s = Solver()
6850+
>>> s.set('produce-proofs','true')
6851+
>>> a = Int('a')
6852+
>>> s.add(a + 2 == 0, a == 0)
6853+
>>> s.check()
6854+
unsat
6855+
>>> p = s.proof()
6856+
>>> p = p.getChildren()[0].getChildren()[0]
6857+
>>> p
6858+
(EQ_RESOLVE: False,
6859+
(ASSUME: a == 0, [a == 0]),
6860+
(MACRO_SR_EQ_INTRO: (a == 0) == False,
6861+
[a == 0, 7, 12],
6862+
(EQ_RESOLVE: a == -2,
6863+
(ASSUME: a + 2 == 0, [a + 2 == 0]),
6864+
(MACRO_SR_EQ_INTRO: (a + 2 == 0) == (a == -2),
6865+
[a + 2 == 0, 7, 12]))))
6866+
"""
6867+
children = self.proof.getChildren()
6868+
return [ProofRef(self.solver, cp) for cp in children]
6869+
6870+
def getArguments(self):
6871+
"""Returns the arguments of the root step of the proof as a list of
6872+
expressions.
6873+
6874+
>>> s = Solver()
6875+
>>> s.set('produce-proofs','true')
6876+
>>> a = Int('a')
6877+
>>> s.add(a + 2 == 0, a == 0)
6878+
>>> s.check()
6879+
unsat
6880+
>>> p = s.proof()
6881+
>>> p.getArguments()
6882+
[]
6883+
>>> p = p.getChildren()[0]
6884+
>>> p.getArguments()
6885+
[a + 2 == 0, a == 0]
6886+
"""
6887+
args = self.proof.getArguments()
6888+
return [_to_expr_ref(a, Context(self.solver)) for a in args]
6889+
6890+
67636891
def simplify(a):
67646892
"""Simplify the expression `a`.
67656893

cvc5_pythonic_api/cvc5_pythonic_printer.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1318,6 +1318,27 @@ def pp_model(self, m):
13181318
break
13191319
return seq3(r, "[", "]")
13201320

1321+
def pp_proof(self, p, d):
1322+
if d > self.max_depth:
1323+
return self.pp_ellipses()
1324+
r = []
1325+
rule = str(p.getRule())[10:]
1326+
result = p.getResult()
1327+
childrenProofs = p.getChildren()
1328+
args = p.getArguments()
1329+
result_pp = self.pp_expr(result, 0, [])
1330+
r.append(
1331+
compose(to_format("{}: ".format(rule)), indent(_len(rule) + 2, result_pp))
1332+
)
1333+
if args:
1334+
r_args = []
1335+
for arg in args:
1336+
r_args.append(self.pp_expr(arg, 0, []))
1337+
r.append(seq3(r_args, "[", "]"))
1338+
for cPf in childrenProofs:
1339+
r.append(self.pp_proof(cPf, d + 1))
1340+
return seq3(r)
1341+
13211342
def pp_func_entry(self, e):
13221343
num = e.num_args()
13231344
if num > 1:
@@ -1377,6 +1398,8 @@ def main(self, a):
13771398
return self.pp_seq(a.assertions(), 0, [])
13781399
elif isinstance(a, cvc.ModelRef):
13791400
return self.pp_model(a)
1401+
elif isinstance(a, cvc.ProofRef):
1402+
return self.pp_proof(a, 0)
13801403
elif isinstance(a, list) or isinstance(a, tuple):
13811404
return self.pp_list(a)
13821405
else:

test/pgm_outputs/proof.py.out

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
unsat

test/pgms/proof.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from cvc5 import ProofRule
2+
from cvc5_pythonic_api import *
3+
4+
def collect_initial_assumptions(proof):
5+
# the initial assumptions are all the arguments of the initial
6+
# SCOPE applications in the proof
7+
proof_assumptions = []
8+
while (proof.getRule() == ProofRule.SCOPE):
9+
proof_assumptions += proof.getArguments()
10+
proof = proof.getChildren()[0]
11+
return proof_assumptions
12+
13+
def validate_proof_assumptions(assertions, proof_assumptions):
14+
# checks that the assumptions in the produced proof match the
15+
# assertions in the problem
16+
return sum([c in assertions for c in proof_assumptions]) == len(proof_assumptions)
17+
18+
19+
p1, p2, p3 = Bools('p1 p2 p3')
20+
x, y = Ints('x y')
21+
s = Solver()
22+
s.set('produce-proofs','true')
23+
assertions = [p1, p2, p3, Implies(p1, x > 0), Implies(p2, y > x), Implies(p2, y < 1), Implies(p3, y > -3)]
24+
25+
for a in assertions:
26+
s.add(a)
27+
28+
print(s.check())
29+
30+
proof = s.proof()
31+
32+
proof_assumptions = collect_initial_assumptions(proof)
33+
34+
assert validate_proof_assumptions(assertions, proof_assumptions)

0 commit comments

Comments
 (0)