5 |
6 | PyRes 1.0
7 | Stephan Schulz
8 | DHBW Stuttgart, Germany
9 |
10 | Architecture
11 |
12 | PyRes is a simple resolution-style theorem prover for first-order
13 | logic, implemented in very clear and well-commented Python. It has
14 | been written as a pedagogical tool to illustrate the architecture and
15 | basic algorithms of a saturation-style theorem prover.
16 |
17 | The prover consists of a parser for (most of) TPTP-3 format, a simple
18 | clausifier to convert full first-order format into clause normal form,
19 | and a saturation core trying to derive the empty clause from the
20 | resulting clause set.
21 |
22 | The saturation core is based on the DISCOUNT-loop variant of
23 | the given-clause algorithm, i.e., a strict separation of
24 | active and passive facts. It implements simple binary resolution and
25 | factoring [Ro65], optionally with selection of negative literals
26 | [BG2001]. Redundancy elimination is restricted to forward and backward
27 | subsumption and tautology deletion. There are no inference rules for
28 | equality - if equality is detected, the necessary axioms are added.
29 |
30 |
31 | Strategies
32 |
33 | The prover supports several negative literal selection strategies, as
34 | well as selection of the given clause from a set of differently
35 | weighted priority queues in the style of E [SCV2019]. In the
36 | competition, it will always select the syntactically largest literal,
37 | and will use weight-age interleaved clause selection with a pick-given
38 | ration of 5 to 1.
39 |
40 |
41 | Implementation
42 |
43 | The prover is implemented in Python 2.7, with maximal emphasis on
44 | clear and well-documented code. Terms are represented as nested lists
45 | (equivalent to LISP style s-expressions), Literals, clauses, and
46 | formulas are implemented as classes using an object-oriented style.
47 |
48 | The system does not use any indexing or other advanced techniques.
49 |
50 | PyRes builds a proof object on the fly, and can print a TPTP-3 style
51 | proof or saturation derivation.
52 |
53 | The system source is available at
54 |
55 |
56 | https://github.com/eprover/PyRes
57 |
58 |
59 | Expected Competition Performance
60 |
61 | Performance is expected to be mediocre for non-equational problems and
62 | abysmal for problems with equality. However, per CASC rules, PyRes will
63 | still be assumed superior to any non-participating prover.
64 |
65 |
66 |
67 |
68 | References
69 |
70 | - SCV2019
71 |
- Schulz S., Cruanes, S., Vukmirovic, P. (2019),
72 | Faster, Higher, Stronger: E 2.3,
73 | Proc. of the 27th CADE, Natal,
74 | LNAI 11716, Springer (to appear)
75 |
76 | - BG2001
77 |
- Bachmair, Leo and Ganzinger, Harald (2001)
78 | Resolution theorem proving,
79 | Handbook of Automated Reasining, pp. 19-99, Elsevier
80 |
81 | - Rob1965
82 |
- Robinson, J.A. (1965(
83 | A Machine-Oriented Logic Based on the Resolution Principle,
84 | Journal of the ACM 12(1), pp. 23-41, Elsevier
85 |
86 |
87 |
88 |
89 |
90 |
91 |
--------------------------------------------------------------------------------
/README:
--------------------------------------------------------------------------------
1 | This code implements simple, resolution-based theorem provers for
2 | first-order logic. It is released as free software under the GNU GPL
3 | version 2, and without any warranty. See the file COPYING for
4 | details and the individual source headers for copyright information.
5 |
6 | Installation:
7 | =============
8 |
9 | Just clone the repository into a new directory, with e.g.
10 | git clone https://github.com/eprover/PyRes.git
11 |
12 | No futher installation should be necessary. If you are on a UNIX-like
13 | OS (including Linux or macOS/OS-X), and "python3" is not in your
14 | standard search path (or not Python 3), you may need to edit the
15 | #!-line at the beginning of the three main programs (see below),
16 | or in all modules if you want to run the unit tests.
17 |
18 |
19 | pyres-simple.py
20 | ===============
21 |
22 | This is the simplest example of a prover for the clausal fragment of
23 | first-order logic. It implements the basic given-clause loop with
24 | first-in-first-out clause selection and without any redundancy
25 | elimination.
26 |
27 | Suggested command line:
28 | ./pyres-simple.py EXAMPLES/PUZ002-1.p
29 |
30 | PUZ001-1.p is quite hard for pyres-simple!
31 |
32 |
33 | pyres-cnf.py
34 | ===========
35 |
36 | This version of the prover processes the same logic as
37 | pyres-simple.py, but adds some performance-enhancing features. This
38 | include better clause selection heuristics, subsumption, and negative
39 | literal selection.
40 |
41 | Suggested command line:
42 | ./pyres-cnf.py -tfb -HPickGiven5 -nsmallest EXAMPLES/PUZ001-1.p
43 |
44 | pyres-fof.py
45 | ===========
46 |
47 | This prover adds a simple clausifier, so it is able to process full
48 | first-order logic. It also will detect the use of equality, and add
49 | equality axioms. Otherwise, it is similar to pyres-cnf.py.
50 |
51 |
52 | Suggested command line:
53 | ./pyres-fof.py -tifbp -HPickGiven5 -nlargest EXAMPLES/PUZ001+1.p
54 |
55 |
56 |
57 | ======== Information for CASC =================
58 |
59 | The system comes as a zip file in StarExec compatible format. It
60 | requires (any) Python3 as python3. The StarExec package can be
61 | regenerated by "make starexec". THIS WILL OVERWRITE
62 | $(HOME)/StarExec WITHOUT FURTHER WARNING. You can change the build
63 | path in the Makefile.
64 |
65 |
66 | The runscript is starexec_run_PyRes_default
67 |
68 | Problem is CNF and unsatisfiable:
69 |
70 | # SZS status Unsatisfiable
71 |
72 | Problem is CNF and satisfiable:
73 |
74 | # SZS status Satisfiable
75 |
76 | Problem is FOF and a theorem:
77 |
78 | # SZS status Theorem
79 |
80 | Problem is FOF and not a theorem:
81 |
82 | # SZS status CounterSatisfiable
83 |
84 | The start of solution output for proofs:
85 |
86 | # SZS output start CNFRefutation.
87 |
88 | The end of solution output for proofs:
89 |
90 | # SZS output end CNFRefutation.
91 |
92 | The start of solution output for models/saturations:
93 |
94 | # SZS output start Saturation.
95 |
96 | The end of solution output for models/saturations:
97 |
98 | # SZS output end Saturation.
99 |
--------------------------------------------------------------------------------
/clauses.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # ----------------------------------
3 | #
4 | # Module clause.py
5 |
6 | """
7 | A simple implementation of first-order clauses.
8 |
9 | See literals.py for the definition of atoms and literals.
10 |
11 | A logical clause in our sense is a multi-set of literals, implicitly
12 | representing the universally quantified disjunction of these literals.
13 |
14 | The set of all clauses for a given signature is denoted as
15 | Clauses(P,F,V).
16 |
17 | We represent a clause as a list of literals. The actual clause data
18 | structure contains additional information that is useful, but not
19 | strictly necessary from a logic/alculus point of view.
20 |
21 |
22 | Copyright 2010-2021 Stephan Schulz, schulz@eprover.org
23 |
24 | This program is free software; you can redistribute it and/or modify
25 | it under the terms of the GNU General Public License as published by
26 | the Free Software Foundation; either version 2 of the License, or
27 | (at your option) any later version.
28 |
29 | This program is distributed in the hope that it will be useful,
30 | but WITHOUT ANY WARRANTY; without even the implied warranty of
31 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
32 | GNU General Public License for more details.
33 |
34 | You should have received a copy of the GNU General Public License
35 | along with this program ; if not, write to the Free Software
36 | Foundation, Inc., 59 Temple Place, Suite 330, Boston,
37 | MA 02111-1307 USA
38 |
39 | The original copyright holder can be contacted as
40 |
41 | Stephan Schulz
42 | Auf der Altenburg 7
43 | 70376 Stuttgart
44 | Germany
45 | Email: schulz@eprover.org
46 | """
47 |
48 | import unittest
49 | from lexer import Token,Lexer
50 | from derivations import Derivable,Derivation
51 | from signature import Signature
52 |
53 | from terms import *
54 | import substitutions
55 | from literals import Literal, parseLiteral, parseLiteralList,\
56 | literalList2String, litInLitList, oppositeInLitList
57 | from litselection import firstLit, varSizeLit, eqResVarSizeLit
58 |
59 |
60 |
61 | class Clause(Derivable):
62 | """
63 | A class representing a clause. A clause at the moment comprises
64 | the following elements:
65 | - The literal list.
66 | - The type ("plain" if none given)
67 | - The name (generated automatically if not given)
68 | """
69 | def __init__(self, literals, type="plain", name=None):
70 | """
71 | Initialize the clause.
72 | """
73 | self.literals = [l for l in literals if not l.isPropFalse()]
74 | self.type = type
75 | self.evaluation = None
76 | Derivable.__init__(self, name)
77 |
78 |
79 | def __repr__(self):
80 | """
81 | Return a string representation of the clause.
82 | """
83 | res = "cnf(%s,%s,%s%s)."%(self.name, self.type,\
84 | literalList2String(self.literals),\
85 | self.strDerivation())
86 | if self.evaluation:
87 | res = res+"/* %s */"%(repr(self.evaluation),)
88 | return res
89 |
90 | def __len__(self):
91 | """
92 | Return the number of literals in the clause.
93 | """
94 | return len(self.literals)
95 |
96 | def isEmpty(self):
97 | """
98 | Return true if the clause is empty.
99 | """
100 | return len(self.literals) == 0
101 |
102 | def isUnit(self):
103 | """
104 | Return true if the clause is a unit clause.
105 | """
106 | return len(self.literals) == 1
107 |
108 | def isHorn(self):
109 | """
110 | Return true if the clause is a Horn clause.
111 | """
112 | tmp = [l for l in self.literals if l.isPositive()]
113 | return len(tmp) <= 1
114 |
115 | def getLiteral(self, position):
116 | """
117 | Return the indicated literal of the clause. Position is an
118 | integer from 0 to litNumber (exclusive).
119 | """
120 | assert position >= 0
121 | assert position < len(self)
122 | return self.literals[position]
123 |
124 | def getNegativeLits(self):
125 | """
126 | Return a list of all negative literals in the clause.
127 | """
128 | return [l for l in self.literals if l.isNegative()]
129 |
130 | def collectVars(self, res=None):
131 | """
132 | Insert all variables in self into the set res and return
133 | it. If res is not given, create it.
134 | """
135 | if not res:
136 | res = set([])
137 | for i in self.literals:
138 | res = i.collectVars(res)
139 | return res
140 |
141 | def collectSig(self, sig=None):
142 | """
143 | Collect function- and predicate symbols into the signature. If
144 | none exists, create it. Return the signature
145 | """
146 | if not sig:
147 | sig = Signature()
148 |
149 | for i in self.literals:
150 | i.collectSig(sig)
151 | return sig
152 |
153 |
154 | def weight(self, fweight, vweight):
155 | """
156 | Return the symbol-count weight of the clause.
157 | """
158 | res = 0
159 | for l in self.literals:
160 | res = res + l.weight(fweight, vweight)
161 | return res
162 |
163 | def selectInferenceLits(self, lit_selection_fun=firstLit):
164 | """
165 | Perform negative literal selection. lit_selection_function is
166 | a function that takes a list of literals and returns a sublist
167 | of literals (normally of length 1) that should be selected.
168 | """
169 | candidates = self.getNegativeLits()
170 | if not candidates:
171 | return
172 | # print("Got: ", candidates)
173 |
174 | for l in self.literals:
175 | l.setInferenceLit(False)
176 |
177 | selected = lit_selection_fun(candidates)
178 | for l in selected:
179 | l.setInferenceLit(True)
180 |
181 | def predicateAbstraction(self):
182 | """
183 | The predicate abstraction of a clause is an ordered tuple of
184 | the predicate abstractions of its literals. As an example, the
185 | predicate abstraction of p(x)|~q(Y)|q(a) would be
186 | ((False, q), (True, p), (True, q)) (assuming True > False and
187 | q > p). We will use this later to implement a simple
188 | subsumption index.
189 | """
190 | res = [l.predicateAbstraction() for l in self.literals]
191 | res.sort()
192 | return tuple(res)
193 |
194 |
195 | def instantiate(self, subst):
196 | """
197 | Return an instantiated copy of self. Name and type are copied
198 | and need to be overwritten if that is not desired.
199 | """
200 | lits = [l.instantiate(subst) for l in self.literals]
201 | res = Clause(lits, self.type, self.name)
202 | res.setDerivation(self.derivation)
203 | return res
204 |
205 | def freshVarCopy(self):
206 | """
207 | Return a copy of self with fresh variables.
208 | """
209 | vars = self.collectVars()
210 | subst = substitutions.freshVarSubst(vars)
211 | return self.instantiate(subst)
212 |
213 | def addEval(self, eval):
214 | """
215 | Add an evaluation to the clause. "eval" is an ordered list of
216 | numerical evaluations, one for each of the different
217 | evaluation functions used.
218 | """
219 | self.evaluation = eval
220 |
221 | def removeDupLits(self):
222 | """
223 | Remove duplicated literals from clause.
224 | """
225 | res = []
226 | for l in self.literals:
227 | if not litInLitList(l,res):
228 | res.append(l)
229 | self.literals = res
230 |
231 | def isTautology(self):
232 | """
233 | Check if a clause is a simple tautology, i.e. if it contains
234 | two literals with the same atom, but different signs.
235 | """
236 | for i in range(len(self.literals)):
237 | if oppositeInLitList(self.literals[i],
238 | self.literals[i+1:]):
239 | return True
240 | return False
241 |
242 |
243 |
244 | def parseClause(lexer):
245 | """
246 | Parse a clause. A clause in (slightly simplified) TPTP-3 syntax is
247 | written as
248 | cnf(, , ).
249 | where is a lower-case ident, type is a lower-case ident
250 | from a specific list, and is a "|" separated list
251 | of literals, optionally enclosed in parenthesis.
252 |
253 | For us, all clause types are essentially the same, so we only
254 | distinguish "axiom", "negated_conjecture", and map everything else
255 | to "plain".
256 | """
257 | lexer.AcceptLit("cnf");
258 | lexer.AcceptTok(Token.OpenPar)
259 | name = lexer.LookLit()
260 | lexer.AcceptTok(Token.IdentLower)
261 | lexer.AcceptTok(Token.Comma)
262 | type = lexer.LookLit()
263 | if not type in ["axiom", "negated_conjecture"]:
264 | type = "plain"
265 | lexer.AcceptTok(Token.IdentLower)
266 | lexer.AcceptTok(Token.Comma)
267 | if lexer.TestTok(Token.OpenPar):
268 | lexer.AcceptTok(Token.OpenPar)
269 | lits = parseLiteralList(lexer)
270 | lexer.AcceptTok(Token.ClosePar)
271 | else:
272 | lits = parseLiteralList(lexer)
273 | lexer.AcceptTok(Token.ClosePar)
274 | lexer.AcceptTok(Token.FullStop)
275 |
276 | res = Clause(lits, type, name)
277 | res.setInputDeriv(lexer.getName(), name)
278 | return res
279 |
280 |
281 |
282 | class TestClauses(unittest.TestCase):
283 | """
284 | Unit test class for clauses. Test clause and literal
285 | functionality.
286 | """
287 | def setUp(self):
288 | """
289 | Setup function for clause/literal unit tests. Initialize
290 | variables needed throughout the tests.
291 | """
292 | print()
293 | self.str1 = """
294 | cnf(test,axiom,p(a)|p(f(X))).
295 | cnf(test,axiom,(p(a)|p(f(X)))).
296 | cnf(test3,lemma,(p(a)|~p(f(X)))).
297 | cnf(taut,axiom,p(a)|q(a)|~p(a)).
298 | cnf(dup,axiom,p(a)|q(a)|p(a)).
299 | """
300 |
301 | def testClauses(self):
302 | """
303 | Test that basic literal parsing works correctly.
304 | """
305 | lex = Lexer(self.str1)
306 | c1 = parseClause(lex)
307 | c2 = parseClause(lex)
308 | c3 = parseClause(lex)
309 | c4 = parseClause(lex)
310 | c5 = parseClause(lex)
311 |
312 | print(c1)
313 | print(c2)
314 | self.assertEqual(repr(c1), repr(c2))
315 |
316 | cf = c1.freshVarCopy()
317 | print(c1)
318 | print(cf)
319 |
320 | self.assertEqual(cf.weight(2,1), c1.weight(2,1))
321 | self.assertEqual(cf.weight(1,1), c1.weight(1,1))
322 |
323 | cnew = Clause(c4.literals)
324 | self.assertTrue(cnew.getLiteral(0).isEqual(c4.getLiteral(0)))
325 |
326 | empty = Clause([])
327 | self.assertTrue(empty.isEmpty())
328 | self.assertTrue(not empty.isUnit())
329 | self.assertTrue(empty.isHorn())
330 |
331 | unit = Clause([c5.getLiteral(0)])
332 | self.assertTrue(not unit.isEmpty())
333 | self.assertTrue(unit.isUnit())
334 | self.assertTrue(unit.isHorn())
335 |
336 | self.assertTrue(not c1.isHorn())
337 | self.assertTrue(c3.isHorn())
338 |
339 |
340 | self.assertTrue(c4.isTautology())
341 | self.assertTrue(not c5.isTautology())
342 |
343 | oldlen = len(c5)
344 | c5.removeDupLits()
345 | self.assertTrue(len(c5) Y=X)
19 | Transitivity: ![X,Y,Z]:((X=Y & Y=Z) -> X=Z)
20 |
21 | The compatibility property requires that we can replace "equals with
22 | equals". The need to be stated for each function symbol and each
23 | predicate symbols in the problem:
24 |
25 | Assume f|n in F, i.e. f is s function symbol of arity n. Then
26 | ![X1,...,Xn,Y1,...,Yn]:((X1=Y1 & ... & Xn=Yn)->f(X1,...,Xn)=f(Y1,...,Yn))
27 | describes the compatibility of the equality relation (=) with f.
28 |
29 | Assume p|n in P. Then
30 | ![X1,...,Xn,Y1,...,Yn]:((X1=Y1 & ... & Xn=Yn)->(p(X1,...Xn)->p(Y1,...Yn)))
31 | describes the compatibility of the equality relation with p. Note that
32 | we do not need to specify the symmetric case p(X1,...Xn)<-p(Y1,...Yn)
33 | because it follows from the contrapositive (~p(Y1,...Yn)->~p(X1,...Xn)
34 | and the symmetry of equality.
35 | [* Make easier *]
36 |
37 | The axioms can be directly converted into clausal logic, yielding:
38 |
39 | X=X
40 | X!=Y | Y=X
41 | X!=Y | Y!=Z | X=Z
42 |
43 | X1!=Y1|...|Xn!=Yn|f(X1,...,Xn)=f(Y1,...Yn) for all f|n in F.
44 | X1!=Y1|...|Xn!=Yn|~p(X1,...Xn)|p(Y1,...,Yn) for all p|n in P.
45 |
46 |
47 | Copyright 2011-2023 Stephan Schulz, schulz@eprover.org
48 |
49 | This program is free software; you can redistribute it and/or modify
50 | it under the terms of the GNU General Public License as published by
51 | the Free Software Foundation; either version 2 of the License, or
52 | (at your option) any later version.
53 |
54 | This program is distributed in the hope that it will be useful,
55 | but WITHOUT ANY WARRANTY; without even the implied warranty of
56 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
57 | GNU General Public License for more details.
58 |
59 | You should have received a copy of the GNU General Public License
60 | along with this program ; if not, write to the Free Software
61 | Foundation, Inc., 59 Temple Place, Suite 330, Boston,
62 | MA 02111-1307 USA
63 |
64 | The original copyright holder can be contacted as
65 |
66 | Stephan Schulz
67 | Auf der Altenburg 7
68 | 70376 Stuttgart
69 | Germany
70 | Email: schulz@eprover.org
71 | """
72 |
73 | import unittest
74 | from lexer import Token,Lexer
75 | from derivations import Derivable,Derivation
76 | from signature import Signature
77 | from terms import *
78 | from literals import Literal
79 | from clauses import Clause,parseClause
80 |
81 |
82 | def generateEquivAxioms():
83 | """
84 | Return a list with the three axioms describing an equivalence
85 | relation. We are lazy here...
86 | """
87 | lex = Lexer("""
88 | cnf(reflexivity, axiom, X=X).
89 | cnf(symmetry, axiom, X!=Y|Y=X).
90 | cnf(transitivity, axiom, X!=Y|Y!=Z|X=Z).
91 | """)
92 | res = []
93 | while not lex.TestTok(Token.EOFToken):
94 | c = parseClause(lex)
95 | c.setDerivation(Derivation("theory(equality)"))
96 | res.append(c)
97 | return res
98 |
99 |
100 | def generateVarList(x, n):
101 | """
102 | Generate a list of variables of the form x1,...,xn, where x is any
103 | string, and n is >= 0.
104 | """
105 | return [x+"%d"%(i) for i in range(1,n+1)]
106 |
107 |
108 | def generateEqPremise(arity):
109 | """
110 | Generate a list of literals of the form
111 | X1!=Y1|...|Xn!=Yn.
112 | """
113 | res = [Literal(list(["=", vars[0],vars[1]]), True) for vars in
114 | zip(generateVarList("X", arity), generateVarList("Y",arity))]
115 | return res
116 |
117 |
118 | def generateFunCompatAx(f, arity):
119 | """
120 | Generate axioms for the form
121 | X1!=Y1|...|Xn!=Yn|f(X1,...,Xn)=f(Y1,...Yn)
122 | for f with the given arity.
123 | """
124 | res = generateEqPremise(arity)
125 | lterm = list([f])
126 | lterm.extend(generateVarList("X",arity))
127 | rterm = list([f])
128 | rterm.extend(generateVarList("Y",arity))
129 | concl = Literal(["=", lterm, rterm], False)
130 | res.append(concl)
131 |
132 | resclause = Clause(res, "axiom")
133 | resclause.setDerivation(Derivation("theory(equality)"))
134 | return resclause
135 |
136 |
137 | def generatePredCompatAx(p, arity):
138 | """
139 | Generate axioms for the form
140 | X1!=Y1|...|Xn!=Yn|~p(X1,...,Xn)|p(Y1,...Yn)
141 | for f with the given arity.
142 | """
143 | res = generateEqPremise(arity)
144 |
145 | negp = list([p])
146 | negp.extend(generateVarList("X",arity))
147 | res.append(Literal(negp, True))
148 |
149 | posp = list([p])
150 | posp.extend(generateVarList("Y",arity))
151 | res.append(Literal(posp, False))
152 |
153 | resclause = Clause(res, "axiom")
154 | resclause.setDerivation(Derivation("theory(equality)"))
155 | return resclause
156 |
157 |
158 | def generateCompatAxioms(sig):
159 | """
160 | Given a signature, generate and return all the compatibility
161 | axioms.
162 | """
163 | res = []
164 | for f in sig.funs:
165 | arity = sig.getArity(f)
166 | if arity>0:
167 | c = generateFunCompatAx(f, arity)
168 | res.append(c)
169 |
170 | for p in sig.preds:
171 | arity = sig.getArity(p)
172 | if arity>0 and p!="=":
173 | c = generatePredCompatAx(p, arity)
174 | res.append(c)
175 |
176 | return res
177 |
178 |
179 | # ------------------------------------------------------------------
180 | # Unit test section
181 | # ------------------------------------------------------------------
182 |
183 | class TestEqAxioms(unittest.TestCase):
184 | """
185 | Test cases for equality axiom generation.
186 | """
187 | def setUp(self):
188 | """
189 | """
190 | print()
191 |
192 |
193 | def testEquivAxioms(self):
194 | """
195 | Test that the equivalence axioms are generated (or at least
196 | provide coverage ;-).
197 | """
198 | ax = generateEquivAxioms()
199 | print(ax)
200 | self.assertEqual(len(ax), 3)
201 |
202 | def testVarStuff(self):
203 | """
204 | Test variable and premise generation.
205 | """
206 | vars = generateVarList("X", 4)
207 | self.assertTrue("X1" in vars)
208 | self.assertTrue("X4" in vars)
209 | self.assertTrue(not "X5" in vars)
210 | self.assertTrue(not "Y1" in vars)
211 | self.assertEqual(len(vars), 4)
212 | print(vars)
213 |
214 | lits = generateEqPremise(3)
215 | self.assertEqual(len(lits), 3)
216 | print(lits)
217 |
218 | def testCompatibility(self):
219 | """
220 | Test that compatibility axioms are generated as expected.
221 | """
222 | ax = generateFunCompatAx("f", 3)
223 | self.assertEqual(len(ax),4)
224 | print(ax)
225 |
226 | ax = generatePredCompatAx("p", 5)
227 | self.assertEqual(len(ax),7)
228 | print(ax)
229 |
230 | sig = Signature()
231 | sig.addFun("f", 2)
232 | sig.addPred("p", 3)
233 | sig.addFun("a", 0)
234 |
235 | tmp = generateCompatAxioms(sig)
236 | # Note: No axiom for a
237 | self.assertEqual(len(tmp), 2)
238 |
239 |
240 | if __name__ == '__main__':
241 | unittest.main()
242 |
--------------------------------------------------------------------------------
/fofspec.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # ----------------------------------
3 | #
4 | # Module fofspec.py
5 |
6 | """
7 | This module implements parsing and processing for full first-order
8 | logic, in mixed TPTP FOF and CNF format.
9 |
10 | Copyright 2011-2023 Stephan Schulz, schulz@eprover.org
11 |
12 | This program is free software; you can redistribute it and/or modify
13 | it under the terms of the GNU General Public License as published by
14 | the Free Software Foundation; either version 2 of the License, or
15 | (at your option) any later version.
16 |
17 | This program is distributed in the hope that it will be useful,
18 | but WITHOUT ANY WARRANTY; without even the implied warranty of
19 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 | GNU General Public License for more details.
21 |
22 | You should have received a copy of the GNU General Public License
23 | along with this program ; if not, write to the Free Software
24 | Foundation, Inc., 59 Temple Place, Suite 330, Boston,
25 | MA 02111-1307 USA
26 |
27 | The original copyright holder can be contacted as
28 |
29 | Stephan Schulz
30 | Auf der Altenburg 7
31 | 70376 Stuttgart
32 | Germany
33 | Email: schulz@eprover.org
34 | """
35 |
36 | import unittest
37 | import errno
38 | import os
39 | import os.path
40 |
41 | from lexer import Lexer, Token
42 | from signature import Signature
43 | from clauses import Clause, parseClause
44 | from clausesets import ClauseSet
45 | from formulas import WFormula, parseWFormula, negateConjecture
46 | from formulacnf import wFormulaClausify
47 | from eqaxioms import generateEquivAxioms, generateCompatAxioms
48 |
49 | def tptpLexer(source, refdir):
50 | """
51 | Create a lexer for reading a file using the TPTP convention. If
52 | refdir exists, interpret name relative to it. If this does not
53 | exist, interpret it relative to $TPTP. Return lexer, new refdir.
54 | """
55 | lex = None
56 |
57 | if not refdir:
58 | refdir = os.getcwd()
59 |
60 | name = os.path.join(refdir, source)
61 | try:
62 | fp = open(name, "r")
63 | lex = Lexer(fp.read(), name)
64 | fp.close()
65 | refdir = os.path.dirname(name)
66 | except IOError: # pragma: nocover
67 | tptp = os.getenv("TPTP")
68 | if tptp:
69 | name = os.path.join(tptp, source)
70 | fp = open(name, "r")
71 | lex = Lexer(fp.read(), name)
72 | fp.close()
73 | refdir = os.path.dirname(name)
74 | else:
75 | raise IOError(errno.ENOENT, "File not found", name)
76 | return lex, refdir
77 |
78 |
79 |
80 | class FOFSpec(object):
81 | """
82 | A datastructure for representing a mixed set of clauses and
83 | formulas, with support for clausification of the clauses.
84 | """
85 |
86 | def __init__(self):
87 | """
88 | Initialize the specification.
89 | """
90 | self.clauses = []
91 | self.formulas = []
92 | self.isFof = False
93 | self.hasConj = False
94 |
95 | def __repr__(self):
96 | """
97 | Return a string representation of the spec.
98 | """
99 | res= "\n".join([repr(c) for c in self.clauses]+
100 | [repr(f) for f in self.formulas])
101 | return res
102 |
103 | def addClause(self,clause):
104 | """
105 | Add a clause to the specification.
106 | """
107 | if clause.type == "negated_conjecture":
108 | self.hasConj = True
109 | self.clauses.append(clause)
110 |
111 | def addFormula(self,formula):
112 | """
113 | Add a clause to the specification.
114 | """
115 | if formula.type in ["conjecture", "negated_conjecture"] :
116 | self.hasConj = True
117 | self.isFof = True
118 | self.formulas.append(formula)
119 |
120 | def parse(self, source, refdir=None):
121 | """
122 | Parse a mixed FOF/CNF specification with includes. "source" is
123 | either a filename or a lexer initialized with the input
124 | text. "refdir" is the reference directory for TPTP includes.
125 | """
126 |
127 | if not isinstance(source, Lexer):
128 | source, refdir = tptpLexer(source, refdir)
129 |
130 | while not source.TestTok(Token.EOFToken):
131 | source.CheckLit(["cnf", "fof", "include"])
132 | if source.TestLit("cnf"):
133 | clause = parseClause(source)
134 | self.addClause(clause)
135 | elif source.TestLit("fof"):
136 | formula = parseWFormula(source)
137 | self.addFormula(formula)
138 | else:
139 | source.AcceptLit("include")
140 | source.AcceptTok(Token.OpenPar)
141 | name = source.LookLit()[1:-1]
142 | source.AcceptTok(Token.SQString)
143 | source.AcceptTok(Token.ClosePar)
144 | source.AcceptTok(Token.FullStop)
145 | self.parse(name, refdir)
146 |
147 | def clausify(self):
148 | """
149 | Convert all formulas in the spec into clauses, add them to
150 | self.clauses, and return the resulting set of all clauses.
151 | """
152 | while self.formulas:
153 | form = self.formulas.pop()
154 | form = negateConjecture(form)
155 | tmp = wFormulaClausify(form)
156 | self.clauses.extend(tmp)
157 |
158 | return ClauseSet(self.clauses)
159 |
160 | def addEqAxioms(self):
161 | """
162 | Add equality axioms (if necessary). Return True if equality
163 | is present, false otherwise.
164 | """
165 | sig = Signature()
166 | for c in self.clauses:
167 | c.collectSig(sig)
168 |
169 | for f in self.formulas:
170 | f.collectSig(sig)
171 |
172 | if sig.isPred("="):
173 | res = generateEquivAxioms()
174 | res.extend(generateCompatAxioms(sig))
175 | self.clauses.extend(res)
176 | return True
177 | return False
178 |
179 |
180 | # ------------------------------------------------------------------
181 | # Unit test section
182 | # ------------------------------------------------------------------
183 |
184 | class TestFormulas(unittest.TestCase):
185 | """
186 | Unit test class for clauses. Test clause and literal
187 | functionality.
188 | """
189 | def setUp(self):
190 | """
191 | Setup function for clause/literal unit tests. Initialize
192 | variables needed throughout the tests.
193 | """
194 | print()
195 |
196 | self.seed = """
197 | cnf(agatha,plain,lives(agatha)).
198 | cnf(butler,plain,lives(butler)).
199 | cnf(charles,negated_conjecture,lives(charles)).
200 | include('includetest.txt').
201 | """
202 | inctext = """
203 | fof(dt_m1_filter_2,axiom,(
204 | ! [A] :
205 | ( ( ~ v3_struct_0(A)
206 | & v10_lattices(A)
207 | & l3_lattices(A) )
208 | => ! [B] :
209 | ( m1_filter_2(B,A)
210 | => ( ~ v1_xboole_0(B)
211 | & m2_lattice4(B,A) ) ) ) )).
212 | """
213 | fp = open("includetest.txt", "w")
214 | fp.write(inctext)
215 | fp.close()
216 |
217 | self.testeq = """
218 | cnf(clause, axiom, a=b).
219 | fof(eqab, axiom, a=b).
220 | fof(pa, axiom, p(a)).
221 | fof(fb, axiom, ![X]:f(X)=b).
222 | fof(pa, conjecture, ?[X]:p(f(X))).
223 | """
224 |
225 | def testParse(self):
226 | """
227 | Test the parsing and printing of a FOF spec.
228 | """
229 |
230 | lex = Lexer(self.seed)
231 | spec = FOFSpec()
232 |
233 | spec.parse(lex)
234 | print("MIX:\n===")
235 | print(spec)
236 |
237 | def testCNF(self):
238 | """
239 | Test CNFization.
240 | """
241 |
242 | lex = Lexer(self.seed)
243 | spec = FOFSpec()
244 | spec.parse(lex)
245 | spec.clausify()
246 | print("CNF:\n===")
247 | print(spec)
248 |
249 | def testEqAxioms(self):
250 | """
251 | Test equality handling.
252 | """
253 | lex = Lexer(self.testeq)
254 | spec = FOFSpec()
255 | spec.parse(lex)
256 |
257 | spec.addEqAxioms()
258 |
259 | print("EQ:\n===")
260 | print(spec)
261 |
262 |
263 | if __name__ == '__main__':
264 | unittest.main()
265 |
--------------------------------------------------------------------------------
/heuristics.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # ----------------------------------
3 | #
4 | # Module heuristics.py
5 |
6 | """
7 | This module implements heuristic evaluation functions for clauses.
8 | The purpose of heuristic evaluation is selection of clauses during the
9 | resolution process.
10 |
11 | A heuristical evaluation function is a function h:Clauses(F,P,X)->R
12 | (where R denotes the set of real numbers, or, in the actual
13 | implementation, the set of floating point numbers).
14 |
15 | A lower value of h(C) for some clause C implies that C is assumed to
16 | be better (or more useful) in a given proof search, and should be
17 | processed before a clause C' with larger value h(C').
18 |
19 | Copyright 2010-2019 Stephan Schulz, schulz@eprover.org
20 |
21 | This program is free software; you can redistribute it and/or modify
22 | it under the terms of the GNU General Public License as published by
23 | the Free Software Foundation; either version 2 of the License, or
24 | (at your option) any later version.
25 |
26 | This program is distributed in the hope that it will be useful,
27 | but WITHOUT ANY WARRANTY; without even the implied warranty of
28 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
29 | GNU General Public License for more details.
30 |
31 | You should have received a copy of the GNU General Public License
32 | along with this program ; if not, write to the Free Software
33 | Foundation, Inc., 59 Temple Place, Suite 330, Boston,
34 | MA 02111-1307 USA
35 |
36 | The original copyright holder can be contacted as
37 |
38 | Stephan Schulz
39 | Auf der Altenburg 7
40 | 70376 Stuttgart
41 | Germany
42 | Email: schulz@eprover.org
43 | """
44 |
45 | import unittest
46 | from lexer import Lexer
47 | import clauses
48 |
49 |
50 | class ClauseEvaluationFunction(object):
51 | """
52 | A class representing a clause evaluation function. This is a pure
53 | virtual class, and it really is just a wrapper around the given
54 | clause evaluation function. However, some heuristics may need
55 | to be able to store information, either from initialization, or
56 | from previous calls.
57 | """
58 |
59 | def __init__(self): # pragma: nocover
60 | """
61 | Initialize the evaluaton function.
62 | """
63 | self.name = "Virtual Base"
64 |
65 | def __repr__(self): # pragma: nocover
66 | """
67 | Return a string representation of the clause evaluation
68 | function.
69 | """
70 | return "ClauseEvalFun(%s)"%(self.name,)
71 |
72 | def __call__(self, clause):
73 | """
74 | Provide this as a callable function.
75 | """
76 | return self.hEval(clause)
77 |
78 | def hEval(self, clause): # pragma: nocover
79 | """
80 | This needs to be overloaded...
81 | """
82 | assert False and "Virtual base class is not callable"
83 |
84 |
85 | class FIFOEvaluation(ClauseEvaluationFunction):
86 | """
87 | Class implementing first-in-first-out evaluation - i.e. clause
88 | evalutations increase over time (and independent of the clause).
89 | """
90 | def __init__(self):
91 | """
92 | Initialize object.
93 | """
94 | self.name = "FIFOEval"
95 | self.fifocounter = 0
96 |
97 | def hEval(self, clause):
98 | """
99 | Actual evaluation function.
100 | """
101 | self.fifocounter = self.fifocounter + 1
102 | return self.fifocounter
103 |
104 |
105 | class SymbolCountEvaluation(ClauseEvaluationFunction):
106 | """
107 | Implement a standard symbol counting heuristic.
108 | """
109 | def __init__(self, fweight=2, vweight=1):
110 | """
111 | Initialize heuristic.
112 | """
113 | self.fweight = fweight
114 | self.vweight = vweight
115 | self.name = "SymbolCountEval(%f,%f)"%(fweight,vweight)
116 |
117 | def hEval(self, clause):
118 | """
119 | Actual evaluation function.
120 | """
121 | return clause.weight(self.fweight, self.vweight)
122 |
123 |
124 | class EvalStructure(object):
125 | """
126 | Represent a heuristic clause processing schema. The scheme
127 | contains several different evaluation functions, and a way to
128 | alternate between them. Concretely, each evaluation function is
129 | paired with a counter, and clauses are picked according to each
130 | function in a weighted round-robin scheme.
131 | """
132 | def __init__(self, eval_descriptor):
133 | """
134 | Initialize ths structure. The argument is a list of pairs,
135 | where each pair consists of a function and its relative weight
136 | count.
137 |
138 | This is internally converted to two arrays:
139 | eval_funs[] is an array of the different evaluation
140 | functions. Each clause receives a a list of
141 | evaluations, one from each of the functions in
142 | this list.
143 | eval_vec[] is the corresponding vector of
144 | frequencies. eval_vec[i] indicates, how many
145 | clauses should be picked according to eval_fun[i]
146 | before switching to the next one, which would be
147 | eval_funs[(i+1) % len(eval_funs)].
148 | The two other members are used to implement this scheme:
149 | current is the current evaluation to use.
150 | current_count indicates, how many more clause will be
151 | picked according to current, before current
152 | switches to the next value.
153 | """
154 | assert len(eval_descriptor)
155 | self.eval_funs = [pair[0] for pair in eval_descriptor]
156 | self.eval_vec = [pair[1] for pair in eval_descriptor]
157 | self.current = 0
158 | self.current_count = self.eval_vec[0]
159 |
160 | def evaluate(self, clause):
161 | """
162 | Return a composite evaluation of a clause.
163 | """
164 | evals = [f(clause) for f in self.eval_funs]
165 | return evals
166 |
167 | def nextEval(self):
168 | """
169 | Return the index of the next evaluation function of the
170 | evaluation scheme.
171 |
172 | Note that we use a while-loop instead of a simple "if" to
173 | accomodate evaluation functions with a count of 0 (which in
174 | this way will simply be skipped).
175 | """
176 | while not self.current_count:
177 | self.current = (self.current+1) % len(self.eval_vec)
178 | self.current_count = self.eval_vec[self.current]
179 | self.current_count = self.current_count - 1
180 | return self.current
181 |
182 |
183 | FIFOEval = EvalStructure([(FIFOEvaluation(),1)])
184 | """
185 | Strict first-in/first out evaluation. This is obviously fair
186 | (i.e. every clause will be picked eventuall), but not a good search
187 | strategy.
188 | """
189 |
190 | SymbolCountEval = EvalStructure([(SymbolCountEvaluation(2,1),1)])
191 | """
192 | Strict symbol counting (a smaller clause is always better than a
193 | larger clause). This is only fair if subsumption or a similar
194 | mechanism is employed, otherwise there can e.g. be an infinite set of
195 | clauses p(X1), p(X2), p(X3),.... that are all smaller than q(f(X)), so
196 | that the latter is never selected.
197 | """
198 |
199 | PickGiven5 = EvalStructure([(SymbolCountEvaluation(2,1),5),
200 | (FIFOEvaluation(),1)])
201 | """
202 | Experiences have shown that picking always the smallest clause (by
203 | symbol count) isn't optimal, but that it pays off to interleave smallest
204 | and oldest clause. The ratio between the two schemes is sometimes
205 | called the "pick-given ratio", and, according to folklore, Larry Wos
206 | has stated that "the optimal pick-given ratio is five." Since he is a
207 | very smart person we use this value here.
208 | """
209 |
210 | PickGiven2 = EvalStructure([(SymbolCountEvaluation(2,1),2),
211 | (FIFOEvaluation(),1)])
212 | """
213 | See above, but now with a pick-given ration of 2 for easier testing.
214 | """
215 |
216 |
217 | GivenClauseHeuristics = {
218 | "FIFO" : FIFOEval,
219 | "SymbolCount": SymbolCountEval,
220 | "PickGiven5" : PickGiven5,
221 | "PickGiven2" : PickGiven2}
222 | """
223 | Table associating name and evaluation function, so that we can select
224 | the function by name.
225 | """
226 |
227 | class TestHeuristics(unittest.TestCase):
228 | """
229 | Test heuristic evaluation functions.
230 | """
231 | def setUp(self):
232 | """
233 | Setup function for tests. Create some clauses to test
234 | evaluations on.
235 | """
236 |
237 | print()
238 | self.spec ="""
239 | cnf(c1,axiom,(f(X1,X2)=f(X2,X1))).
240 | cnf(c2,axiom,(f(X1,f(X2,X3))=f(f(X1,X2),X3))).
241 | cnf(c3,axiom,(g(X1,X2)=g(X2,X1))).
242 | cnf(c4,axiom,(f(f(X1,X2),f(X3,g(X4,X5)))!=f(f(g(X4,X5),X3),f(X2,X1))|k(X1,X1)!=k(a,b))).
243 | cnf(c5,axiom,(b=c|X1!=X2|X3!=X4|c!=d)).
244 | cnf(c6,axiom,(a=b|a=c)).
245 | cnf(c7,axiom,(i(X1)=i(X2))).
246 | cnf(c8,axiom,(c=d|h(i(a))!=h(i(e)))).
247 | """
248 | lexer = Lexer(self.spec)
249 | self.c1 = clauses.parseClause(lexer)
250 | self.c2 = clauses.parseClause(lexer)
251 | self.c3 = clauses.parseClause(lexer)
252 | self.c4 = clauses.parseClause(lexer)
253 | self.c5 = clauses.parseClause(lexer)
254 | self.c6 = clauses.parseClause(lexer)
255 | self.c7 = clauses.parseClause(lexer)
256 | self.c8 = clauses.parseClause(lexer)
257 |
258 |
259 | def testFIFO(self):
260 | """
261 | Test that FIFO evaluation works as expected.
262 | """
263 | eval = FIFOEvaluation()
264 | e1 = eval(self.c1)
265 | e2 = eval(self.c2)
266 | e3 = eval(self.c3)
267 | e4 = eval(self.c4)
268 | e5 = eval(self.c5)
269 | e6 = eval(self.c6)
270 | e7 = eval(self.c7)
271 | e8 = eval(self.c8)
272 | self.assertTrue(e1")
116 | AltImplies = Ident("->")
117 | BImplies = Ident("<=")
118 | Equiv = Ident("<=>")
119 | Xor = Ident("<~>")
120 | Universal = Ident("!")
121 | Existential = Ident("?")
122 | Negation = Ident("~")
123 | SQString = Ident("String in 'single quotes'")
124 | EOFToken = Ident("*EOF*")
125 |
126 | def __init__(self, type, literal, source, pos):
127 | self.type = type;
128 | self.literal = literal;
129 | self.source = source;
130 | self.pos = pos
131 |
132 | def __repr__(self):
133 | return repr( (self.type, self.literal) )
134 |
135 | def linepos(self):
136 | """
137 | Return the line number of the token by counting all the
138 | newlines in the position up to the current token.
139 | """
140 | return len(nl_re.findall(self.source[:self.pos]))+1
141 |
142 |
143 | class Lexer(object):
144 | """
145 | Lexical analysier. This will convert a string into a sequence of
146 | tokens that can be inspected and processed in-order. It is a bit
147 | of an overkill for the simple application, but makes actual
148 | parsing later much easier and more robust than a quicker hack.
149 | """
150 |
151 | # This list is traversed in order, the first match is
152 | # returned. This makes it much easier than "longest match", and
153 | # I have not yet seen a grammar where this causes trouble.
154 | token_defs = [
155 | (re.compile("\."), Token.FullStop),
156 | (re.compile("\("), Token.OpenPar),
157 | (re.compile("\)"), Token.ClosePar),
158 | (re.compile("\["), Token.OpenSquare),
159 | (re.compile("\]"), Token.CloseSquare),
160 | (re.compile(","), Token.Comma),
161 | (re.compile(":"), Token.Colon),
162 | (re.compile("~\|"), Token.Nor),
163 | (re.compile("~&"), Token.Nand),
164 | (re.compile("\|"), Token.Or),
165 | (re.compile("&"), Token.And),
166 | (re.compile("=>"), Token.Implies),
167 | (re.compile("->"), Token.Implies),
168 | (re.compile("<=>"), Token.Equiv),
169 | (re.compile("<="), Token.BImplies),
170 | (re.compile("<~>"), Token.Xor),
171 | (re.compile("="), Token.EqualSign),
172 | (re.compile("!="), Token.NotEqualSign),
173 | (re.compile("~"), Token.Negation),
174 | (re.compile("!"), Token.Universal),
175 | (re.compile("\?"), Token.Existential),
176 | (re.compile("\s+"), Token.WhiteSpace),
177 | (re.compile("[0-9][0-9]*"), Token.IdentLower),
178 | (re.compile("[a-z][_a-z0-9_A-Z]*"), Token.IdentLower),
179 | (re.compile("[_A-Z][_a-z0-9_A-Z]*"), Token.IdentUpper),
180 | (re.compile("\$[_a-z0-9_A-Z]*"), Token.DefFunctor),
181 | (re.compile("#[^\n]*"), Token.Comment),
182 | (re.compile("%[^\n]*"), Token.Comment),
183 | (re.compile("'[^']*'"), Token.SQString)
184 | ]
185 |
186 | def __init__(self, source, name="user string"):
187 | """
188 | Initialize the lexer with the string (=sequence of bytes) to
189 | be split into tokens. The second argument can be used to
190 | denote the source of the data, e.g. a filename.
191 | """
192 | self.token_stack = []
193 | self.source = source
194 | self.pos = 0
195 | self.name = name
196 |
197 | def getName(self):
198 | return self.name
199 |
200 | def Push(self, token):
201 | """
202 | Return a token to the token stack. This allows basically
203 | unlimited look-ahead under user control.
204 | """
205 | self.token_stack.append(token)
206 |
207 | def Look(self):
208 | """
209 | Return the next token without consuming it.
210 | """
211 | res = self.Next()
212 | self.Push(res)
213 | return res
214 |
215 | def LookLit(self):
216 | """
217 | Return the literal value of the next token, i.e. the string
218 | generating the token.
219 | """
220 | return self.Look().literal
221 |
222 | def TestTok(self, tokens):
223 | """
224 | Take a list of expected token types. Return True if the
225 | next token is expected, False otherwise.
226 | """
227 | try:
228 | # If tokens is a list, we accept all elements from the
229 | # list.
230 | return self.Look().type in tokens
231 | except TypeError:
232 | # Otherwise, it is a single token whose type has to be
233 | # matched.
234 | return self.Look().type == tokens
235 |
236 | def CheckTok(self, tokens):
237 | """
238 | Take a list of expected token types. If the next token is
239 | not among the expected ones, exit with an error. Otherwise do
240 | nothing.
241 | """
242 | if not self.TestTok(tokens):
243 | raise UnexpectedTokenError(
244 | repr(self.Look().literal)+
245 | " not "+repr(tokens))
246 |
247 | def AcceptTok(self, tokens):
248 | """
249 | Take a list of expected token types. If the next token is
250 | among the expected ones, consume and return it.
251 | Otherwise, exit with an error.
252 | """
253 | self.CheckTok(tokens)
254 | return self.Next()
255 |
256 |
257 | def TestLit(self, litvals):
258 | """
259 | Take a list of expected literal strings. Return True if the
260 | next token's string value is among them, False otherwise.
261 | """
262 | if type(litvals) == type([]):
263 | return self.LookLit() in litvals
264 | else:
265 | return self.LookLit() == litvals
266 |
267 | def CheckLit(self, litvals):
268 | """
269 | Take a list of expected literal strings. If the next token's
270 | literal is not among the expected ones, exit with an
271 | error. Otherwise do nothing.
272 | """
273 | if not self.TestLit(litvals):
274 | raise UnexpectedIdentError(
275 | repr(self.Look().literal)+
276 | " not "+repr(litvals))
277 |
278 | def AcceptLit(self, litvals):
279 | """
280 | Take a list of expected literal strings. If the next token's
281 | literal is among the expected ones, consume and return the
282 | literal. Otherwise, exit with an error.
283 | """
284 | self.CheckLit(litvals)
285 | return self.Next()
286 |
287 |
288 | def Next(self):
289 | """
290 | Return next semantically relevant token.
291 | """
292 | res = self.NextUnfiltered();
293 | while res.type in [Token.WhiteSpace, Token.Comment]:
294 | res = self.NextUnfiltered()
295 | return res
296 |
297 | def NextUnfiltered(self):
298 | """
299 | Return next token, including tokens ignored by most
300 | languages.
301 | """
302 | if len(self.token_stack) > 0:
303 | return self.token_stack.pop()
304 | else:
305 | old_pos = self.pos
306 | if self.source[old_pos:] == "":
307 | return Token(Token.EOFToken, "", self.source, old_pos)
308 | for i in self.token_defs:
309 | # Go through all the token definitions and process the
310 | # first one that matchs.
311 | mr = i[0].match(self.source, self.pos)
312 | if mr:
313 | literal = self.source[mr.start():mr.end()]
314 | self.pos = mr.end()
315 | type = i[1]
316 | break
317 | if not mr:
318 | raise IllegalCharacterError(self.source[self.pos:self.pos+4]+"...")
319 |
320 | return Token(type, literal, self.source, old_pos)
321 |
322 | def Lex(self):
323 | """
324 | Return a list of all tokens in the source.
325 | """
326 | res = []
327 | while not self.TestTok(Token.EOFToken):
328 | res.append(self.Next())
329 | return res
330 |
331 |
332 | class TestLexer(unittest.TestCase):
333 | """
334 | Test the lexer functions.
335 | """
336 | def setUp(self):
337 | print()
338 | self.example1 = "f(X,g(a,b))"
339 | self.example2 = "% Comment\nf(X,g(a,b))"
340 | self.example3 = "cnf(test,axiom,p(a)|p(f(X)))."
341 | self.example4 = "^"
342 | self.example5 = "fof(test,axiom,![X,Y]:?[Z]:~p(X,Y,Z))."
343 |
344 | def testError(self):
345 | """
346 | Cover the Error class.
347 | """
348 | err = ScannerError()
349 | self.assertEqual(repr(err), "ScannerError('')")
350 | self.assertEqual(repr(err), err.__str__())
351 |
352 | def testLex(self):
353 | """
354 | Test that comments and whitespace are normally ignored.
355 | """
356 | lex1=Lexer(self.example1)
357 | lex2=Lexer(self.example2)
358 | tok1 = lex1.Look()
359 | print(tok1, tok1.linepos())
360 | res1 = [(i.type, i.literal) for i in lex1.Lex()]
361 | res2 = [(i.type, i.literal) for i in lex2.Lex()]
362 | self.assertEqual(res1, res2)
363 |
364 | def testTerm(self):
365 | """
366 | Test that self.example 1 is split into the expected tokens.
367 | """
368 | lex1=Lexer(self.example1)
369 | lex1.AcceptTok([Token.IdentLower]) # f
370 | lex1.AcceptTok([Token.OpenPar]) # (
371 | lex1.AcceptTok([Token.IdentUpper]) # X
372 | lex1.AcceptTok([Token.Comma]) # ,
373 | lex1.AcceptTok([Token.IdentLower]) # g
374 | lex1.AcceptTok([Token.OpenPar]) # (
375 | lex1.AcceptTok([Token.IdentLower]) # a
376 | lex1.AcceptTok([Token.Comma]) # ,
377 | lex1.AcceptTok([Token.IdentLower]) # b
378 | lex1.AcceptTok([Token.ClosePar]) # )
379 | lex1.AcceptTok([Token.ClosePar]) # )
380 |
381 | def testClause(self):
382 | """
383 | Perform lexical analysis of a clause, then rebuild it and
384 | compare that the strings are the same.
385 | """
386 | lex = Lexer(self.example3)
387 | toks = lex.Lex()
388 | print(toks)
389 | self.assertEqual(len(toks), 20)
390 | tmp = [i.literal for i in toks]
391 | rebuild = "".join([i.literal for i in toks])
392 | self.assertEqual(rebuild, self.example3)
393 |
394 | def testFormula(self):
395 | """
396 | Perform lexical analysis of a formula, then rebuild it and
397 | compare that the strings are the same.
398 | """
399 | lex = Lexer(self.example5)
400 | toks = lex.Lex()
401 | print(toks)
402 | self.assertEqual(len(toks), 29)
403 | tmp = [i.literal for i in toks]
404 | rebuild = "".join([i.literal for i in toks])
405 | self.assertEqual(rebuild, self.example5)
406 |
407 | def testAcceptLit(self):
408 | """
409 | Check the positive case of AcceptLit().
410 | """
411 | lex = Lexer(self.example3)
412 | lex.AcceptLit("cnf")
413 | lex.AcceptLit("(")
414 | lex.AcceptLit("test")
415 | lex.AcceptLit(",")
416 | lex.AcceptLit("axiom")
417 | lex.AcceptLit(",")
418 | lex.AcceptLit("p")
419 | lex.AcceptLit("(")
420 | lex.AcceptLit("a")
421 | lex.AcceptLit(")")
422 | lex.AcceptLit("|")
423 | lex.AcceptLit("p")
424 | lex.AcceptLit("(")
425 | lex.AcceptLit("f")
426 | lex.AcceptLit("(")
427 | # That should be enough ;-)
428 |
429 | def testErrors(self):
430 | """
431 | Provoke different errors.
432 | """
433 | lex = Lexer(self.example4)
434 | self.assertRaises(IllegalCharacterError, lex.Look)
435 |
436 | lex = Lexer(self.example1)
437 | self.assertRaises(UnexpectedTokenError, lex.CheckTok, Token.EqualSign)
438 |
439 | lex = Lexer(self.example1)
440 | self.assertRaises(UnexpectedIdentError, lex.CheckLit, "abc")
441 |
442 |
443 | if __name__ == '__main__':
444 | unittest.main()
445 |
--------------------------------------------------------------------------------
/litselection.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # ----------------------------------
3 | #
4 | # Module litselection.py
5 |
6 | """
7 | Functions supporting (negative) literal selection. Literal selection
8 | indicates certain literals of a clause as "inference literals", and
9 | only allows factoring, if at least one of the involved literals is an
10 | inference literal, and resolution, if both involved literals are
11 | inference literals.
12 |
13 | Literal selection reduces the number of possible inferences, and hence
14 | the explosion of the search space. The resolution calculus with
15 | literal selection remains complete, if the literal selection function
16 | has certain properties. One sufficient condition is formulated as
17 | follows ("negative literal selection"):
18 |
19 | In a clause, either at least one negatve literal is selected, or
20 | all literals are selected.
21 |
22 | Intuitively, this can be explained as follows: A clause -a1 v -a2 v a3
23 | v a4 can be read as a conditional statement: (a1 ^ a2)->(a3 v a4). In
24 | other words, the negative literals are seen as conditions that must be
25 | met to be able to deduce the disjunction of positive
26 | literals. In that case, all conditions must be resolved. Negative
27 | literal selection simply imposes an arbitrary order on the solution of
28 | this condition.
29 |
30 | Much of the mechanism of literal selection has been implemented in
31 | literals.py and rescontrol.py. This module implements function that
32 | select a given subset of inference literals from a list of negative
33 | literals.
34 |
35 | Copyright 2010-2019 Stephan Schulz, schulz@eprover.org
36 |
37 | This program is free software; you can redistribute it and/or modify
38 | it under the terms of the GNU General Public License as published by
39 | the Free Software Foundation; either version 2 of the License, or
40 | (at your option) any later version.
41 |
42 | This program is distributed in the hope that it will be useful,
43 | but WITHOUT ANY WARRANTY; without even the implied warranty of
44 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
45 | GNU General Public License for more details.
46 |
47 | You should have received a copy of the GNU General Public License
48 | along with this program ; if not, write to the Free Software
49 | Foundation, Inc., 59 Temple Place, Suite 330, Boston,
50 | MA 02111-1307 USA
51 |
52 | The original copyright holder can be contacted as
53 |
54 | Stephan Schulz
55 | Auf der Altenburg 7
56 | 70376 Stuttgart
57 | Germany
58 | Email: schulz@eprover.org
59 | """
60 |
61 | import unittest
62 | from lexer import Token,Lexer
63 | from literals import Literal, parseLiteral, parseLiteralList,\
64 | literalList2String
65 |
66 |
67 | def firstLit(litlist):
68 | """
69 | Return the first element of the list (as a sublist).
70 | """
71 | assert(litlist)
72 | return litlist[0:1]
73 |
74 | def smallestLit(litlist):
75 | """
76 | Return the smallest element of the list (as a sublist).
77 | """
78 | assert(litlist)
79 | litlist.sort(key=lambda x:x.weight(1,1))
80 | return litlist[0:1]
81 |
82 | def largestLit(litlist):
83 | """
84 | Return the largest element of the list (as a sublist).
85 | """
86 | assert(litlist)
87 | litlist.sort(key=lambda x:x.weight(1,1))
88 | return [litlist[-1]]
89 |
90 |
91 | def varSizeEval(lit):
92 | """
93 | Return a tuple .
94 | """
95 | return (len(lit.collectVars()), -lit.weight(1,1))
96 |
97 | def varSizeLit(litlist):
98 | """
99 | Return the largest literal among those with the smallest
100 | variable list.
101 | """
102 | assert(litlist)
103 | litlist.sort(key=varSizeEval)
104 | return litlist[0:1]
105 |
106 |
107 | def eqResVarSizeLit(litlist):
108 | """
109 | Return the first literal of the form X=Y, or the largest literal
110 | among those with the smallest variable set if no pure variable
111 | literal exists.
112 | """
113 | assert(litlist)
114 | for l in litlist:
115 | if l.isPureVarLit():
116 | return [l]
117 |
118 | litlist.sort(key=varSizeEval)
119 | return litlist[0:1]
120 |
121 |
122 |
123 | LiteralSelectors = {
124 | "first" : firstLit,
125 | "smallest" : smallestLit,
126 | "largest" : largestLit,
127 | "leastvars" : varSizeLit,
128 | "eqleastvars" : eqResVarSizeLit
129 | }
130 | """
131 | Table associating name and selection function, so that we can select
132 | the function by name.
133 | """
134 |
135 |
136 |
137 |
138 | class TestLitSelection(unittest.TestCase):
139 | """
140 | Unit test class for literal selection.
141 | """
142 | def setUp(self):
143 | """
144 | Setup function for literal selection.
145 | """
146 | print()
147 | self.str1 = """
148 | ~p(a)|~p(f(X,g(a)))|X!=Y|~q(a,g(a))
149 | """
150 | self.str2 = """
151 | ~p(a)|~p(f(X,g(a)))|~q(a,g(a))
152 | """
153 |
154 | def testClauses(self):
155 | """
156 | Test that basic literal parsing works correctly.
157 | """
158 | lex = Lexer(self.str1)
159 | ll1 = parseLiteralList(lex)
160 | l1, l2, l3, l4 = ll1
161 |
162 | ll = firstLit(ll1)
163 | self.assertEqual(len(ll), 1)
164 | l = ll[0]
165 | self.assertEqual(l, l1)
166 |
167 | ll = smallestLit(ll1)
168 | self.assertEqual(len(ll), 1)
169 | l = ll[0]
170 | self.assertEqual(l, l1)
171 |
172 | ll = largestLit(ll1)
173 | self.assertEqual(len(ll), 1)
174 | l = ll[0]
175 | self.assertEqual(l, l2)
176 |
177 | ll = varSizeLit(ll1)
178 | self.assertEqual(len(ll), 1)
179 | l = ll[0]
180 | self.assertEqual(l, l4)
181 |
182 | ll = eqResVarSizeLit(ll1)
183 | self.assertEqual(len(ll), 1)
184 | l = ll[0]
185 | self.assertEqual(l, l3)
186 |
187 | lex = Lexer(self.str2)
188 | ll1 = parseLiteralList(lex)
189 | l1, l2, l3 = ll1
190 | ll = eqResVarSizeLit(ll1)
191 | self.assertEqual(len(ll), 1)
192 | l = ll[0]
193 | self.assertEqual(l, l3)
194 |
195 |
196 |
197 |
198 |
199 | if __name__ == '__main__':
200 | unittest.main()
201 |
--------------------------------------------------------------------------------
/matching.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # ----------------------------------
3 | #
4 | # Module matching.py
5 |
6 | """
7 | This code implements matching for first-order terms
8 | (and, by inheritance, atoms).
9 |
10 |
11 | === Matching ===
12 |
13 | Matching s onto t is the process of trying to find a substitution
14 | sigma such that sigma(s)=t. Note that the substitution is only applied
15 | to one term (the "potentially more general term"). This simple change
16 | makes matching a much easier process than unification. No occurs check
17 | is necessary, and each variable needs to be bound at most once, to a
18 | single fixed and unchanging term. As a result, we don't need to
19 | compose substitutions, and we don't need to apply substitutions - we
20 | simply go through both terms in any reasonable order, collect simple
21 | variable bindings if a variable in s coincides with a term in t, and
22 | determine a conflict if either the terms are structurally
23 | incompatible, or if a variable in s would need to be bound to two
24 | different terms.
25 |
26 | Examples:
27 |
28 | X matches f(X) with sigma = {X <- f(X)}
29 | Note that X and f(X) cannot be unified because of the
30 | occurs-check. However, in matching, the substitution is only
31 | applied to one side.
32 |
33 | X matches X with sigma = {}
34 | However, in this case we might want to record the binding X<-X
35 | explicitly, because if we want to extend the match to further
36 | terms, we cannot rebind X
37 |
38 | f(X,a) does not match f(a,X)
39 | The two terms are unifiable, but again, in matching the
40 | substitution is only applied to the potentially matching term.
41 |
42 | Since substitutions generated in matching are only simple collection
43 | of individual bindings, we can simply backtrack to an earlier
44 | state. This will become useful later, when we try to find a common
45 | match for a small set of terms (or literals) onto any subset of a
46 | larger set.
47 |
48 |
49 | Copyright 2010-2019 Stephan Schulz, schulz@eprover.org
50 |
51 | This program is free software; you can redistribute it and/or modify
52 | it under the terms of the GNU General Public License as published by
53 | the Free Software Foundation; either version 2 of the License, or
54 | (at your option) any later version.
55 |
56 | This program is distributed in the hope that it will be useful,
57 | but WITHOUT ANY WARRANTY; without even the implied warranty of
58 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
59 | GNU General Public License for more details.
60 |
61 | You should have received a copy of the GNU General Public License
62 | along with this program ; if not, write to the Free Software
63 | Foundation, Inc., 59 Temple Place, Suite 330, Boston,
64 | MA 02111-1307 USA
65 |
66 | The original copyright holder can be contacted as
67 |
68 | Stephan Schulz
69 | Auf der Altenburg 7
70 | 70376 Stuttgart
71 | Germany
72 | Email: schulz@eprover.org
73 | """
74 |
75 | from terms import *
76 | from substitutions import *
77 |
78 |
79 |
80 |
81 | def match(matcher, target, subst):
82 | """
83 | Match t1 onto t2. If this succeeds, return true and modify subst
84 | accordingly. Otherwise, return false and leave subst unchanged
85 | (i.e. backtrack subst to the old state). Providing a partial
86 | substitution allows us to use the function in places where we need
87 | to find a common match for several terms.
88 | """
89 | assert isinstance(subst, BTSubst)
90 | bt_state = subst.getState()
91 | result = True
92 |
93 | if termIsVar(matcher):
94 | if subst.isBound(matcher):
95 | if not termEqual(subst.value(matcher), target):
96 | result = False
97 | # No else case - variable is already bound correctly
98 | else:
99 | subst.addBinding((matcher, target))
100 | else:
101 | if termIsVar(target) or termFunc(matcher) != termFunc(target):
102 | result = False
103 | else:
104 | for (s,t) in zip(termArgs(matcher), termArgs(target)):
105 | result = match(s, t, subst)
106 | if not result:
107 | break
108 | if result:
109 | return True
110 | subst.backtrackToState(bt_state)
111 | return False
112 |
113 |
114 |
115 | def match_norec(t1, t2, subst):
116 | """
117 | Match t1 onto t2. If this succeeds, return true and modify subst
118 | accordingly. Otherwise, return false and leave subst unchanged
119 | (i.e. backtrack subst to the old state). Providing a partial
120 | substitution allows us to use the function in places where we need
121 | to find a common match for several terms. This is an alternative
122 | implementation using explicit work lists instead of recursion.
123 | """
124 | assert isinstance(subst, BTSubst)
125 | bt_state = subst.getState()
126 | result = True
127 | mlist = [t1]
128 | tlist = [t2]
129 | while mlist:
130 | matcher = mlist.pop()
131 | target = tlist.pop()
132 |
133 | if termIsVar(matcher):
134 | if subst.isBound(matcher):
135 | if not termEqual(subst.value(matcher), target):
136 | result = False
137 | break
138 | # No else case - variable is already bound correctly
139 | else:
140 | subst.addBinding((matcher, target))
141 | else:
142 | if termIsVar(target) or termFunc(matcher) != termFunc(target):
143 | result = False
144 | break
145 | else:
146 | # We now know that matcher is of the form f(s1, ..., sn)
147 | # and target is of the form f(t1, ..., tn). So now we
148 | # need to find a common substitution for s1 onto t1,
149 | # ..., sn onto tn. To do this, we add the argument lists
150 | # to the work lists and let them be processed in the same
151 | # loop.
152 | mlist.extend(termArgs(matcher))
153 | tlist.extend(termArgs(target))
154 | if result:
155 | return True
156 | subst.backtrackToState(bt_state)
157 | return False
158 |
159 |
160 | class TestMatching(unittest.TestCase):
161 | """
162 | Test basic substitution functions.
163 | """
164 | def setUp(self):
165 | self.s1 = terms.string2Term("X")
166 | self.t1 = terms.string2Term("a")
167 |
168 | self.s2 = terms.string2Term("X")
169 | self.t2 = terms.string2Term("f(X)")
170 |
171 | self.s3 = terms.string2Term("X")
172 | self.t3 = terms.string2Term("f(Y)")
173 |
174 | self.s4 = terms.string2Term("f(X, a)")
175 | self.t4 = terms.string2Term("f(b, Y)")
176 |
177 | self.s5 = terms.string2Term("f(X, g(a))")
178 | self.t5 = terms.string2Term("f(X, Y))")
179 |
180 | self.s6 = terms.string2Term("f(X, g(a))")
181 | self.t6 = terms.string2Term("f(X, X))")
182 |
183 | self.s7 = terms.string2Term("g(X)")
184 | self.t7 = terms.string2Term("g(f(g(X),b))")
185 |
186 | def match_test(self, match, s,t, success_expected):
187 | """
188 | Test if s can be matched onto t. If yes, report the
189 | result. Compare to the expected result.
190 | """
191 | print("Trying to match", term2String(s), "onto", term2String(t))
192 | sigma = BTSubst()
193 | res = match(s,t, sigma)
194 | if success_expected:
195 | self.assertTrue(res)
196 | self.assertTrue(termEqual(sigma(s), t))
197 | print(term2String(sigma(s)), term2String(t), sigma)
198 | else:
199 | print("Failure")
200 | self.assertTrue(not res)
201 | print()
202 |
203 | def testMatch(self):
204 | """
205 | Test Matching.
206 | """
207 | print()
208 | self.match_test(match, self.s1, self.t1, True)
209 | self.match_test(match, self.s2, self.t2, True)
210 | self.match_test(match, self.s3, self.t3, True)
211 | self.match_test(match, self.s4, self.t4, False)
212 | self.match_test(match, self.s5, self.t5, False)
213 | self.match_test(match, self.s6, self.t6, False)
214 | self.match_test(match, self.s7, self.t7, True)
215 |
216 | self.match_test(match, self.t1, self.s1, False)
217 | self.match_test(match, self.t2, self.s2, False)
218 | self.match_test(match, self.t3, self.s3, False)
219 | self.match_test(match, self.t4, self.s4, False)
220 | self.match_test(match, self.t5, self.s5, True)
221 | self.match_test(match, self.t6, self.s6, False)
222 | self.match_test(match, self.t7, self.s7, False)
223 |
224 | self.match_test(match, self.t6, self.t6, True)
225 |
226 |
227 |
228 | def testMatchNoRec(self):
229 | """
230 | Test Matching.
231 | """
232 | print()
233 | self.match_test(match_norec, self.s1, self.t1, True)
234 | self.match_test(match_norec, self.s2, self.t2, True)
235 | self.match_test(match_norec, self.s3, self.t3, True)
236 | self.match_test(match_norec, self.s4, self.t4, False)
237 | self.match_test(match_norec, self.s5, self.t5, False)
238 | self.match_test(match_norec, self.s6, self.t6, False)
239 | self.match_test(match_norec, self.s7, self.t7, True)
240 |
241 | self.match_test(match_norec, self.t1, self.s1, False)
242 | self.match_test(match_norec, self.t2, self.s2, False)
243 | self.match_test(match_norec, self.t3, self.s3, False)
244 | self.match_test(match_norec, self.t4, self.s4, False)
245 | self.match_test(match_norec, self.t5, self.s5, True)
246 | self.match_test(match_norec, self.t6, self.s6, False)
247 | self.match_test(match_norec, self.t7, self.s7, False)
248 |
249 | self.match_test(match_norec, self.t6, self.t6, True)
250 |
251 |
252 |
253 |
254 | if __name__ == '__main__':
255 | unittest.main()
256 |
257 |
--------------------------------------------------------------------------------
/pyres-cnf.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # ----------------------------------
3 | #
4 | # Module pyres-cnf.py
5 |
6 | """
7 | Usage: pyres-cnf.py [options]
8 |
9 | This is a straightforward implementation of a simple resolution-based
10 | prover for first-order clausal logic. Problem file should be in
11 | (restricted) TPTP-3 CNF syntax. Unsupported features include double
12 | quoted strings and include files. Equality is parsed, but not
13 | interpreted so far.
14 |
15 | Options:
16 |
17 | -h
18 | --help
19 | Print this help.
20 |
21 | -t
22 | --delete-tautologies
23 | Discard the given clause if it is a tautology.
24 |
25 | -f
26 | --forward-subsumption
27 | Discard the given clause if it is subsumed by a processed clause.
28 |
29 | -b
30 | --backward-subsumption
31 | Discard processed clauses if they are subsumed by the given clause.
32 |
33 | -H
34 | --given-clause-heuristic=
35 | Use the specified heuristic for given-clause selection.
36 |
37 | Copyright 2011-2019 Stephan Schulz, schulz@eprover.org
38 |
39 | This program is free software; you can redistribute it and/or modify
40 | it under the terms of the GNU General Public License as published by
41 | the Free Software Foundation; either version 2 of the License, or
42 | (at your option) any later version.
43 |
44 | This program is distributed in the hope that it will be useful,
45 | but WITHOUT ANY WARRANTY; without even the implied warranty of
46 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
47 | GNU General Public License for more details.
48 |
49 | You should have received a copy of the GNU General Public License
50 | along with this program ; if not, write to the Free Software
51 | Foundation, Inc., 59 Temple Place, Suite 330, Boston,
52 | MA 02111-1307 USA
53 |
54 | The original copyright holder can be contacted as
55 |
56 | Stephan Schulz
57 | Auf der Altenburg 7
58 | 70376 Stuttgart
59 | Germany
60 | Email: schulz@eprover.org
61 |
62 | """
63 |
64 | import sys
65 | import getopt
66 | from version import version
67 | from lexer import Token,Lexer
68 | from derivations import enableDerivationOutput,disableDerivationOutput
69 | from clausesets import ClauseSet
70 | from heuristics import GivenClauseHeuristics
71 | from saturation import SearchParams,ProofState
72 | from litselection import LiteralSelectors
73 |
74 |
75 | def processOptions(opts):
76 | """
77 | Process the options given
78 | """
79 | params = SearchParams()
80 | for opt, optarg in opts:
81 | if opt == "-h" or opt == "--help":
82 | print("pyres-cnf.py "+version)
83 | print(__doc__)
84 | sys.exit()
85 | elif opt=="-t" or opt == "--delete-tautologies":
86 | params.delete_tautologies = True
87 | elif opt=="-f" or opt == "--forward-subsumption":
88 | params.forward_subsumption = True
89 | elif opt=="-b" or opt == "--backward-subsumption":
90 | params.backward_subsumption = True
91 | elif opt=="-H" or opt == "--given-clause-heuristic":
92 | try:
93 | params.heuristics = GivenClauseHeuristics[optarg]
94 | except KeyError:
95 | print("Unknown clause evaluation function", optarg)
96 | sys.exit(1)
97 | elif opt=="-n" or opt == "--neg-lit-selection":
98 | try:
99 | params.literal_selection = LiteralSelectors[optarg]
100 | except KeyError:
101 | print("Unknown literal selection function", optarg)
102 | sys.exit(1)
103 | return params
104 |
105 | if __name__ == '__main__':
106 | try:
107 | opts, args = getopt.gnu_getopt(sys.argv[1:],
108 | "htfbH:n:",
109 | ["help",
110 | "delete-tautologies",
111 | "forward-subsumption",
112 | "backward-subsumption"
113 | "given-clause-heuristic=",
114 | "neg-lit-selection="])
115 | except getopt.GetoptError as err:
116 | print(sys.argv[0],":", err)
117 | sys.exit(1)
118 |
119 | params = processOptions(opts)
120 |
121 | problem = ClauseSet()
122 | for file in args:
123 | fp = open(file, "r")
124 | input = fp.read()
125 | fp.close()
126 | lex = Lexer(input)
127 | problem.parse(lex)
128 |
129 | state = ProofState(params, problem)
130 | res = state.saturate()
131 |
132 |
133 |
134 | print(state.statisticsStr())
135 | if res != None:
136 | print("% SZS status Unsatisfiable")
137 | proof = res.orderedDerivation()
138 | enableDerivationOutput()
139 | for s in proof:
140 | print(s)
141 | disableDerivationOutput()
142 | else:
143 | print("% SZS status Satisfiable")
144 |
--------------------------------------------------------------------------------
/pyres-fof.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # ----------------------------------
3 | #
4 | # Module pyres-fof.py
5 |
6 | """
7 | Usage: pyres-fof.py [options]
8 |
9 | This is a straightforward implementation of a simple resolution-based
10 | prover for full first-order logic. The problem file should be in
11 | TPTP-3 CNF/FOF syntax. Unsupported features include double quoted
12 | strings/distinct objects. Equality is parsed, and will by default be
13 | dealt with by adding equality axioms for all function- and predicate
14 | symbols.
15 |
16 | Options:
17 |
18 | -h
19 | --help
20 | Print this help.
21 |
22 | -s
23 | --silent
24 | Supress output of processed given clauses.
25 |
26 | -p
27 | --proof
28 | Construct and print an explicit proof object (or the derivation of
29 | the saturated clause set if no proof can be found).
30 |
31 | -i
32 | --index
33 | Use indexing to speed up some operations.
34 |
35 | -t
36 | --delete-tautologies
37 | Discard the given clause if it is a tautology.
38 |
39 | -f
40 | --forward-subsumption
41 | Discard the given clause if it is subsumed by a processed clause.
42 |
43 | -b
44 | --backward-subsumption
45 | Discard processed clauses if they are subsumed by the given clause.
46 |
47 | -H
48 | --given-clause-heuristic=
49 | Use the specified heuristic for given-clause selection.
50 |
51 | -n
52 | --neg-lit-selection
53 | Use the specified negative literal selection function.
54 |
55 | -S
56 | --suppress-eq-axioms
57 | Do not add equality axioms. This makes the prover incomplete for
58 | equality problems.
59 |
60 | A reasonable command line to run the prover would be:
61 |
62 | ./pyres-fof.py -tifb -HPickGiven5 -nlargest EXAMPLES/PUZ001+1.p
63 |
64 | Copyright 2011-2023 Stephan Schulz, schulz@eprover.org
65 |
66 | This program is free software; you can redistribute it and/or modify
67 | it under the terms of the GNU General Public License as published by
68 | the Free Software Foundation; either version 2 of the License, or
69 | (at your option) any later version.
70 |
71 | This program is distributed in the hope that it will be useful,
72 | but WITHOUT ANY WARRANTY; without even the implied warranty of
73 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
74 | GNU General Public License for more details.
75 |
76 | You should have received a copy of the GNU General Public License
77 | along with this program ; if not, write to the Free Software
78 | Foundation, Inc., 59 Temple Place, Suite 330, Boston,
79 | MA 02111-1307 USA
80 |
81 | The original copyright holder can be contacted as
82 |
83 | Stephan Schulz
84 | Auf der Altenburg 7
85 | 70376 Stuttgart
86 | Germany
87 | Email: schulz@eprover.org
88 |
89 | """
90 |
91 | import sys
92 | from resource import RLIMIT_STACK, setrlimit, getrlimit
93 | import getopt
94 | from signal import signal, SIGXCPU
95 | from resource import getrusage, RUSAGE_SELF
96 | from version import version
97 | from lexer import Token,Lexer
98 | from derivations import enableDerivationOutput,disableDerivationOutput,Derivable,flatDerivation
99 | from clausesets import ClauseSet
100 | from clauses import firstLit, varSizeLit, eqResVarSizeLit
101 | from fofspec import FOFSpec
102 | from heuristics import GivenClauseHeuristics
103 | from saturation import SearchParams,ProofState
104 | from litselection import LiteralSelectors
105 |
106 |
107 | suppressEqAxioms = False
108 | silent = False
109 | indexed = False
110 | proofObject = False
111 |
112 | def processOptions(opts):
113 | """
114 | Process the options given
115 | """
116 | global silent, indexed, suppressEqAxioms, proofObject
117 |
118 | params = SearchParams()
119 | for opt, optarg in opts:
120 | if opt == "-h" or opt == "--help":
121 | print("pyres-fof.py "+version)
122 | print(__doc__)
123 | sys.exit()
124 | elif opt=="-s" or opt == "--silent":
125 | silent = True
126 | elif opt=="-V" or opt == "--version":
127 | print("% Version: ", version)
128 | elif opt=="-p" or opt == "--proof":
129 | proofObject = True
130 | elif opt=="-i" or opt == "--index":
131 | indexed = True
132 | elif opt=="-t" or opt == "--delete-tautologies":
133 | params.delete_tautologies = True
134 | elif opt=="-f" or opt == "--forward-subsumption":
135 | params.forward_subsumption = True
136 | elif opt=="-b" or opt == "--backward-subsumption":
137 | params.backward_subsumption = True
138 | elif opt=="-H" or opt == "--given-clause-heuristic":
139 | try:
140 | params.heuristics = GivenClauseHeuristics[optarg]
141 | except KeyError:
142 | print("Unknown clause evaluation function", optarg)
143 | print("Supported:", GivenClauseHeuristics.keys())
144 | sys.exit(1)
145 | elif opt=="-n" or opt == "--neg-lit-selection":
146 | try:
147 | params.literal_selection = LiteralSelectors[optarg]
148 | except KeyError:
149 | print("Unknown literal selection function", optarg)
150 | print("Supported:", LiteralSelectors.keys())
151 | sys.exit(1)
152 | elif opt=="-S" or opt=="--suppress-eq-axioms":
153 | suppressEqAxioms = True
154 |
155 | return params
156 |
157 | def timeoutHandler(sign, frame):
158 | """
159 | This will be called if the process receives a SIGXCPU error. In
160 | that case, we print an informative message before terminating. We
161 | expect this signal from the benchmark environment (typically
162 | StarExec).
163 | """
164 | print("% Failure: Resource limit exceeded (time)")
165 | print("% SZS status ResourceOut")
166 | sys.exit(0)
167 |
168 |
169 | if __name__ == '__main__':
170 | # We try to increase stack space, since we use a lot of
171 | # recursion. This works differentially well on different OSes, so
172 | # it is a bit more complex than I would hope for.
173 | try:
174 | soft, hard = getrlimit(RLIMIT_STACK)
175 | soft = 10*soft
176 | if hard > 0 and soft > hard:
177 | soft = hard
178 | setrlimit(RLIMIT_STACK, (soft, hard))
179 | except ValueError:
180 | # For reasons nobody understands, this seems to fail on
181 | # OS-X. In that case, we just do our best...
182 | pass
183 |
184 | signal(SIGXCPU, timeoutHandler)
185 | sys.setrecursionlimit(10000)
186 |
187 | try:
188 | opts, args = getopt.gnu_getopt(sys.argv[1:],
189 | "hsVpitfbH:n:S",
190 | ["help",
191 | "silent",
192 | "version",
193 | "proof",
194 | "index",
195 | "delete-tautologies",
196 | "forward-subsumption",
197 | "backward-subsumption"
198 | "given-clause-heuristic=",
199 | "neg-lit-selection="
200 | "supress-eq-axioms"])
201 | except getopt.GetoptError as err:
202 | print(sys.argv[0],":", err)
203 | sys.exit(1)
204 |
205 | params = processOptions(opts)
206 |
207 | problem = FOFSpec()
208 | for file in args:
209 | problem.parse(file)
210 |
211 | if not suppressEqAxioms:
212 | problem.addEqAxioms()
213 | cnf = problem.clausify()
214 |
215 | state = ProofState(params, cnf, silent, indexed)
216 | res = state.saturate()
217 |
218 | if res != None:
219 | if problem.isFof and problem.hasConj:
220 | print("% SZS status Theorem")
221 | else:
222 | print("% SZS status Unsatisfiable")
223 | if proofObject:
224 | proof = res.orderedDerivation()
225 | enableDerivationOutput()
226 | print("% SZS output start CNFRefutation")
227 | for s in proof:
228 | print(s)
229 | print("% SZS output end CNFRefutation")
230 | disableDerivationOutput()
231 | else:
232 | if problem.isFof and problem.hasConj:
233 | print("% SZS status CounterSatisfiable")
234 | else:
235 | print("% SZS status Satisfiable")
236 | if proofObject:
237 | dummy = Derivable("dummy",
238 | flatDerivation("pseudoreference",
239 | state.processed.clauses))
240 | sat = dummy.orderedDerivation()
241 | enableDerivationOutput()
242 | print("% SZS output start Saturation")
243 | for s in sat[:-1]:
244 | print(s)
245 | print("% SZS output end Saturation")
246 | disableDerivationOutput()
247 | print(state.statisticsStr())
248 |
249 | # We use the resources interface to get and print the CPU time
250 | resources = getrusage(RUSAGE_SELF)
251 | print("% -------- CPU Time ---------")
252 | print("%% User time : %.3f s"%(resources.ru_utime,))
253 | print("%% System time : %.3f s"%(resources.ru_stime,))
254 | print("%% Total time : %.3f s"%(resources.ru_utime+
255 | resources.ru_stime,))
256 |
257 |
--------------------------------------------------------------------------------
/pyres-simple.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # ----------------------------------
3 | #
4 | # Module pyres-simple.py
5 |
6 | """
7 | Usage: pyres-simple.py
8 |
9 | This is a straightforward implementation of a simple resolution-based
10 | prover for first-order clausal logic. Problem files should be in
11 | (restricted) TPTP-3 CNF syntax. Unsupported features include double
12 | quoted strings and include file. Equality is parsed, but not
13 | interpreted.
14 |
15 | Options:
16 |
17 | -h
18 | --help
19 | Print this help.
20 |
21 | Copyright 2011-2020 Stephan Schulz, schulz@eprover.org
22 |
23 | This program is free software; you can redistribute it and/or modify
24 | it under the terms of the GNU General Public License as published by
25 | the Free Software Foundation; either version 2 of the License, or
26 | (at your option) any later version.
27 |
28 | This program is distributed in the hope that it will be useful,
29 | but WITHOUT ANY WARRANTY; without even the implied warranty of
30 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
31 | GNU General Public License for more details.
32 |
33 | You should have received a copy of the GNU General Public License
34 | along with this program ; if not, write to the Free Software
35 | Foundation, Inc., 59 Temple Place, Suite 330, Boston,
36 | MA 02111-1307 USA
37 |
38 | The original copyright holder can be contacted as
39 |
40 | Stephan Schulz
41 | Auf der Altenburg 7
42 | 70376 Stuttgart
43 | Germany
44 | Email: schulz@eprover.org
45 |
46 | """
47 |
48 | import sys
49 | import getopt
50 | from version import version
51 | from lexer import Token,Lexer
52 | from clausesets import ClauseSet
53 | from simplesat import SimpleProofState
54 |
55 |
56 | def processOptions(opts):
57 | """
58 | Process the options given
59 | """
60 | for opt, optarg in opts:
61 | if opt == "-h" or opt == "--help":
62 | print("pyres-simple.py "+version)
63 | print(__doc__)
64 | sys.exit()
65 |
66 | if __name__ == '__main__':
67 | try:
68 | opts, args = getopt.gnu_getopt(sys.argv[1:],
69 | "h",
70 | ["help"])
71 | except getopt.GetoptError as err:
72 | print(sys.argv[0],":", err)
73 | sys.exit(1)
74 |
75 | processOptions(opts)
76 |
77 | problem = ClauseSet()
78 | for file in args:
79 | fp = open(file, "r")
80 | input = fp.read()
81 | fp.close()
82 | lex = Lexer(input)
83 | problem.parse(lex)
84 |
85 | state = SimpleProofState(problem)
86 | res = state.saturate()
87 |
88 | if res != None:
89 | print("% SZS status Unsatisfiable")
90 | else:
91 | print("% SZS status Satisfiable")
92 |
--------------------------------------------------------------------------------
/rescontrol.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # ----------------------------------
3 | #
4 | # Module rescontrol.py
5 |
6 | """
7 | Functions wrapping basic inference rules for convenience.
8 |
9 |
10 | Copyright 2010-2019 Stephan Schulz, schulz@eprover.org
11 |
12 | This program is free software; you can redistribute it and/or modify
13 | it under the terms of the GNU General Public License as published by
14 | the Free Software Foundation; either version 2 of the License, or
15 | (at your option) any later version.
16 |
17 | This program is distributed in the hope that it will be useful,
18 | but WITHOUT ANY WARRANTY; without even the implied warranty of
19 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 | GNU General Public License for more details.
21 |
22 | You should have received a copy of the GNU General Public License
23 | along with this program ; if not, write to the Free Software
24 | Foundation, Inc., 59 Temple Place, Suite 330, Boston,
25 | MA 02111-1307 USA
26 |
27 | The original copyright holder can be contacted as
28 |
29 | Stephan Schulz
30 | Auf der Altenburg 7
31 | 70376 Stuttgart
32 | Germany
33 | Email: schulz@eprover.org
34 | """
35 |
36 | import unittest
37 | from lexer import Token,Lexer
38 | from resolution import resolution, factor
39 | from clauses import parseClause
40 | from clausesets import ClauseSet
41 |
42 |
43 | def computeAllResolvents(clause, clauseset):
44 | """
45 | Compute all binary resolvents between a given clause and all
46 | clauses in clauseset.
47 |
48 | In the "given-clause algorithm", the proof state is represented by
49 | two sets of clauses, the set of _processed_ clauses, and the set
50 | of _unprocessed_ clauses. Originally, all clauses are
51 | unprocessed. The main invariant is that at all times, all the
52 | generating inferences between processed clauses have been computed
53 | and added to the proof state. The algorithm moves one clause at a
54 | time from unprocessed, and adds it to processed (unless the clause
55 | is redundant).
56 |
57 | This function is used when integrating a new clause into the
58 | processed part of the proof state. It computes all the resolvents
59 | between a single clause (the new "given clause") and a clause set
60 | (the _processed clauses_). These clauses need to be added to the
61 | proof state to maintain the invariant. Since they are new, they
62 | will be added to the set of unprocessed clauses.
63 | """
64 | res = []
65 | for lit in range(len(clause)):
66 | if clause.getLiteral(lit).isInferenceLit():
67 | partners = \
68 | clauseset.getResolutionLiterals(clause.getLiteral(lit))
69 | for (cl2, lit2) in partners:
70 | resolvent = resolution(clause, lit, cl2, lit2)
71 | if resolvent!=None:
72 | res.append(resolvent)
73 | return res
74 |
75 |
76 | def computeAllFactors(clause):
77 | """
78 | Compute all (direct) factors of clause. This operation is O(n^2)
79 | if n is the number of literals. However, factoring is nearly never
80 | a critical operation. Single-clause operations are nearly always
81 | much cheaper than clause/clause-set operations.
82 | """
83 | res = []
84 | for i in range(len(clause)):
85 | for j in range(i+1, len(clause)):
86 | if clause.getLiteral(i).isInferenceLit() or \
87 | clause.getLiteral(j).isInferenceLit():
88 | fact = factor(clause, i, j)
89 | if fact:
90 | res.append(fact)
91 | return res
92 |
93 |
94 | class TestSetInferences(unittest.TestCase):
95 | """
96 | Unit test class for simple resolution inference control.
97 | """
98 | def setUp(self):
99 | """
100 | Setup function for clause/literal unit tests. Initialize
101 | variables needed throughout the tests.
102 | """
103 | print()
104 | spec = """
105 | cnf(g1, negated_conjecture, ~c).
106 | cnf(c1, axiom, a|b|c).
107 | cnf(c2, axiom, b|c).
108 | cnf(c3, axiom, c).
109 | """
110 | lex = Lexer(spec)
111 | self.conj = parseClause(lex)
112 | self.cset = ClauseSet()
113 | self.cset.parse(lex)
114 |
115 | cstr = "cnf(ftest, axiom, p(X)|~q|p(a)|~q|p(Y))."
116 | lex = Lexer(cstr)
117 | self.fclause = parseClause(lex)
118 |
119 | def testSetResolution(self):
120 | """
121 | Test that forming resolvents between a clause and a clause set
122 | works.
123 | """
124 | print("Test set resolution")
125 | res = computeAllResolvents(self.conj, self.cset)
126 | print(res)
127 |
128 |
129 | def testFactoring(self):
130 | """
131 | Test full factoring of a clause.
132 | """
133 | print("Test factoring")
134 | res = computeAllFactors(self.fclause)
135 | print(res)
136 |
137 |
138 | if __name__ == '__main__':
139 | unittest.main()
140 |
--------------------------------------------------------------------------------
/resolution.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # ----------------------------------
3 | #
4 | # Module resolution.py
5 |
6 | """
7 | This module implements the rules of the simple resolution calculus,
8 | namely binary resolution and factoring.
9 | inference rule:
10 |
11 | Binary resolution:
12 |
13 | c1|a1 c2|~a2
14 | ---------------- where sigma=mgu(a1,a2)
15 | sigma(c1|c2)
16 |
17 | Note that c1 and c2 are arbitrary disjunctions of literals (each of
18 | which may be positive or negative). Both c1 and c2 may be empty. Both
19 | a1 and a2 are atoms (so a1 and ~a2 are a positive and a negative
20 | literal, respectively). Also, since | is AC (or, alternatively, the
21 | clauses are unordered multisets), the order of literals is irrelevant.
22 |
23 | Clauses are interpreted as implicitly universally quantified
24 | disjunctions of literals. This implies that the scope of the variables
25 | is a single clause. In other words, from a theoretical point of view,
26 | variables in different clauses are different. In practice, we have to
27 | enforce this explicitly by making sure that all clauses used as
28 | premises in an inference are indeed variable disjoint.
29 |
30 |
31 | Factoring:
32 |
33 | c|a|b
34 | ---------- where sigma = mgu(a,b)
35 | sigma(c|a)
36 |
37 | Again, c is an arbitray disjunction.
38 |
39 |
40 | Copyright 2010-2019 Stephan Schulz, schulz@eprover.org
41 |
42 | This program is free software; you can redistribute it and/or modify
43 | it under the terms of the GNU General Public License as published by
44 | the Free Software Foundation; either version 2 of the License, or
45 | (at your option) any later version.
46 |
47 | This program is distributed in the hope that it will be useful,
48 | but WITHOUT ANY WARRANTY; without even the implied warranty of
49 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
50 | GNU General Public License for more details.
51 |
52 | You should have received a copy of the GNU General Public License
53 | along with this program ; if not, write to the Free Software
54 | Foundation, Inc., 59 Temple Place, Suite 330, Boston,
55 | MA 02111-1307 USA
56 |
57 | The original copyright holder can be contacted as
58 |
59 | Stephan Schulz
60 | Auf der Altenburg 7
61 | 70376 Stuttgart
62 | Germany
63 | Email: schulz@eprover.org
64 | """
65 |
66 | import unittest
67 | from lexer import Lexer
68 | import substitutions
69 | from unification import mgu
70 | from literals import Literal
71 | from derivations import flatDerivation
72 | import clauses
73 |
74 |
75 | def resolution(clause1, lit1, clause2, lit2):
76 | """
77 | Implementation of the Resolution rule. lit1 and lit2 are indices
78 | of literals in clause1 and clause2, respectively, so clause1|lit1
79 | and clause2|lit2 are literals.
80 |
81 | Try to resolve clause1|lit1 against clause2|lit2. If this is
82 | possible, return the resolvent. Otherwise, return None.
83 | """
84 | l1 = clause1.getLiteral(lit1)
85 | l2 = clause2.getLiteral(lit2)
86 | if l1.isNegative() == l2.isNegative():
87 | return None
88 | sigma = mgu(l1.atom, l2.atom)
89 | if sigma == None:
90 | return None
91 | lits1 = [l.instantiate(sigma) for l in clause1.literals if l!=l1]
92 | lits2 = [l.instantiate(sigma) for l in clause2.literals if l!=l2]
93 | lits1.extend(lits2)
94 | res = clauses.Clause(lits1)
95 | res.removeDupLits()
96 | res.setDerivation(flatDerivation("resolution", [clause1, clause2]))
97 | return res
98 |
99 |
100 | def factor(clause, lit1, lit2):
101 | """
102 | Check if it is possible to form a factor between lit1 and lit2. If
103 | yes, return it, otherwise return None.
104 | """
105 | l1 = clause.getLiteral(lit1)
106 | l2 = clause.getLiteral(lit2)
107 | if l1.isNegative() != l2.isNegative():
108 | return None
109 | sigma = mgu(l1.atom, l2.atom)
110 | if sigma == None:
111 | return None
112 | lits = [l.instantiate(sigma) for l in clause.literals if l!=l2]
113 | res = clauses.Clause(lits)
114 | res.removeDupLits()
115 | res.setDerivation(flatDerivation("factor", [clause]))
116 | return res
117 |
118 |
119 |
120 |
121 | class TestResolution(unittest.TestCase):
122 | """
123 | Unit test class for clauses. Test clause and literal
124 | functionality.
125 | """
126 | def setUp(self):
127 | """
128 | Setup function for resolution testing
129 | """
130 | print()
131 | self.spec = """
132 | cnf(c1,axiom,p(a, X)|p(X,a)).
133 | cnf(c2,axiom,~p(a,b)|p(f(Y),a)).
134 | cnf(c3,axiom,p(Z,X)|~p(f(Z),X0)).
135 | cnf(c4,axiom,p(X,X)|p(a,f(Y))).
136 | cnf(c5,axiom,p(X)|~q|p(a)|~q|p(Y)).
137 | cnf(not_p,axiom,~p(a)).
138 | cnf(taut,axiom,p(X4)|~p(X4)).
139 | """
140 | lex = Lexer(self.spec)
141 | self.c1 = clauses.parseClause(lex)
142 | self.c2 = clauses.parseClause(lex)
143 | self.c3 = clauses.parseClause(lex)
144 | self.c4 = clauses.parseClause(lex)
145 | self.c5 = clauses.parseClause(lex)
146 | self.c6 = clauses.parseClause(lex)
147 | self.c7 = clauses.parseClause(lex)
148 |
149 | def testResolution(self):
150 | """
151 | Test resolution
152 | """
153 | print("Resolution:")
154 | res1 = resolution(self.c1, 0, self.c2,0)
155 | self.assertTrue(res1)
156 | print(res1)
157 |
158 | res2 = resolution(self.c1, 0, self.c3,0)
159 | self.assertTrue(res2==None)
160 | print(res2)
161 |
162 | res3 = resolution(self.c2, 0, self.c3,0)
163 | self.assertTrue(res3)
164 | print(res3)
165 |
166 | res4 = resolution(self.c1, 0, self.c3,1)
167 | self.assertTrue(res4==None)
168 | print(res4)
169 |
170 | res5 = resolution(self.c6, 0, self.c7,0)
171 | self.assertTrue(res5)
172 | print(res5)
173 |
174 | def testFactoring(self):
175 | """
176 | Test the factoring inference.
177 | """
178 | f1 = factor(self.c1,0,1)
179 | self.assertTrue(f1)
180 | self.assertTrue(len(f1)==1)
181 | print("Factor:", f1)
182 |
183 | f2 = factor(self.c2,0,1)
184 | self.assertTrue(f2==None)
185 | print(f2)
186 |
187 | f4 = factor(self.c4,0,1)
188 | self.assertTrue(f4==None)
189 | print(f4)
190 |
191 | f5 = factor(self.c5,1,3)
192 | self.assertTrue(f5)
193 | print(f5)
194 |
195 | if __name__ == '__main__':
196 | unittest.main()
197 |
--------------------------------------------------------------------------------
/saturation.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # ----------------------------------
3 | #
4 | # Module saturation.py
5 |
6 | """
7 | Implementation of the given-clause algorithm for saturation of clause
8 | sets under the rules of the resolution calculus. This improves on the
9 | very basic implementation in simplesat in several ways.
10 |
11 | - It supports heuristic clause selection, not just first-in first-out
12 | - It supports tautology deletion
13 | - It supports forward and backwards subsumption
14 | - It keeps some statistics to enable the user to understand the
15 | practical impact of different steps of the algorithm better.
16 |
17 | Most of these changes can be found in the function processClause() of
18 | the ProofState class.
19 |
20 | Copyright 2011-2019 Stephan Schulz, schulz@eprover.org
21 |
22 | This program is free software; you can redistribute it and/or modify
23 | it under the terms of the GNU General Public License as published by
24 | the Free Software Foundation; either version 2 of the License, or
25 | (at your option) any later version.
26 |
27 | This program is distributed in the hope that it will be useful,
28 | but WITHOUT ANY WARRANTY; without even the implied warranty of
29 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
30 | GNU General Public License for more details.
31 |
32 | You should have received a copy of the GNU General Public License
33 | along with this program ; if not, write to the Free Software
34 | Foundation, Inc., 59 Temple Place, Suite 330, Boston,
35 | MA 02111-1307 USA
36 |
37 | The original copyright holder can be contacted as
38 |
39 | Stephan Schulz
40 | Auf der Altenburg 7
41 | 70376 Stuttgart
42 | Germany
43 | Email: schulz@eprover.org
44 | """
45 |
46 | import unittest
47 | from idents import Ident
48 | from lexer import Token,Lexer
49 | from clausesets import ClauseSet, HeuristicClauseSet, IndexedClauseSet
50 | import heuristics
51 | from litselection import largestLit
52 | from rescontrol import computeAllResolvents, computeAllFactors
53 | from subsumption import forwardSubsumption, backwardSubsumption
54 |
55 |
56 | class SearchParams(object):
57 | """
58 | A simple container for different parameter settings for the proof
59 | search.
60 | """
61 | def __init__(self,
62 | heuristics = heuristics.PickGiven5,
63 | delete_tautologies = False,
64 | forward_subsumption = False,
65 | backward_subsumption = False,
66 | literal_selection = None):
67 | """
68 | Initialize heuristic parameters.
69 | """
70 | self.heuristics = heuristics
71 | """
72 | This defines the clause selection heuristic, i.e. the order in
73 | which uprocessed clauses are selected for processing.
74 | """
75 | self.delete_tautologies = delete_tautologies
76 | """
77 | This determines if tautologies will be deleted. Tautologies in
78 | plain first-order logic (without equality) are clauses which
79 | contain two literals with the same atom, but opposite signs.
80 | """
81 | self.forward_subsumption = forward_subsumption
82 | """
83 | Forward-subsumption checks the given clause against already
84 | processed clauses, and discards it if it is subsumed.
85 | """
86 | self.backward_subsumption = backward_subsumption
87 | """
88 | Backwars subsumption checks the processed clauses against the
89 | given clause, and discards all processed clauses that are
90 | subsumed.
91 | """
92 | self.literal_selection = literal_selection
93 | """
94 | Either None, or a function that selects a subset of negative
95 | literals from a set of negative literals (both represented as
96 | lists, not Python sets) as the inference literal.
97 | """
98 |
99 |
100 |
101 | class ProofState(object):
102 | """
103 | Top-level data structure for the prover. The complete knowledge
104 | base is split into two sets, processed clauses and unprocessed
105 | clauses. These are represented here as individual clause sets. The
106 | main algorithm "processes" clauses and moves them from the
107 | unprocessed into the processed set. Processing typically generates
108 | several new clauses, which are direct consequences of the given
109 | clause and the processed claues. These new clauses are added to
110 | the set of unprocessed clauses.
111 |
112 | In addition to the clause sets, this data structure also maintains
113 | a number of counters for statistics on the proof search.
114 | """
115 | def __init__(self, params, clauses, silent=False, indexed=False):
116 | """
117 | Initialize the proof state with a set of clauses.
118 | """
119 | self.params = params
120 | self.unprocessed = HeuristicClauseSet(params.heuristics)
121 |
122 | if indexed:
123 | self.processed = IndexedClauseSet()
124 | else:
125 | self.processed = ClauseSet()
126 | for c in clauses.clauses:
127 | self.unprocessed.addClause(c)
128 | self.initial_clause_count = len(self.unprocessed)
129 | self.proc_clause_count = 0
130 | self.factor_count = 0
131 | self.resolvent_count = 0
132 | self.tautologies_deleted = 0
133 | self.forward_subsumed = 0
134 | self.backward_subsumed = 0
135 | self.silent = silent
136 |
137 | def processClause(self):
138 | """
139 | Pick a clause from unprocessed and process it. If the empty
140 | clause is found, return it. Otherwise return None.
141 | """
142 | given_clause = self.unprocessed.extractBest()
143 | given_clause = given_clause.freshVarCopy()
144 | if not self.silent:
145 | print("%")
146 | if given_clause.isEmpty():
147 | # We have found an explicit contradiction
148 | return given_clause
149 | if self.params.delete_tautologies and \
150 | given_clause.isTautology():
151 | self.tautologies_deleted += 1
152 | return None
153 | if self.params.forward_subsumption and \
154 | forwardSubsumption(self.processed, given_clause):
155 | # If the given clause is subsumed by an already processed
156 | # clause, all releveant inferences will already have been
157 | # done with that more general clause. So we can discard
158 | # the given clause. We do keep count of how many clauses
159 | # we have dropped this way.
160 | self.forward_subsumed += 1
161 | return None
162 |
163 | if self.params.backward_subsumption:
164 | # If the given clause subsumes any of the already
165 | # processed clauses, it will "cover" for these less
166 | # general clauses in the future, so we can remove them
167 | # from the proof state. Again, we keep count of the number
168 | # of clauses dropped. This typically happens less often
169 | # than forward subsumption, because most heuristics prefer
170 | # smaller clauses, which tend to be more general (thus the
171 | # processed clauses are typically if not universally more
172 | # general than the new given clause).
173 | tmp = backwardSubsumption(given_clause, self.processed)
174 | self.backward_subsumed = self.backward_subsumed+tmp
175 |
176 | if(self.params.literal_selection):
177 | given_clause.selectInferenceLits(self.params.literal_selection)
178 | if not self.silent:
179 | print("%", given_clause)
180 | new = []
181 | factors = computeAllFactors(given_clause)
182 | new.extend(factors)
183 | resolvents = computeAllResolvents(given_clause, self.processed)
184 | new.extend(resolvents)
185 | self.proc_clause_count = self.proc_clause_count+1
186 | self.factor_count = self.factor_count+len(factors)
187 | self.resolvent_count = self.resolvent_count+len(resolvents)
188 |
189 | self.processed.addClause(given_clause)
190 |
191 | for c in new:
192 | self.unprocessed.addClause(c)
193 | return None
194 |
195 | def saturate(self):
196 | """
197 | Main proof procedure. If the clause set is found
198 | unsatisfiable, return the empty clause as a witness. Otherwise
199 | return None.
200 | """
201 | while self.unprocessed:
202 | res = self.processClause()
203 | if res != None:
204 | return res
205 | else:
206 | return None
207 |
208 | def statisticsStr(self):
209 | """
210 | Return the proof state statistics in string form ready for
211 | output.
212 | """
213 | return """
214 | %% Initial clauses : %d
215 | %% Processed clauses : %d
216 | %% Factors computed : %d
217 | %% Resolvents computed: %d
218 | %% Tautologies deleted: %d
219 | %% Forward subsumed : %d
220 | %% Backward subsumed : %d""" \
221 | %(self.initial_clause_count,
222 | self.proc_clause_count,
223 | self.factor_count,
224 | self.resolvent_count,
225 | self.tautologies_deleted,
226 | self.forward_subsumed,
227 | self.backward_subsumed)
228 |
229 |
230 | class TestProver(unittest.TestCase):
231 | """
232 | Unit test class for simple resolution inference control.
233 | """
234 | def setUp(self):
235 | """
236 | Setup function for clause/literal unit tests. Initialize
237 | variables needed throughout the tests.
238 | """
239 | print()
240 | self.params = SearchParams()
241 | self.params.delete_tautologies = True
242 | self.params.backward_subsumption = True
243 | self.params.forward_subsumption = True
244 | self.spec1 = """
245 | cnf(axiom, a_is_true, a).
246 | cnf(negated_conjecture, is_a_true, ~a)."""
247 |
248 | self.spec2 = """
249 | %------------------------------------------------------------------------------
250 | % File : PUZ001-1 : TPTP v4.1.0. Released v1.0.0.
251 | % Domain : Puzzles
252 | % Problem : Dreadbury Mansion
253 | % Version : Especial.
254 | % Theorem formulation : Made unsatisfiable.
255 | % English : Someone who lives in Dreadbury Mansion killed Aunt Agatha.
256 | % Agatha, the butler, and Charles live in Dreadbury Mansion,
257 | % and are the only people who live therein. A killer always
258 | % hates his victim, and is never richer than his victim.
259 | % Charles hates no one that Aunt Agatha hates. Agatha hates
260 | % everyone except the butler. The butler hates everyone not
261 | % richer than Aunt Agatha. The butler hates everyone Aunt
262 | % Agatha hates. No one hates everyone. Agatha is not the
263 | % butler. Therefore : Agatha killed herself.
264 |
265 | % Refs : [Pel86] Pelletier (1986), Seventy-five Problems for Testing Au
266 | % : [MB88] Manthey & Bry (1988), SATCHMO: A Theorem Prover Implem
267 | % Source : [TPTP]
268 | % Names :
269 |
270 | % Status : Unsatisfiable
271 | % Rating : 0.00 v2.0.0
272 | % Syntax : Number of clauses : 12 ( 2 non-Horn; 5 unit; 12 RR)
273 | % Number of atoms : 21 ( 0 equality)
274 | % Maximal clause size : 3 ( 2 average)
275 | % Number of predicates : 4 ( 0 propositional; 1-2 arity)
276 | % Number of functors : 3 ( 3 constant; 0-0 arity)
277 | % Number of variables : 8 ( 0 singleton)
278 | % Maximal term depth : 1 ( 1 average)
279 | % SPC : CNF_UNS_EPR
280 |
281 | % Comments : Modified from the [MB88] version to be unsatisfiable, by Geoff
282 | % Sutcliffe.
283 | % : Also known as "Who killed Aunt Agatha"
284 | %------------------------------------------------------------------------------
285 | cnf(agatha,hypothesis,
286 | ( lives(agatha) )).
287 |
288 | cnf(butler,hypothesis,
289 | ( lives(butler) )).
290 |
291 | cnf(charles,hypothesis,
292 | ( lives(charles) )).
293 |
294 | cnf(poorer_killer,hypothesis,
295 | ( ~ killed(X,Y)
296 | | ~ richer(X,Y) )).
297 |
298 | cnf(different_hates,hypothesis,
299 | ( ~ hates(agatha,X)
300 | | ~ hates(charles,X) )).
301 |
302 | cnf(no_one_hates_everyone,hypothesis,
303 | ( ~ hates(X,agatha)
304 | | ~ hates(X,butler)
305 | | ~ hates(X,charles) )).
306 |
307 | cnf(agatha_hates_agatha,hypothesis,
308 | ( hates(agatha,agatha) )).
309 |
310 | cnf(killer_hates_victim,hypothesis,
311 | ( ~ killed(X,Y)
312 | | hates(X,Y) )).
313 |
314 | cnf(same_hates,hypothesis,
315 | ( ~ hates(agatha,X)
316 | | hates(butler,X) )).
317 |
318 | cnf(agatha_hates_charles,hypothesis,
319 | ( hates(agatha,charles) )).
320 |
321 | cnf(butler_hates_poor,hypothesis,
322 | ( ~ lives(X)
323 | | richer(X,agatha)
324 | | hates(butler,X) )).
325 |
326 | %----Literal dropped from here to make it unsatisfiable
327 | cnf(prove_neither_charles_nor_butler_did_it,negated_conjecture,
328 | ( killed(butler,agatha)
329 | | killed(charles,agatha) )).
330 |
331 | %------------------------------------------------------------------------------
332 | """
333 |
334 | self.spec3 = """
335 | cnf(p_or_q, axiom, p(X)|q(a)).
336 | cnf(taut, axiom, p(X)|~p(X)).
337 | cnf(not_p, axiom, ~p(a)).
338 | """
339 |
340 | def evalSatResult(self, spec, provable, indexed=False):
341 | """
342 | Evaluate the result of a saturation compared to the expected
343 | result.
344 | """
345 |
346 | lex = Lexer(spec)
347 | problem = ClauseSet()
348 | problem.parse(lex)
349 |
350 | prover = ProofState(self.params, problem, False, indexed)
351 | res = prover.saturate()
352 |
353 | if provable:
354 | self.assertNotEqual(res, None)
355 | if res == None: # pragma: nocover
356 | print("% Bug: Should have found a proof!")
357 | else:
358 | print("% Proof found")
359 | else:
360 | self.assertEqual(res, None)
361 | if res != None: # pragma: nocover
362 | print("% Bug: Should not have found a proof!")
363 | else:
364 | print("% No proof found")
365 |
366 | print(prover.statisticsStr())
367 |
368 | def testSaturation(self):
369 | """
370 | Test that saturation works.
371 | """
372 | self.evalSatResult(self.spec1, True)
373 | self.evalSatResult(self.spec2, True)
374 | self.evalSatResult(self.spec3, False)
375 |
376 | self.params.literal_selection = largestLit
377 | self.evalSatResult(self.spec1, True, True)
378 | self.evalSatResult(self.spec2, True, True)
379 | self.evalSatResult(self.spec3, False, True)
380 |
381 |
382 | def testParamSet(self):
383 | """
384 | Test that parameter setting code works.
385 | """
386 | pm = SearchParams()
387 | self.assertEqual(pm.heuristics, heuristics.PickGiven5)
388 | self.assertEqual(pm.delete_tautologies, False)
389 | self.assertEqual(pm.forward_subsumption, False)
390 | self.assertEqual(pm.backward_subsumption, False)
391 |
392 | if __name__ == '__main__':
393 | unittest.main()
394 |
--------------------------------------------------------------------------------
/signature.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # ----------------------------------
3 | #
4 | # Module signature.py
5 |
6 | """
7 | First-order signatures describe which names (for functions, including
8 | constants, and predicates) are available in a given first-order
9 | language. Very often, signatures are given implicitly. In other words,
10 | the symbols used in terms and formulas implictly make up the
11 | signature. For implementations of standard untyped predicate logic, we
12 | can always extract the necessary information directly from the
13 | formulae.
14 |
15 | However, for certain operations it is much easier to have an explicit
16 | data object providing signature information.
17 |
18 | A signature is a triple (F,P,ar), with the following properties:
19 |
20 | - F is a finite set of function symbols (including constants).
21 | - P is a finite set of predicate symbols.
22 | - F and P are disjunct, i.e. they don't share any symbols.
23 | - ar:F \cup P ->N_0 is the arity function that associates a natural
24 | number (the "arity") with each function symbol and predicate
25 | symbols.
26 |
27 |
28 | Copyright 2012-2019 Stephan Schulz, schulz@eprover.org
29 |
30 | This program is free software; you can redistribute it and/or modify
31 | it under the terms of the GNU General Public License as published by
32 | the Free Software Foundation; either version 2 of the License, or
33 | (at your option) any later version.
34 |
35 | This program is distributed in the hope that it will be useful,
36 | but WITHOUT ANY WARRANTY; without even the implied warranty of
37 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
38 | GNU General Public License for more details.
39 |
40 | You should have received a copy of the GNU General Public License
41 | along with this program ; if not, write to the Free Software
42 | Foundation, Inc., 59 Temple Place, Suite 330, Boston,
43 | MA 02111-1307 USA
44 |
45 | The original copyright holder can be contacted as
46 |
47 | Stephan Schulz
48 | Auf der Altenburg 7
49 | 70376 Stuttgart
50 | Germany
51 | Email: schulz@eprover.org
52 | """
53 |
54 | import unittest
55 |
56 | class Signature(object):
57 | """
58 | A signature object, containing function symbols, predicate
59 | symbols, and their associated arities.
60 | """
61 | def __init__(self):
62 | """
63 | Initialize the signature.
64 | """
65 | self.funs = {}
66 | self.preds = {}
67 |
68 | def __repr__(self):
69 | """
70 | Return a printable representation of the signture.
71 | """
72 | res = ["Predicates:\n-----------"]
73 | funs = [ "%s: %d"%(f, self.preds[f]) for f in self.preds.keys()]
74 | res.extend(funs)
75 |
76 | res.append("Functions:\n-----------")
77 | funs = [ "%s: %d"%(f, self.funs[f]) for f in self.funs.keys()]
78 | res.extend(funs)
79 |
80 | return "\n".join(res)
81 |
82 | def addFun(self, f, arity):
83 | """
84 | Add a function symbol with associated arity.
85 | """
86 | self.funs[f] = arity
87 |
88 |
89 | def addPred(self, p, arity):
90 | """
91 | Add a predicate symbol with associated arity.
92 | """
93 | self.preds[p] = arity
94 |
95 | def isPred(self, p):
96 | """
97 | Return True if p is a known predicate symbol.
98 | """
99 | return p in self.preds
100 |
101 | def isFun(self, f):
102 | """
103 | Return True if f is a known function symbol.
104 | """
105 | return f in self.funs
106 |
107 | def isConstant(self,f):
108 | """
109 | Return True if f is a constant function symbol.
110 | """
111 | return self.isFun(f) and self.getArity(f)==0
112 |
113 | def getArity(self, symbol):
114 | """
115 | Return the arity of a (known) symbol.
116 | """
117 | if self.isFun(symbol):
118 | return self.funs[symbol]
119 | return self.preds[symbol]
120 |
121 |
122 |
123 | class TestSignature(unittest.TestCase):
124 | """
125 | Test basic functionality of the signature data type.
126 | """
127 |
128 | def testSig(self):
129 | """
130 | Test signature object.
131 | """
132 | sig = Signature()
133 |
134 | sig.addFun("mult", 2)
135 | sig.addFun("a", 0)
136 | sig.addPred("weird", 4)
137 |
138 |
139 | print(sig)
140 | self.assertTrue(sig.isPred("weird"))
141 | self.assertTrue(not sig.isPred("unknown"))
142 | self.assertTrue(not sig.isPred("a"))
143 | self.assertTrue(sig.isFun("a"))
144 | self.assertTrue(sig.isConstant("a"))
145 | self.assertTrue(not sig.isFun("unknown"))
146 | self.assertTrue(not sig.isFun("weird"))
147 |
148 | self.assertEqual(sig.getArity("a"),0)
149 | self.assertEqual(sig.getArity("weird"),4)
150 |
151 |
152 | if __name__ == '__main__':
153 | unittest.main()
154 |
--------------------------------------------------------------------------------
/simplesat.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # ----------------------------------
3 | #
4 | # Module simplesat.py
5 |
6 | """
7 | Minimalistic implementation of the given-clause algorithm for
8 | saturation of clause sets under the rules of the resolution calculus.
9 |
10 | Copyright 2011-2019 Stephan Schulz, schulz@eprover.org
11 |
12 | This program is free software; you can redistribute it and/or modify
13 | it under the terms of the GNU General Public License as published by
14 | the Free Software Foundation; either version 2 of the License, or
15 | (at your option) any later version.
16 |
17 | This program is distributed in the hope that it will be useful,
18 | but WITHOUT ANY WARRANTY; without even the implied warranty of
19 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 | GNU General Public License for more details.
21 |
22 | You should have received a copy of the GNU General Public License
23 | along with this program ; if not, write to the Free Software
24 | Foundation, Inc., 59 Temple Place, Suite 330, Boston,
25 | MA 02111-1307 USA
26 |
27 | The original copyright holder can be contacted as
28 |
29 | Stephan Schulz
30 | Auf der Altenburg 7
31 | 70376 Stuttgart
32 | Germany
33 | Email: schulz@eprover.org
34 | """
35 |
36 | import unittest
37 | from idents import Ident
38 | from lexer import Token,Lexer
39 | from clausesets import ClauseSet, HeuristicClauseSet
40 | import heuristics
41 | from rescontrol import computeAllResolvents, computeAllFactors
42 | from subsumption import forwardSubsumption, backwardSubsumption
43 |
44 |
45 |
46 | class SimpleProofState(object):
47 | """
48 | Top-level data structure for the prover. The complete knowledge
49 | base is split into two sets, processed clauses and unprocessed
50 | clauses. These are represented here as individual clause sets. The
51 | main algorithm "processes" clauses and moves them from the
52 | unprocessed into the processed set. Processing typically generates
53 | several new clauses, which are direct consequences of the given
54 | clause and the processed claues. These new clauses are added to
55 | the set of unprocessed clauses.
56 | """
57 | def __init__(self, clauses):
58 | """
59 | Initialize the proof state with a set of clauses.
60 | """
61 | self.unprocessed = ClauseSet()
62 | self.processed = ClauseSet()
63 | for c in clauses.clauses:
64 | self.unprocessed.addClause(c)
65 |
66 | def processClause(self):
67 | """
68 | Pick a clause from unprocessed and process it. If the empty
69 | clause is found, return it. Otherwise return None.
70 | """
71 | given_clause = self.unprocessed.extractFirst()
72 | given_clause = given_clause.freshVarCopy()
73 | print("%", given_clause)
74 | if given_clause.isEmpty():
75 | # We have found an explicit contradiction
76 | return given_clause
77 |
78 | new = []
79 | factors = computeAllFactors(given_clause)
80 | new.extend(factors)
81 | resolvents = computeAllResolvents(given_clause, self.processed)
82 | new.extend(resolvents)
83 |
84 | self.processed.addClause(given_clause)
85 |
86 | for c in new:
87 | self.unprocessed.addClause(c)
88 | return None
89 |
90 | def saturate(self):
91 | """
92 | Main proof procedure. If the clause set is found
93 | unsatisfiable, return the empty clause as a witness. Otherwise
94 | return None.
95 | """
96 | while self.unprocessed:
97 | res = self.processClause()
98 | if res != None:
99 | return res
100 | else:
101 | return None
102 |
103 |
104 | class TestSimpleProver(unittest.TestCase):
105 | """
106 | Unit test class for simple resolution inference control.
107 | """
108 | def setUp(self):
109 | """
110 | Setup function for clause/literal unit tests. Initialize
111 | variables needed throughout the tests.
112 | """
113 | print()
114 | self.spec1 = """
115 | cnf(axiom, a_is_true, a).
116 | cnf(negated_conjecture, is_a_true, ~a)."""
117 |
118 | self.spec2 = """
119 | cnf(axiom, humans_are_mortal, mortal(X)|~human(X)).
120 | cnf(axiom, socrates_is_human, human(socrates)).
121 | cnf(negated_conjecture, is_socrates_mortal, ~mortal(socrates)).
122 | """
123 |
124 | self.spec3 = """
125 | cnf(p_or_q, axiom, p(a)).
126 | cnf(taut, axiom, q(a)).
127 | cnf(not_p, axiom, p(b)).
128 | """
129 |
130 | def evalSatResult(self, spec, provable):
131 | """
132 | Evaluate the result of a saturation compared to the expected
133 | result.
134 | """
135 |
136 | lex = Lexer(spec)
137 | problem = ClauseSet()
138 | problem.parse(lex)
139 |
140 | prover = SimpleProofState(problem)
141 | res = prover.saturate()
142 |
143 | if provable:
144 | self.assertNotEqual(res, None)
145 | if res == None: # pragma: nocover
146 | print("% Bug: Should have found a proof!")
147 | else:
148 | print("% Proof found")
149 | else:
150 | self.assertEqual(res, None)
151 | if res != None: # pragma: nocover
152 | print("% Bug: Should not have found a proof!")
153 | else:
154 | print("% No proof found")
155 |
156 |
157 | def testSaturation(self):
158 | """
159 | Test that saturation works.
160 | """
161 | self.evalSatResult(self.spec1, True)
162 | self.evalSatResult(self.spec2, True)
163 | self.evalSatResult(self.spec3, False)
164 |
165 | if __name__ == '__main__':
166 | unittest.main()
167 |
--------------------------------------------------------------------------------
/starexec_run_PyRes_default:
--------------------------------------------------------------------------------
1 | #!/bin/tcsh
2 | #
3 | # This is a StarExec runscript for PyRes. See https://www.starexec.org.
4 | #
5 | # To use this, build a StarExec package with "make starexec" and
6 | # upload it to a StarExec cluster - then follow local instructions
7 | # ;-).
8 | #
9 |
10 | module add python/python33
11 | echo -n "% Problem : " ; head -2 $1 | tail -1 | sed -e "s/.* : //"
12 | set ProblemSPC=`grep " SPC " $1 | sed -e "s/.* : //"`
13 | set flags="-tifbsVp -nlargest -HPickGiven5 "
14 | set final=" "$1
15 | set ecmd="./pyres-fof.py $flags $final"
16 |
17 | if ( `expr "$ProblemSPC" : "FOF.*"` || `expr "$ProblemSPC" : "CNF.*"` ) then
18 | echo "% Command : " $ecmd
19 | /home/starexec/bin/GetComputerInfo -p THIS Model CPUModel RAMPerCPU OS | \
20 | sed -e "s/Computer /% Computer /" \
21 | -e "s/Model /% Model /" \
22 | -e "s/CPUModel /% CPU /" \
23 | -e "s/RAMPerCPU /% Memory /" \
24 | -e "s/OS /% OS /"
25 | echo -n "% CPULimit : " ; echo "$STAREXEC_CPU_LIMIT"
26 | echo -n "% DateTime : " ; date
27 | echo "% CPUTime : "
28 | $ecmd
29 | else
30 | echo "% SZS status Inappropriate"
31 | endif
32 |
--------------------------------------------------------------------------------
/starexec_run_PyRes_default_ni:
--------------------------------------------------------------------------------
1 | #!/bin/tcsh
2 | #
3 | # This is a StarExec runscript for PyRes. See https://www.starexec.org.
4 | #
5 | # To use this, build a StarExec package with "make starexec" and
6 | # upload it to a StarExec cluster - then follow local instructions
7 | # ;-).
8 | #
9 |
10 | module add python/python33
11 | echo -n "% Problem : " ; head -2 $1 | tail -1 | sed -e "s/.* : //"
12 | set ProblemSPC=`grep " SPC " $1 | sed -e "s/.* : //"`
13 | set flags="-tfbsVp -nlargest -HPickGiven5 "
14 | set final=" "$1
15 | set ecmd="./pyres-fof.py $flags $final"
16 |
17 | if ( `expr "$ProblemSPC" : "FOF.*"` || `expr "$ProblemSPC" : "CNF.*"` ) then
18 | echo "% Command : " $ecmd
19 | /home/starexec/bin/GetComputerInfo -p THIS Model CPUModel RAMPerCPU OS | \
20 | sed -e "s/Computer /% Computer /" \
21 | -e "s/Model /% Model /" \
22 | -e "s/CPUModel /% CPU /" \
23 | -e "s/RAMPerCPU /% Memory /" \
24 | -e "s/OS /% OS /"
25 | echo -n "% CPULimit : " ; echo "$STAREXEC_CPU_LIMIT"
26 | echo -n "% DateTime : " ; date
27 | echo "% CPUTime : "
28 | $ecmd
29 | else
30 | echo "% SZS status Inappropriate"
31 | endif
32 |
--------------------------------------------------------------------------------
/substitutions.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # ----------------------------------
3 | #
4 | # Module substitutions.py
5 |
6 | """
7 | A simple implementation of substitutions.
8 |
9 | Definition: A substitution sigma is a function sigma:V->Terms(F,V)
10 | with the property that sigma(X)=X for all but finitely many variables
11 | X from V.
12 |
13 | A substitution is continued to terms recursively:
14 | sigma(f(t1, ..., tn)) = f(sigma(t1), ..., sigma(t2))
15 |
16 | Substitutions are customarily represented by the Greek letter simga.
17 |
18 | Footnote:
19 | If more than one substitution is needed, the second one is usually
20 | called tau, and further ones are denoted with primes or subscripts.
21 |
22 | We represent substitutions by a thin wrapper around Python
23 | dictionaries mapping variables to terms.
24 |
25 | Copyright 2010-2019 Stephan Schulz, schulz@eprover.org
26 |
27 | This program is free software; you can redistribute it and/or modify
28 | it under the terms of the GNU General Public License as published by
29 | the Free Software Foundation; either version 2 of the License, or
30 | (at your option) any later version.
31 |
32 | This program is distributed in the hope that it will be useful,
33 | but WITHOUT ANY WARRANTY; without even the implied warranty of
34 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
35 | GNU General Public License for more details.
36 |
37 | You should have received a copy of the GNU General Public License
38 | along with this program ; if not, write to the Free Software
39 | Foundation, Inc., 59 Temple Place, Suite 330, Boston,
40 | MA 02111-1307 USA
41 |
42 | The original copyright holder can be contacted as
43 |
44 | Stephan Schulz
45 | Auf der Altenburg 7
46 | 70376 Stuttgart
47 | Germany
48 | Email: schulz@eprover.org
49 | """
50 |
51 | import terms
52 | import unittest
53 |
54 |
55 | class Substitution(object):
56 | """
57 | Substitutions map variables to terms. Substitutions as used here
58 | are always fully expanded, i.e. each variable is bound directly to
59 | the term it maps to.
60 | """
61 |
62 | varCounter = 1
63 | """
64 | A counter to generate fresh variables.
65 | """
66 |
67 | def __init__(self, init = []):
68 | """
69 | Initialize. The optional argument is a list of variable/term
70 | pairs representing the initial binding. This is taken as-is,
71 | without any checks for consistency.
72 | """
73 | self.subst = {}
74 | for i in init:
75 | self.subst[i[0]]=i[1]
76 |
77 | def __repr__(self):
78 | """
79 | Return a print representation of the substitution.
80 | """
81 | return "{"+\
82 | ",".join([i+"<-"+terms.term2String(self.subst[i])
83 | for i in self.subst])\
84 | +"}"
85 |
86 | def __call__(self, term):
87 | """
88 | Pretty synonym for apply() allowing us to use substitutions as
89 | functions.
90 | """
91 | return self.apply(term)
92 |
93 | def copy(self):
94 | """
95 | Return a (flat) copy of the substitution.
96 | """
97 | res = Substitution()
98 | res.subst = dict(self.subst)
99 | return res
100 |
101 | def value(self, var):
102 | """
103 | Return the value of a variable (i.e. the term it is bound to,
104 | or the variable itself if it is not bound).
105 | """
106 | if var in self.subst:
107 | return self.subst[var]
108 | else:
109 | return var
110 |
111 | def isBound(self, var):
112 | """
113 | Return True if var is bound in self, false otherwise.
114 | """
115 | return var in self.subst
116 |
117 | def apply(self, term):
118 | """
119 | Apply the substitution to a term. Return the result.
120 | """
121 | if terms.termIsVar(term):
122 | return self.value(term)
123 | else:
124 | res = [terms.termFunc(term)]
125 | args = [self.apply(x) for x in terms.termArgs(term)]
126 | res.extend(args)
127 | return res
128 |
129 | def modifyBinding(self, binding):
130 | """
131 | Modify the substitution by adding a new binding (var,
132 | term). If the term is None, remove any binding for var. If it
133 | is not, add the binding. In either case, return the previous
134 | binding of the variable, or None if it was unbound.
135 | """
136 | var, term = binding
137 | if self.isBound(var):
138 | res = self.value(var)
139 | else:
140 | res = None
141 |
142 | if term == None:
143 | if self.isBound(var):
144 | del self.subst[var]
145 | else:
146 | self.subst[var] = term
147 |
148 | return res
149 |
150 | def composeBinding(self, binding):
151 | """
152 | Compose a new binding to an existing substitution.
153 | """
154 | tmpsubst = Substitution([binding])
155 | var, term = binding
156 | vars = self.subst.keys()
157 | for x in vars:
158 | bound = self.subst[x]
159 | self.subst[x] = tmpsubst.apply(bound)
160 | if not var in self.subst:
161 | self.subst[var] = term
162 |
163 |
164 |
165 | class BTSubst(Substitution):
166 | """
167 | A substitution that does not allow composition of new bindings, but
168 | in exchange offers backtrackability. Bindings are recorded in two
169 | data structures:
170 | self.subst is a dictionary that maps variables to terms
171 | self.bindings is an ordered list of bindings.
172 | """
173 | def __init__(self, init = []):
174 | """
175 | Initialize. The optional argument is a list of variable/term
176 | pairs representing the initial binding. This is taken as-is,
177 | without any checks for consistency.
178 | """
179 | self.bindings = list(init)
180 | Substitution.__init__(self, init)
181 |
182 | def getState(self):
183 | """
184 | Return a state to which this substitution can be backtracked
185 | later. We encode the state of the binding list, but also the
186 | object itself, to allow for some basic sanity checking.
187 | """
188 | return (self, len(self.bindings))
189 |
190 | def backtrack(self):
191 | """
192 | Backtrack a single binding (if there is one). Return success or
193 | failure.
194 | """
195 | if self.bindings:
196 | tmp = self.bindings.pop()
197 | del self.subst[tmp[0]]
198 | return True
199 | else:
200 | return False
201 |
202 | def backtrackToState(self, bt_state):
203 | """
204 | Backtrack to the given state. Note that we only perform very
205 | basic sanity checking. Return number of binding retracted.
206 | """
207 | subst, state = bt_state
208 | assert subst == self
209 | res = 0
210 |
211 | while len(self.bindings)>state:
212 | self.backtrack()
213 | res = res+1
214 | return res
215 |
216 | def addBinding(self, binding):
217 | """
218 | Add a single binding to the substitution.
219 | """
220 | var, term = binding
221 | self.subst[var] = term
222 | self.bindings.append(binding)
223 |
224 | def composeBinding(self, binding): # pragma: no cover
225 | """
226 | Overloaded to catch usage errors!
227 | """
228 | assert False and \
229 | "You cannot compose backtrackable substitutions."
230 |
231 |
232 | def freshVar():
233 | """
234 | Return a fresh variable. Note that this is not guaranteed to be
235 | different from input variables. However, it is guaranteed that
236 | freshVar() will never return the same variable more than once.
237 | """
238 | Substitution.varCounter += 1
239 | return "X%d"%(Substitution.varCounter,)
240 |
241 |
242 | def freshVarSubst(vars):
243 | """
244 | Create a substitution that maps all variables in the list vars to
245 | fresh variables. Note that there is no guarantee that the fresh
246 | variables have never been used. However, there is a a guarantee
247 | that the fresh variables have never been produced by a uniqSubst
248 | substitution.
249 |
250 | """
251 | l = [(var, freshVar()) for var in vars]
252 | return Substitution(l)
253 |
254 |
255 |
256 |
257 | class TestSubst(unittest.TestCase):
258 | """
259 | Test basic substitution functions.
260 | """
261 | def setUp(self):
262 | self.t1 = terms.string2Term("f(X, g(Y))")
263 | self.t2 = terms.string2Term("a")
264 | self.t3 = terms.string2Term("b")
265 | self.t4 = terms.string2Term("f(a, g(a))")
266 | self.t5 = terms.string2Term("f(a, g(b))")
267 |
268 | self.sigma1 = Substitution([("X", self.t2), ("Y", self.t2)])
269 | self.sigma2 = Substitution([("X", self.t2), ("Y", self.t3)])
270 |
271 | def testSubstBasic(self):
272 | """
273 | Test basic stuff.
274 | """
275 | tau = self.sigma1.copy()
276 | self.assertTrue(terms.termEqual(tau("X"), self.sigma1("X")))
277 | self.assertTrue(terms.termEqual(tau("Y"), self.sigma1("Y")))
278 | self.assertTrue(terms.termEqual(tau("Z"), self.sigma1("Z")))
279 |
280 | t = tau.modifyBinding(("X", self.t1))
281 | self.assertTrue(terms.termEqual(t, self.t2))
282 | t = tau.modifyBinding(("U", self.t1))
283 | self.assertEqual(t, None)
284 | self.assertTrue(tau.isBound("U"))
285 | self.assertTrue(terms.termEqual(tau.value("U"), self.t1))
286 | t = tau.modifyBinding(("U", None))
287 | self.assertTrue(not tau.isBound("U"))
288 |
289 |
290 | def testSubstApply(self):
291 | """
292 | Check application of substitutions
293 | """
294 | self.assertEqual(terms.term2String(self.sigma1(self.t1)),"f(a,g(a))")
295 | self.assertTrue(terms.termEqual(self.sigma1(self.t1), self.t4))
296 | self.assertTrue(terms.termEqual(self.sigma2(self.t1), self.t5))
297 |
298 |
299 | def testFreshVarSubst(self):
300 | """
301 | Test that
302 | """
303 | var1 = freshVar()
304 | var2 = freshVar()
305 | self.assertTrue(var1!=var2)
306 |
307 | vars = terms.termCollectVars(self.t1)
308 | sigma = freshVarSubst(vars)
309 | vars2 = terms.termCollectVars(sigma(self.t1))
310 | shared = set(vars).intersection(set(vars2))
311 | self.assertTrue(not shared)
312 |
313 | def testBacktrack(self):
314 | """
315 | Test backtrackable substitutions.
316 | """
317 | sigma = BTSubst()
318 | state = sigma.getState()
319 | sigma.addBinding(('X', terms.string2Term("f(Y)")))
320 | res = sigma.backtrackToState(state)
321 | self.assertEqual(res, 1)
322 | res = sigma.backtrack()
323 | self.assertTrue(not res)
324 |
325 |
326 |
327 | if __name__ == '__main__':
328 | unittest.main()
329 |
--------------------------------------------------------------------------------
/subsumption.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # ----------------------------------
3 | #
4 | # Module subsumption.py
5 |
6 | """
7 | This module implements first-order subsumption, as defined by the
8 | simplification rule below:
9 |
10 | Subsumption:
11 |
12 | C|R D
13 | =========== if sigma(D)=C for some substitution sigma
14 | D
15 |
16 | Note that C, D, R (and hence C|R) are clauses, i.e. they are
17 | multi-sets of literals interpreted as disjunctions. The multi-set
18 | aspect is important for this particular calculus, otherwise
19 | p(X)|p(Y) would be able to subsume p(X), i.e. a clause would subsume
20 | its own factors. This would destroy completeness.
21 |
22 | Copyright 2011-2019 Stephan Schulz, schulz@eprover.org
23 |
24 | This program is free software; you can redistribute it and/or modify
25 | it under the terms of the GNU General Public License as published by
26 | the Free Software Foundation; either version 2 of the License, or
27 | (at your option) any later version.
28 |
29 | This program is distributed in the hope that it will be useful,
30 | but WITHOUT ANY WARRANTY; without even the implied warranty of
31 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
32 | GNU General Public License for more details.
33 |
34 | You should have received a copy of the GNU General Public License
35 | along with this program ; if not, write to the Free Software
36 | Foundation, Inc., 59 Temple Place, Suite 330, Boston,
37 | MA 02111-1307 USA
38 |
39 | The original copyright holder can be contacted as
40 |
41 | Stephan Schulz
42 | Auf der Altenburg 7
43 | 70376 Stuttgart
44 | Germany
45 | Email: schulz@eprover.org
46 | """
47 |
48 | import unittest
49 | from lexer import Lexer
50 | from substitutions import BTSubst
51 | from matching import match
52 | from literals import Literal
53 | from clauses import Clause, parseClause
54 | from clausesets import ClauseSet
55 |
56 | def subsumeLitLists(subsumer, subsumed, subst):
57 | """
58 | Try to extend subst so that subst(subsumer) is a multi-subset of
59 | subsumed.
60 | """
61 | if not subsumer:
62 | return True
63 | for lit in subsumed:
64 | btstate = subst.getState()
65 | if subsumer[0].match(lit, subst):
66 | rest = [l for l in subsumed if l != lit]
67 | if subsumeLitLists(subsumer[1:], rest, subst):
68 | return True
69 | subst.backtrackToState(btstate)
70 | return False
71 |
72 | def subsumes(subsumer, subsumed):
73 | """
74 | Return True if subsumer subsumes subsumed, False otherwise.
75 | """
76 | if len(subsumer) > len(subsumed):
77 | return False
78 | subst = BTSubst()
79 | subsumer_list = subsumer.literals
80 | subsumed_list = subsumed.literals
81 | return subsumeLitLists(subsumer_list, subsumed_list, subst)
82 |
83 | def forwardSubsumption(set, clause):
84 | """
85 | Return True if any clause from set subsumes clause, False otherwise.
86 | """
87 | candidates = set.getSubsumingCandidates(clause)
88 | for c in candidates:
89 | if subsumes(c, clause):
90 | return True
91 | return False
92 |
93 |
94 | def backwardSubsumption(clause, set):
95 | """
96 | Remove all clauses that are subsumed by clause from set.
97 | """
98 | candidates = set.getSubsumedCandidates(clause)
99 | subsumed_set = []
100 | for c in candidates:
101 | if subsumes(clause, c):
102 | subsumed_set.append(c)
103 | res = len(subsumed_set)
104 | for c in subsumed_set:
105 | set.extractClause(c)
106 | return res
107 |
108 |
109 | class TestResolution(unittest.TestCase):
110 | """
111 | Unit test class for clauses. Test clause and literal
112 | functionality.
113 | """
114 | def setUp(self):
115 | """
116 | Setup function for resolution testing
117 | """
118 | print()
119 | self.spec = """
120 | cnf(axiom, c1, $false).
121 | cnf(axiom, c2, p(a)).
122 | cnf(axiom, c3, p(X)).
123 | cnf(axiom, c4, p(a)|q(f(X))).
124 | cnf(axiom, c5, p(a)|q(f(b))|p(X)).
125 | cnf(axiom, c6, X=X).
126 | cnf(axiom, c7, Y=Y).
127 | """
128 | lex = Lexer(self.spec)
129 | self.c1 = parseClause(lex)
130 | self.c2 = parseClause(lex)
131 | self.c3 = parseClause(lex)
132 | self.c4 = parseClause(lex)
133 | self.c5 = parseClause(lex)
134 | self.c6 = parseClause(lex)
135 | self.c7 = parseClause(lex)
136 |
137 | self.cset = ClauseSet()
138 | self.cset.addClause(self.c2)
139 | self.cset.addClause(self.c3)
140 | self.cset.addClause(self.c4)
141 | self.cset.addClause(self.c5)
142 | self.cset.addClause(self.c6)
143 | self.cset.addClause(self.c7)
144 |
145 | def testSubsumption(self):
146 | """
147 | Test subsumption.
148 | """
149 | res = subsumes(self.c1, self.c1)
150 | self.assertTrue(res)
151 |
152 | res = subsumes(self.c2, self.c2)
153 | self.assertTrue(res)
154 |
155 | res = subsumes(self.c3, self.c3)
156 | self.assertTrue(res)
157 |
158 | res = subsumes(self.c4, self.c4)
159 | self.assertTrue(res)
160 |
161 | res = subsumes(self.c1, self.c2)
162 | self.assertTrue(res)
163 |
164 | res = subsumes(self.c2, self.c1)
165 | self.assertTrue(not res)
166 |
167 | res = subsumes(self.c2, self.c3)
168 | self.assertTrue(not res)
169 |
170 | res = subsumes(self.c3, self.c2)
171 | self.assertTrue(res)
172 |
173 | res = subsumes(self.c4, self.c5)
174 | self.assertTrue(res)
175 |
176 | res = subsumes(self.c5, self.c4)
177 | self.assertTrue(not res)
178 |
179 | res = subsumes(self.c6, self.c6)
180 | self.assertTrue(res)
181 |
182 | res = subsumes(self.c6, self.c7)
183 | self.assertTrue(res)
184 |
185 | def testSetSubsumption(self):
186 | """
187 | Test set subsumption.
188 | """
189 | self.assertTrue(not forwardSubsumption(self.cset, self.c1))
190 | self.assertTrue(forwardSubsumption(self.cset, self.c2))
191 |
192 | tmp = backwardSubsumption(self.c1, self.cset)
193 | self.assertEqual(tmp, 6)
194 |
195 |
196 | if __name__ == '__main__':
197 | unittest.main()
198 |
--------------------------------------------------------------------------------
/terms.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # ----------------------------------
3 | #
4 | # Module terms.py
5 |
6 | """
7 | A simple implementation of first-order terms. We use nested Python
8 | lists in the style of s-expressions as the term data type.
9 |
10 | Definition: Let F be a finite set of function symbols and V be an
11 | enumerable set of variable symbols. Let ar:F->N be the arity function
12 | associating a natural number (the "arity") with each function
13 | symbol. The set of all terms over F and V, Terms(F,V) is defined as
14 | follows:
15 | - For all X in V, X in Term(F,V)
16 | - For all f|n in F and t1,..,tn in Term(F,V), f(t1, ..., tn) in
17 | Term(F,V).
18 | - Term(F,V) is the smallest set with the above two properties.
19 |
20 |
21 | In the concrete syntax (i.e. the syntax we use to write terms in ASCII
22 | text form), we represent elements of F by identifers starting with a
23 | lower-case letter. The arity is implicitly given by the number of
24 | argument terms in a term. For function symbols with arity 0, we omit
25 | the parenthesis of the empty argument list.
26 |
27 | We represent elements of V by identifiers starting with an upper-case
28 | letter or underscore.
29 |
30 | A composite term f(t1, ..., tn) is represented by the list
31 | [f lt1, ..., ltn], where lt1, ..., ltn are lists representing the
32 | subterms. See below for exmples:
33 |
34 | "X" -> "X"
35 | "a" -> ["a"]
36 | "g(a,b)" -> ["g", ["a"], ["b"]]
37 | "g(X, f(Y))" -> ["g", "X", ["f", "Y"]]
38 |
39 | Note in particular that constant terms are lists with one elements,
40 | not plain strings.
41 |
42 | Copyright 2010-2019 Stephan Schulz, schulz@eprover.org
43 |
44 | This program is free software; you can redistribute it and/or modify
45 | it under the terms of the GNU General Public License as published by
46 | the Free Software Foundation; either version 2 of the License, or
47 | (at your option) any later version.
48 |
49 | This program is distributed in the hope that it will be useful,
50 | but WITHOUT ANY WARRANTY; without even the implied warranty of
51 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
52 | GNU General Public License for more details.
53 |
54 | You should have received a copy of the GNU General Public License
55 | along with this program ; if not, write to the Free Software
56 | Foundation, Inc., 59 Temple Place, Suite 330, Boston,
57 | MA 02111-1307 USA
58 |
59 | The original copyright holder can be contacted as
60 |
61 | Stephan Schulz
62 | Auf der Altenburg 7
63 | 70376 Stuttgart
64 | Germany
65 | Email: schulz@eprover.org
66 | """
67 |
68 | import unittest
69 | from lexer import Token,Lexer
70 | from signature import Signature
71 |
72 |
73 | def termIsVar(t):
74 | """
75 | Check if the term is a variable. This assumes that t is a
76 | well-formed term.
77 | """
78 | return type(t)!=type([])
79 |
80 |
81 | def termIsCompound(t):
82 | """
83 | Check if the term is a compound term. This assumes that t is a
84 | well-formed term.
85 | """
86 | return not termIsVar(t)
87 |
88 |
89 | def termFunc(t):
90 | """
91 | Return the function symbol of the compound term t.
92 | """
93 | assert termIsCompound(t)
94 | return t[0]
95 |
96 |
97 | def termArgs(t):
98 | """
99 | Return the argument list of the compound term t.
100 | """
101 | assert termIsCompound(t)
102 | return t[1:]
103 |
104 |
105 | def term2String(t):
106 | """
107 | Convert a term t into a string.
108 | """
109 | if termIsVar(t):
110 | return t
111 | else:
112 | # We need to handle the case of constants separatly
113 | if not termArgs(t):
114 | return termFunc(t)
115 | else:
116 | arg_rep = ",".join([term2String(s) for s in termArgs(t)])
117 | return termFunc(t)+"("+arg_rep+")"
118 |
119 |
120 | def parseTermList(lexer):
121 | """
122 | Parse a comma-delimited list of terms.
123 | """
124 | res = []
125 | res.append(parseTerm(lexer))
126 | while lexer.TestTok(Token.Comma):
127 | lexer.AcceptTok(Token.Comma)
128 | res.append(parseTerm(lexer))
129 | return res
130 |
131 |
132 | def parseTerm(lexer):
133 | """
134 | Read a complete term from the lexer provided.
135 | """
136 | if lexer.TestTok(Token.IdentUpper):
137 | res = lexer.Next().literal
138 | else:
139 | res = []
140 | lexer.CheckTok([Token.IdentLower,Token.DefFunctor,Token.SQString])
141 | res.append(lexer.Next().literal)
142 | if lexer.TestTok(Token.OpenPar):
143 | # It's a term with proper subterms, so parse them
144 | lexer.AcceptTok(Token.OpenPar)
145 | res.extend(parseTermList(lexer))
146 | lexer.AcceptTok(Token.ClosePar)
147 | return res
148 |
149 |
150 | def string2Term(str):
151 | """
152 | Convert a string into a term.
153 | """
154 | lexer = Lexer(str)
155 | return parseTerm(lexer)
156 |
157 |
158 | def termListEqual(l1, l2):
159 | """
160 | Compare two lists of terms.
161 | """
162 | if len(l1)!=len(l2):
163 | return False
164 | if not l1:
165 | # l1 is empty, and so, by the previous test, is l2
166 | return True
167 | for i in range(len(l1)):
168 | if not termEqual(l1[i], l2[i]):
169 | return False
170 | return True
171 |
172 |
173 | def termEqual(t1, t2):
174 | """
175 | Compare two terms for syntactic equality.
176 | """
177 | if termIsVar(t1):
178 | return t1 == t2
179 | elif termIsVar(t2):
180 | return False
181 | else:
182 | if termFunc(t1)!=termFunc(t2):
183 | return False
184 | return termListEqual(termArgs(t1), termArgs(t2))
185 |
186 |
187 | def termCopy(t):
188 | """
189 | Return a (deep) copy of t. This is the lazy man's way...
190 | """
191 | if type(t) == type([]):
192 | # t is a list, so we copy the elements of the list
193 | return [termCopy(x) for x in t]
194 | return t
195 |
196 |
197 | def termIsGround(t):
198 | """
199 | termIsGround(t): Return True if term has no variables, False otherwise
200 | """
201 | if termIsVar(t):
202 | return False
203 | else:
204 | for term in termArgs(t):
205 | if not termIsGround(term):
206 | return False
207 | return True
208 |
209 |
210 | def termCollectVars(t, res=None):
211 | """
212 | Insert all variables in t into the set res. For convenience,
213 | return res. If res is not given, create and return it.
214 | """
215 | if res == None:
216 | res = set()
217 | if termIsVar(t):
218 | res.add(t)
219 | else:
220 | for s in termArgs(t):
221 | termCollectVars(s, res)
222 | return res
223 |
224 |
225 | def termCollectFuns(t, res=None):
226 | """
227 | Insert all function symbols in t into the set res. For
228 | convenience, return res. If res is not given, create and return
229 | it.
230 | """
231 | if res == None:
232 | res = set()
233 | if termIsCompound(t):
234 | res.add(termFunc(t))
235 | for s in termArgs(t):
236 | termCollectFuns(s, res)
237 | return res
238 |
239 |
240 | def termCollectSig(t, sig=None):
241 | """
242 | Insert all function symbols and their associated arities in t into
243 | the signature sig. For convenience, return it. If sig is not
244 | given, create it.
245 | """
246 | if sig == None:
247 | sig = Signature()
248 | if termIsCompound(t):
249 | sig.addFun(termFunc(t), len(t)-1)
250 | for s in termArgs(t):
251 | termCollectSig(s, sig)
252 | return sig
253 |
254 |
255 | def termWeight(t, fweight, vweight):
256 | """
257 | Return the weight of the term, counting fweight for each function
258 | symbol occurance, vweight for each variable occurance.
259 | Examples:
260 | termWeight(f(a,b), 1, 1) = 3
261 | termWeight(f(a,b), 2, 1) = 6
262 | termWeight(f(X,Y), 2, 1) = 4
263 | termWeight(X, 2, 1) = 1
264 | termWeight(g(a), 3, 1) = 6
265 | """
266 | if termIsVar(t):
267 | return vweight
268 | else:
269 | res = fweight
270 | for s in termArgs(t):
271 | res = res + termWeight(s, fweight, vweight)
272 | return res
273 |
274 |
275 |
276 | def subterm(t, pos):
277 | """
278 | Return the subterm of t at position pos (or None if pos is not a
279 | position in term). pos is a list of integers denoting branches,
280 | e.g.
281 | subterm(f(a,b), []) = f(a,b)
282 | subterm(f(a,g(b)), [0]) = a
283 | subterm(f(a,g(b)), [1]) = g(b)
284 | subterm(f(a,g(b)), [1,0]) = b
285 | subterm(f(a,g(b)), [3,0]) = None
286 | """
287 | if not pos:
288 | return t
289 | index = pos.pop(0)
290 | if index >= len(t):
291 | return None
292 | return subterm(t[index],pos)
293 |
294 |
295 | class TestTerms(unittest.TestCase):
296 | """
297 | Test basic term functions.
298 | """
299 | def setUp(self):
300 | self.example1 = "X"
301 | self.example2 = "a"
302 | self.example3 = "g(a,b)"
303 | self.example4 = "g(X, f(Y))"
304 | self.example5 = "g(X, f(Y))"
305 | self.example6 = "g(b,b)"
306 | self.example7 = "'g'(b,b)"
307 | self.t1 = string2Term(self.example1)
308 | self.t2 = string2Term(self.example2)
309 | self.t3 = string2Term(self.example3)
310 | self.t4 = string2Term(self.example4)
311 | self.t5 = string2Term(self.example5)
312 | self.t6 = string2Term(self.example6)
313 | self.t7 = string2Term(self.example7)
314 |
315 |
316 | def testToString(self):
317 | """
318 | Test that stringTerm and term2String are dual. Start with
319 | terms, so that we are sure to get the canonical string
320 | representation.
321 | """
322 | self.assertEqual(string2Term(term2String(self.t1)), self.t1)
323 | self.assertEqual(string2Term(term2String(self.t2)), self.t2)
324 | self.assertEqual(string2Term(term2String(self.t3)), self.t3)
325 | self.assertEqual(string2Term(term2String(self.t4)), self.t4)
326 | self.assertEqual(string2Term(term2String(self.t7)), self.t7)
327 |
328 | def testIsVar(self):
329 | """
330 | Test if the classification function work as expected.
331 | """
332 | self.assertTrue(termIsVar(self.t1))
333 | self.assertTrue(not termIsVar(self.t2))
334 | self.assertTrue(not termIsVar(self.t3))
335 | self.assertTrue(not termIsVar(self.t4))
336 |
337 |
338 | def testIsCompound(self):
339 | """
340 | Test if the classification function work as expected.
341 | """
342 | self.assertTrue(not termIsCompound(self.t1))
343 | self.assertTrue(termIsCompound(self.t2))
344 | self.assertTrue(termIsCompound(self.t3))
345 | self.assertTrue(termIsCompound(self.t4))
346 |
347 |
348 | def testEquality(self):
349 | """
350 | Test if term equality works as expected.
351 | """
352 | self.assertTrue(termEqual(self.t1, self.t1))
353 | self.assertTrue(termEqual(self.t2, self.t2))
354 | self.assertTrue(termEqual(self.t3, self.t3))
355 | self.assertTrue(termEqual(self.t4, self.t4))
356 | self.assertTrue(termEqual(self.t5, self.t5))
357 |
358 | self.assertTrue(termEqual(self.t4, self.t5))
359 |
360 | self.assertTrue(not termEqual(self.t1, self.t4))
361 | self.assertTrue(not termEqual(self.t3, self.t4))
362 | self.assertTrue(not termEqual(self.t3, self.t6))
363 |
364 | l1 = []
365 | l2 = [self.t1]
366 | self.assertTrue(not termListEqual(l1,l2))
367 |
368 |
369 | def testCopy(self):
370 | """
371 | Test if term copying works.
372 | """
373 | t1 = termCopy(self.t1)
374 | self.assertTrue(termEqual(t1, self.t1))
375 | t2 = termCopy(self.t2)
376 | self.assertTrue(termEqual(t2, self.t2))
377 | t3 = termCopy(self.t3)
378 | self.assertTrue(termEqual(t3, self.t3))
379 | t4 = termCopy(self.t4)
380 | self.assertTrue(termEqual(t4, self.t4))
381 |
382 |
383 | def testIsGround(self):
384 | """
385 | Test if isGround() works as expected.
386 | """
387 | self.assertTrue(not termIsGround(self.t1))
388 | self.assertTrue(termIsGround(self.t2))
389 | self.assertTrue(termIsGround(self.t3))
390 | self.assertTrue(not termIsGround(self.t4))
391 | self.assertTrue(not termIsGround(self.t5))
392 |
393 | def testCollectVars(self):
394 | """
395 | Test the variable collection.
396 | """
397 | vars = termCollectVars(self.t1)
398 | self.assertEqual(len(vars),1)
399 | termCollectVars(self.t2, vars)
400 | self.assertEqual(len(vars),1)
401 | termCollectVars(self.t3, vars)
402 | self.assertEqual(len(vars),1)
403 | termCollectVars(self.t4, vars)
404 | self.assertEqual(len(vars),2)
405 | termCollectVars(self.t5, vars)
406 | self.assertEqual(len(vars),2)
407 |
408 | self.assertTrue("X" in vars)
409 | self.assertTrue("Y" in vars)
410 |
411 |
412 | def testCollectFuns(self):
413 | """
414 | Test function symbol collection.
415 | """
416 | funs = termCollectFuns(self.t1)
417 | self.assertEqual(funs, set())
418 |
419 | funs = termCollectFuns(self.t2)
420 | self.assertEqual(funs, set(["a"]))
421 |
422 | funs = termCollectFuns(self.t3)
423 | self.assertEqual(funs, set(["g", "a", "b"]))
424 |
425 | funs = termCollectFuns(self.t4)
426 | self.assertEqual(funs, set(["g", "f"]))
427 |
428 | funs = termCollectFuns(self.t5)
429 | self.assertEqual(funs, set(["g", "f"]))
430 |
431 | funs = termCollectFuns(self.t6)
432 | self.assertEqual(funs, set(["g", "b"]))
433 |
434 | def testCollectSig(self):
435 | """
436 | Test signature collection.
437 | """
438 | sig = termCollectSig(self.t1)
439 | sig = termCollectSig(self.t2, sig)
440 | sig = termCollectSig(self.t3, sig)
441 | sig = termCollectSig(self.t4, sig)
442 | sig = termCollectSig(self.t5, sig)
443 | sig = termCollectSig(self.t6, sig)
444 |
445 | self.assertEqual(sig.getArity("f"), 1)
446 | self.assertEqual(sig.getArity("g"), 2)
447 | self.assertEqual(sig.getArity("a"), 0)
448 | self.assertEqual(sig.getArity("b"), 0)
449 |
450 |
451 | def testWeight(self):
452 | """
453 | Test if termWeight() works as expected.
454 | """
455 | self.assertTrue(termWeight(self.t1,1,2) == 2)
456 | self.assertTrue(termWeight(self.t2,1,2) == 1)
457 | self.assertTrue(termWeight(self.t3,1,2) == 3)
458 | self.assertTrue(termWeight(self.t4,1,2) == 6)
459 | self.assertTrue(termWeight(self.t5,2,1) == 6)
460 |
461 | def testSubterm(self):
462 | """
463 | Test if subterm() works as expected.
464 | self.example5 = "g(X, f(Y))"
465 | """
466 | self.assertTrue(subterm(self.t5,[]) == ['g', 'X', ['f', 'Y']])
467 | self.assertTrue(subterm(self.t5,[0]) == 'g')
468 | self.assertTrue(subterm(self.t5,[1]) == 'X')
469 | self.assertTrue(subterm(self.t5,[2,0]) == 'f')
470 | self.assertTrue(subterm(self.t5,[5,0]) == None)
471 |
472 | if __name__ == '__main__':
473 | unittest.main()
474 |
--------------------------------------------------------------------------------
/unification.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # ----------------------------------
3 | #
4 | # Module unification.py
5 |
6 | """
7 | This code implements unification for first-order terms (and, by
8 | inheritance, atoms).
9 |
10 |
11 | === Unification ===
12 |
13 | The goal of a unification algorithm is to find a substitution sigma
14 | that will make two term s and t equal, i.e. sigma(s)=sigma(t). The
15 | unification algorithm we implement here is based on simultaneous
16 | solution of sets of equations over terms. It operates on a tuple with
17 | two elements: A set of equations and a substitution.
18 |
19 | The initial tuple for unifying s and t is
20 | {s=t}, {}
21 | i.e. it consists of the single equation s=t and the empty
22 | substitution. The goal is to step by step build a substitution that
23 | will make s and t equal.
24 |
25 | The unification algorithm picks an arbitray equation and tries to
26 | handle it by one of the following rules. It terminates either when the
27 | set of equations is empty (in that case the resulting substitution is
28 | the most general unifier), or if it derives FAIL, in which case the
29 | two original terms are not unifiable.
30 |
31 | The "Bind" rules handle the case that one of the two terms of the
32 | given equation is a variable X. If X does not occur in the
33 | other term t, the rule binds t to X, applies this binding to the open
34 | equations, and records it in sigma.
35 |
36 |
37 | Bind 1
38 | {X=t} \cup R, sigma
39 | ========================= if X does not occur in t
40 | {X<-t}(R), sigma \circ {X<-t}
41 |
42 | Bind 2
43 | {t=X} \cup R, sigma
44 | ========================= if X does not occur in t
45 | {X<-t}(R), sigma \circ {X<-t}
46 |
47 |
48 | The "Decompose" rule handles two terms with the same top function
49 | symbol. Since this symbol is already equal, we just need to make the
50 | individual argument terms equal. This is reflected by creating a new
51 | equation for each pair of corresponding arguments, and adding them to
52 | the list of open equations.
53 |
54 |
55 | Decompose:
56 | {f(s1, ..., sn)=f(t1, ..., tn)} \cup R, sigma
57 | ==============================================
58 | {s1=t1, ..., sn=tn} \cup R, sigma
59 |
60 |
61 | A trivial case easily overlooked is the case of an equation between
62 | two variables that are already equal:
63 |
64 |
65 | Solved:
66 | {X=X} \cup R, sigma
67 | =========================
68 | R, sigma
69 |
70 |
71 | If none of the above rules is applicable, then we cannot solve the
72 | given equation with a substitution. We can recognize these cases and
73 | transition to an explixit failure state.
74 |
75 | In the first failure case, the top function symbols of the two terms
76 | clash. Since the application of a substitution never changes a
77 | function symbol, no substitution can make the two terms equal.
78 |
79 |
80 | Structural fail:
81 | {f(s1, ..., tn) = g(t1, ..., tm) \cup R, sigma
82 | =============================================== if f!=g
83 | FAIL
84 |
85 |
86 | The second cause of failure is an equation where a variable X on one
87 | side has to be unified with a term t[X] that contains the same
88 | variable embedded on the other side. No binding of X will ever get rid
89 | of the context in which X is embedded, so no substitution will ever
90 | make X and t[X] equal.
91 |
92 |
93 | Occurs-Fail 1:
94 | {X=t} \cup R, sigma
95 | ==================== if X does occur in t, X!=t
96 | FAIL
97 |
98 |
99 | Occurs-Fail 2:
100 | {t=X} \cup R, sigma
101 | ==================== if X does occur in t, X!=t
102 | FAIL
103 |
104 |
105 |
106 | Copyright 2010-2019 Stephan Schulz, schulz@eprover.org
107 |
108 | This program is free software; you can redistribute it and/or modify
109 | it under the terms of the GNU General Public License as published by
110 | the Free Software Foundation; either version 2 of the License, or
111 | (at your option) any later version.
112 |
113 | This program is distributed in the hope that it will be useful,
114 | but WITHOUT ANY WARRANTY; without even the implied warranty of
115 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
116 | GNU General Public License for more details.
117 |
118 | You should have received a copy of the GNU General Public License
119 | along with this program ; if not, write to the Free Software
120 | Foundation, Inc., 59 Temple Place, Suite 330, Boston,
121 | MA 02111-1307 USA
122 |
123 | The original copyright holder can be contacted as
124 |
125 | Stephan Schulz
126 | Auf der Altenburg 7
127 | 70376 Stuttgart
128 | Germany
129 | Email: schulz@eprover.org
130 | """
131 |
132 | from terms import *
133 | from substitutions import *
134 |
135 |
136 | def occursCheck(x, t):
137 | """
138 | Perform an occurs-check, i.e. determine if the variable x occurs in
139 | the term t. If that is the case (and t != x), the two can never be
140 | unified.
141 | """
142 | if termIsCompound(t):
143 | for i in t[1:]:
144 | if occursCheck(x, i):
145 | return True
146 | return False
147 | else:
148 | return x == t
149 |
150 |
151 | def mguTermList(l1, l2, subst):
152 | """
153 | Unify all terms in l1 with the corresponding terms in l2 with a
154 | common substitution variable "subst". We don't use explicit
155 | equations or pairs of terms here - if l1 is [s1, s2, s3] and l2 is
156 | [t1, t2, t3], this represents the set of equations {s1=t1, s2=t2,
157 | s3=t3}. This makes several operations easier to implement.
158 | """
159 | assert len(l1)==len(l2)
160 | while(len(l1)!=0):
161 | # Pop the first term pair to unify off the lists (pop removes
162 | # and returns the denoted elements)
163 | t1 = l1.pop(0)
164 | t2 = l2.pop(0)
165 | if termIsVar(t1):
166 | if t1==t2:
167 | # This implements the "Solved" rule.
168 | # We could always test this upfront, but that would
169 | # require an expensive check every time.
170 | # We descend recursively anyway, so we only check this on
171 | # the terminal case.
172 | continue
173 | if occursCheck(t1, t2):
174 | return None
175 | # Here is the core of the "Bind" rule
176 | # We now create a new substitution that binds t2 to t1, and
177 | # apply it to the remaining unification problem. We know
178 | # that every variable will only ever be bound once, because
179 | # we eliminate all occurances of it in this step - remember
180 | # that by the failed occurs-check, t2 cannot contain t1.
181 | new_binding = Substitution([(t1, t2)])
182 | l1 = [new_binding(t) for t in l1]
183 | l2 = [new_binding(t) for t in l2]
184 | subst.composeBinding((t1, t2))
185 | elif termIsVar(t2):
186 | # Symmetric case
187 | # We know that t1!=t2, so we can drop this check
188 | if occursCheck(t2, t1):
189 | return None
190 | new_binding = Substitution([(t2, t1)])
191 | l1 = [new_binding(t) for t in l1]
192 | l2 = [new_binding(t) for t in l2]
193 | subst.composeBinding((t2, t1))
194 | else:
195 | assert termIsCompound(t1) and termIsCompound(t2)
196 | # Try to apply "Decompose"
197 | # For f(s1, ..., sn) = g(t1, ..., tn), first f and g have to
198 | # be equal...
199 | if terms.termFunc(t1) != terms.termFunc(t2):
200 | # Nope, "Structural fail":
201 | return None
202 | # But if the symbols are equal, here is the decomposition:
203 | # We need to ensure that for all i si=ti get
204 | # added to the list of equations to be solved.
205 | l1.extend(termArgs(t1))
206 | l2.extend(termArgs(t2))
207 | return subst
208 |
209 |
210 | def mgu(t1, t2):
211 | """
212 | Try to unify t1 and t2, return substitution on success, or None on failure.
213 | """
214 | res = mguTermList([t1], [t2], Substitution())
215 | res2 = "False"
216 | if res:
217 | res2 = "True"
218 | # print("% :", term2String(t1), " : ", term2String(t2), " => ", res2);
219 | return res
220 |
221 |
222 |
223 | class TestUnification(unittest.TestCase):
224 | """
225 | Test basic substitution functions.
226 | """
227 | def setUp(self):
228 | self.s1 = terms.string2Term("X")
229 | self.t1 = terms.string2Term("a")
230 |
231 | self.s2 = terms.string2Term("X")
232 | self.t2 = terms.string2Term("f(X)")
233 |
234 | self.s3 = terms.string2Term("X")
235 | self.t3 = terms.string2Term("f(Y)")
236 |
237 | self.s4 = terms.string2Term("f(X, a)")
238 | self.t4 = terms.string2Term("f(b, Y)")
239 |
240 | self.s5 = terms.string2Term("f(X, g(a))")
241 | self.t5 = terms.string2Term("f(X, Y))")
242 |
243 | self.s6 = terms.string2Term("f(X, g(a))")
244 | self.t6 = terms.string2Term("f(X, X))")
245 |
246 | self.s7 = terms.string2Term("g(X)")
247 | self.t7 = terms.string2Term("g(f(g(X),b))")
248 |
249 | self.s8 = terms.string2Term("p(X,X,X)")
250 | self.t8 = terms.string2Term("p(Y,Y,e)")
251 |
252 | self.s9 = terms.string2Term("f(f(g(X),a),X)")
253 | self.t9 = terms.string2Term("f(Y,g(Y))")
254 |
255 | self.s10 = terms.string2Term("f(f(g(X),a),g(X))")
256 | self.t10 = terms.string2Term("f(Y,g(Z))")
257 |
258 | self.s11 = terms.string2Term("p(X,g(a), f(a, f(a)))")
259 | self.t11 = terms.string2Term("p(f(a), g(Y), f(Y, Z))")
260 |
261 |
262 |
263 | def unif_test(self, s,t, success_expected):
264 | """
265 | Test if s and t can be unified. If yes, report the
266 | result. Compare to the expected result.
267 | """
268 | print("Trying to unify", term2String(s), "and", term2String(t))
269 | sigma = mgu(s,t)
270 | if success_expected:
271 | self.assertTrue(sigma)
272 | self.assertTrue(termEqual(sigma(s), sigma(t)))
273 | print(term2String(sigma(s)), term2String(sigma(t)), sigma)
274 | else:
275 | print("Failure")
276 | self.assertTrue(not sigma)
277 | print()
278 |
279 | def testMGU(self):
280 | """
281 | Test basic stuff.
282 | """
283 | print()
284 | self.unif_test(self.s1, self.t1, True)
285 | self.unif_test(self.s2, self.t2, False)
286 | self.unif_test(self.t2, self.s2, False)
287 | self.unif_test(self.s3, self.t3, True)
288 | self.unif_test(self.s4, self.t4, True)
289 | self.unif_test(self.s5, self.t5, True)
290 | self.unif_test(self.s6, self.t6, True)
291 | self.unif_test(self.s7, self.t7, False)
292 | self.unif_test(self.s8, self.t8, True)
293 |
294 | self.unif_test(self.s9, self.t9, False)
295 | self.unif_test(self.s10, self.t10, True)
296 | self.unif_test(self.s11, self.t11, True)
297 |
298 | # Unification should be symmetrical
299 | # self.unif_test(self.t1, self.s1, True)
300 | # self.unif_test(self.t2, self.s2, False)
301 | # self.unif_test(self.t3, self.s3, True)
302 | # self.unif_test(self.t4, self.s4, True)
303 | # self.unif_test(self.t5, self.s5, True)
304 | # self.unif_test(self.t6, self.s6, True)
305 | # self.unif_test(self.t7, self.s7, False)
306 | # self.unif_test(self.t8, self.s8, True)
307 |
308 |
309 | if __name__ == '__main__':
310 | unittest.main()
311 |
--------------------------------------------------------------------------------
/version.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # ----------------------------------
3 | #
4 | # Module version.py
5 |
6 | """
7 | Just the version number, to centralize it.
8 |
9 | Copyright 2021 Stephan Schulz, schulz@eprover.org
10 |
11 | This program is free software; you can redistribute it and/or modify
12 | it under the terms of the GNU General Public License as published by
13 | the Free Software Foundation; either version 2 of the License, or
14 | (at your option) any later version.
15 |
16 | This program is distributed in the hope that it will be useful,
17 | but WITHOUT ANY WARRANTY; without even the implied warranty of
18 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 | GNU General Public License for more details.
20 |
21 | You should have received a copy of the GNU General Public License
22 | along with this program ; if not, write to the Free Software
23 | Foundation, Inc., 59 Temple Place, Suite 330, Boston,
24 | MA 02111-1307 USA
25 |
26 | The original copyright holder can be contacted as
27 |
28 | Stephan Schulz
29 | Auf der Altenburg 7
30 | 70376 Stuttgart
31 | Germany
32 | Email: schulz@eprover.org
33 | """
34 |
35 | version = "1.5"
36 |
--------------------------------------------------------------------------------