├── COPYING ├── EXAMPLES ├── PUZ001+1.p ├── PUZ001-1.p ├── PUZ002-1.p └── PUZ003-1.p ├── Makefile ├── PyRes-1.0.html ├── README ├── clauses.py ├── clausesets.py ├── derivations.py ├── eqaxioms.py ├── fofspec.py ├── formulacnf.py ├── formulas.py ├── heuristics.py ├── idents.py ├── indexing.py ├── lexer.py ├── literals.py ├── litselection.py ├── matching.py ├── pyres-cnf.py ├── pyres-fof.py ├── pyres-simple.py ├── rescontrol.py ├── resolution.py ├── saturation.py ├── signature.py ├── simplesat.py ├── starexec_run_PyRes_default ├── starexec_run_PyRes_default_ni ├── substitutions.py ├── subsumption.py ├── terms.py ├── unification.py └── version.py /EXAMPLES/PUZ001+1.p: -------------------------------------------------------------------------------- 1 | %------------------------------------------------------------------------------ 2 | % File : PUZ001+1 : TPTP v6.0.0. Released v2.0.0. 3 | % Domain : Puzzles 4 | % Problem : Dreadbury Mansion 5 | % Version : Especial. 6 | % Theorem formulation : Reduced > Complete. 7 | % English : Someone who lives in Dreadbury Mansion killed Aunt Agatha. 8 | % Agatha, the butler, and Charles live in Dreadbury Mansion, 9 | % and are the only people who live therein. A killer always 10 | % hates his victim, and is never richer than his victim. 11 | % Charles hates no one that Aunt Agatha hates. Agatha hates 12 | % everyone except the butler. The butler hates everyone not 13 | % richer than Aunt Agatha. The butler hates everyone Aunt 14 | % Agatha hates. No one hates everyone. Agatha is not the 15 | % butler. Therefore : Agatha killed herself. 16 | 17 | % Refs : [Pel86] Pelletier (1986), Seventy-five Problems for Testing Au 18 | % : [Hah94] Haehnle (1994), Email to G. Sutcliffe 19 | % Source : [Hah94] 20 | % Names : Pelletier 55 [Pel86] 21 | 22 | % Status : Theorem 23 | % Rating : 0.20 v6.0.0, 0.26 v5.5.0, 0.07 v5.3.0, 0.19 v5.2.0, 0.00 v5.0.0, 0.08 v4.1.0, 0.13 v4.0.0, 0.12 v3.7.0, 0.14 v3.5.0, 0.00 v3.4.0, 0.08 v3.3.0, 0.11 v3.2.0, 0.22 v3.1.0, 0.17 v2.7.0, 0.00 v2.5.0, 0.33 v2.4.0, 0.33 v2.2.1, 0.00 v2.1.0 24 | % Syntax : Number of formulae : 14 ( 6 unit) 25 | % Number of atoms : 24 ( 5 equality) 26 | % Maximal formula depth : 5 ( 3 average) 27 | % Number of connectives : 16 ( 6 ~; 2 |; 1 &) 28 | % ( 0 <=>; 7 =>; 0 <=; 0 <~>) 29 | % ( 0 ~|; 0 ~&) 30 | % Number of predicates : 5 ( 0 propositional; 1-2 arity) 31 | % Number of functors : 3 ( 3 constant; 0-0 arity) 32 | % Number of variables : 12 ( 0 sgn; 10 !; 2 ?) 33 | % Maximal term depth : 1 ( 1 average) 34 | % SPC : FOF_THM_RFO_SEQ 35 | 36 | % Comments : Modified by Geoff Sutcliffe. 37 | % : Also known as "Who killed Aunt Agatha" 38 | %------------------------------------------------------------------------------ 39 | %----Problem axioms 40 | fof(pel55_1,axiom, 41 | ( ? [X] : 42 | ( lives(X) 43 | & killed(X,agatha) ) )). 44 | 45 | fof(pel55_2_1,axiom, 46 | ( lives(agatha) )). 47 | 48 | fof(pel55_2_2,axiom, 49 | ( lives(butler) )). 50 | 51 | fof(pel55_2_3,axiom, 52 | ( lives(charles) )). 53 | 54 | fof(pel55_3,axiom, 55 | ( ! [X] : 56 | ( lives(X) 57 | => ( X = agatha 58 | | X = butler 59 | | X = charles ) ) )). 60 | 61 | fof(pel55_4,axiom, 62 | ( ! [X,Y] : 63 | ( killed(X,Y) 64 | => hates(X,Y) ) )). 65 | 66 | fof(pel55_5,axiom, 67 | ( ! [X,Y] : 68 | ( killed(X,Y) 69 | => ~ richer(X,Y) ) )). 70 | 71 | fof(pel55_6,axiom, 72 | ( ! [X] : 73 | ( hates(agatha,X) 74 | => ~ hates(charles,X) ) )). 75 | 76 | fof(pel55_7,axiom, 77 | ( ! [X] : 78 | ( X != butler 79 | => hates(agatha,X) ) )). 80 | 81 | fof(pel55_8,axiom, 82 | ( ! [X] : 83 | ( ~ richer(X,agatha) 84 | => hates(butler,X) ) )). 85 | 86 | fof(pel55_9,axiom, 87 | ( ! [X] : 88 | ( hates(agatha,X) 89 | => hates(butler,X) ) )). 90 | 91 | fof(pel55_10,axiom, 92 | ( ! [X] : 93 | ? [Y] : ~ hates(X,Y) )). 94 | 95 | fof(pel55_11,axiom, 96 | ( agatha != butler )). 97 | 98 | fof(pel55,conjecture, 99 | ( killed(agatha,agatha) )). 100 | 101 | %------------------------------------------------------------------------------ 102 | -------------------------------------------------------------------------------- /EXAMPLES/PUZ001-1.p: -------------------------------------------------------------------------------- 1 | %------------------------------------------------------------------------------ 2 | % File : PUZ001-1 : TPTP v4.1.0. Released v1.0.0. 3 | % Domain : Puzzles 4 | % Problem : Dreadbury Mansion 5 | % Version : Especial. 6 | % Theorem formulation : Made unsatisfiable. 7 | % English : Someone who lives in Dreadbury Mansion killed Aunt Agatha. 8 | % Agatha, the butler, and Charles live in Dreadbury Mansion, 9 | % and are the only people who live therein. A killer always 10 | % hates his victim, and is never richer than his victim. 11 | % Charles hates no one that Aunt Agatha hates. Agatha hates 12 | % everyone except the butler. The butler hates everyone not 13 | % richer than Aunt Agatha. The butler hates everyone Aunt 14 | % Agatha hates. No one hates everyone. Agatha is not the 15 | % butler. Therefore : Agatha killed herself. 16 | 17 | % Refs : [Pel86] Pelletier (1986), Seventy-five Problems for Testing Au 18 | % : [MB88] Manthey & Bry (1988), SATCHMO: A Theorem Prover Implem 19 | % Source : [TPTP] 20 | % Names : 21 | 22 | % Status : Unsatisfiable 23 | % Rating : 0.00 v2.0.0 24 | % Syntax : Number of clauses : 12 ( 2 non-Horn; 5 unit; 12 RR) 25 | % Number of atoms : 21 ( 0 equality) 26 | % Maximal clause size : 3 ( 2 average) 27 | % Number of predicates : 4 ( 0 propositional; 1-2 arity) 28 | % Number of functors : 3 ( 3 constant; 0-0 arity) 29 | % Number of variables : 8 ( 0 singleton) 30 | % Maximal term depth : 1 ( 1 average) 31 | % SPC : CNF_UNS_EPR 32 | 33 | % Comments : Modified from the [MB88] version to be unsatisfiable, by Geoff 34 | % Sutcliffe. 35 | % : Also known as "Who killed Aunt Agatha" 36 | %------------------------------------------------------------------------------ 37 | cnf(agatha,hypothesis, 38 | ( lives(agatha) )). 39 | 40 | cnf(butler,hypothesis, 41 | ( lives(butler) )). 42 | 43 | cnf(charles,hypothesis, 44 | ( lives(charles) )). 45 | 46 | cnf(poorer_killer,hypothesis, 47 | ( ~ killed(X,Y) 48 | | ~ richer(X,Y) )). 49 | 50 | cnf(different_hates,hypothesis, 51 | ( ~ hates(agatha,X) 52 | | ~ hates(charles,X) )). 53 | 54 | cnf(no_one_hates_everyone,hypothesis, 55 | ( ~ hates(X,agatha) 56 | | ~ hates(X,butler) 57 | | ~ hates(X,charles) )). 58 | 59 | cnf(agatha_hates_agatha,hypothesis, 60 | ( hates(agatha,agatha) )). 61 | 62 | cnf(agatha_hates_charles,hypothesis, 63 | ( hates(agatha,charles) )). 64 | 65 | cnf(killer_hates_victim,hypothesis, 66 | ( ~ killed(X,Y) 67 | | hates(X,Y) )). 68 | 69 | cnf(same_hates,hypothesis, 70 | ( ~ hates(agatha,X) 71 | | hates(butler,X) )). 72 | 73 | cnf(butler_hates_poor,hypothesis, 74 | ( ~ lives(X) 75 | | richer(X,agatha) 76 | | hates(butler,X) )). 77 | 78 | %----Literal dropped from here to make it unsatisfiable 79 | cnf(prove_neither_charles_nor_butler_did_it,negated_conjecture, 80 | ( killed(butler,agatha) 81 | | killed(charles,agatha) )). 82 | 83 | %------------------------------------------------------------------------------ 84 | -------------------------------------------------------------------------------- /EXAMPLES/PUZ002-1.p: -------------------------------------------------------------------------------- 1 | %-------------------------------------------------------------------------- 2 | % File : PUZ002-1 : TPTP v4.1.0. Released v1.0.0. 3 | % Domain : Puzzles 4 | % Problem : The Animals Puzzle 5 | % Version : Especial. 6 | % English : 1) The only animals in this house are cats. 7 | % 2) Every animal is suitable for a pet, that loves to gaze at 8 | % the moon. 9 | % 3) When I detest an animal, I avoid it. 10 | % 4) No animals are carnivorous, unless they prowl at night. 11 | % 5) No cat fails to kill mice. 12 | % 6) No animals ever take to me, except what are in this house. 13 | % 7) Kangaroos are not suitable for pets. 14 | % 8) None but carnivora kill mice. 15 | % 9) I detest animals that do not take to me. 16 | % 10) Animals that prowl at night always love to gaze at the moon. 17 | % The problem is to prove that "I always avoid a kangaroo". 18 | 19 | % Refs : [Car86] Carroll (1986), Lewis Carroll's Symbolic Logic 20 | % Source : [ANL] 21 | % Names : animals.ver1.in [ANL] 22 | 23 | % Status : Unsatisfiable 24 | % Rating : 0.00 v2.0.0 25 | % Syntax : Number of clauses : 12 ( 1 non-Horn; 2 unit; 11 RR) 26 | % Number of atoms : 22 ( 0 equality) 27 | % Maximal clause size : 2 ( 2 average) 28 | % Number of predicates : 11 ( 0 propositional; 1-1 arity) 29 | % Number of functors : 1 ( 1 constant; 0-0 arity) 30 | % Number of variables : 10 ( 0 singleton) 31 | % Maximal term depth : 1 ( 1 average) 32 | % SPC : CNF_UNS_EPR 33 | 34 | % Comments : 35 | %-------------------------------------------------------------------------- 36 | cnf(only_cats_in_house,axiom, 37 | ( ~ in_house(Cat) 38 | | cat(Cat) )). 39 | 40 | cnf(gazers_are_suitable_pets,axiom, 41 | ( ~ gazer(Gazer) 42 | | suitable_pet(Gazer) )). 43 | 44 | cnf(avoid_detested,axiom, 45 | ( ~ detested(Detested) 46 | | avoided(Detested) )). 47 | 48 | cnf(carnivores_are_prowlers,axiom, 49 | ( ~ carnivore(Carnivore) 50 | | prowler(Carnivore) )). 51 | 52 | cnf(cats_are_mice_killers,axiom, 53 | ( ~ cat(Cat) 54 | | mouse_killer(Cat) )). 55 | 56 | cnf(in_house_if_takes_to_me,axiom, 57 | ( ~ takes_to_me(Taken_animal) 58 | | in_house(Taken_animal) )). 59 | 60 | cnf(kangaroos_are_not_pets,axiom, 61 | ( ~ kangaroo(Kangaroo) 62 | | ~ suitable_pet(Kangaroo) )). 63 | 64 | cnf(mouse_killers_are_carnivores,axiom, 65 | ( ~ mouse_killer(Killer) 66 | | carnivore(Killer) )). 67 | 68 | cnf(takes_to_me_or_detested,axiom, 69 | ( takes_to_me(Animal) 70 | | detested(Animal) )). 71 | 72 | cnf(prowlers_are_gazers,axiom, 73 | ( ~ prowler(Prowler) 74 | | gazer(Prowler) )). 75 | 76 | cnf(kangaroo_is_a_kangaroo,axiom, 77 | ( kangaroo(the_kangaroo) )). 78 | 79 | cnf(avoid_kangaroo,negated_conjecture, 80 | ( ~ avoided(the_kangaroo) )). 81 | 82 | %-------------------------------------------------------------------------- 83 | -------------------------------------------------------------------------------- /EXAMPLES/PUZ003-1.p: -------------------------------------------------------------------------------- 1 | %-------------------------------------------------------------------------- 2 | % File : PUZ003-1 : TPTP v4.1.0. Released v1.0.0. 3 | % Domain : Puzzles 4 | % Problem : The Barber Puzzle 5 | % Version : Especial. 6 | % English : There is a barbers' club that obeys the following three 7 | % conditions: 8 | % (1) If any member A has shaved any other member B - whether 9 | % himself or another - then all members have shaved A, 10 | % though not necessarily at the same time. 11 | % (2) Four of the members are named Guido, Lorenzo, Petrucio, 12 | % and Cesare. 13 | % (3) Guido has shaved Cesare. 14 | % Prove Petrucio has shaved Lorenzo 15 | 16 | % Refs : 17 | % Source : [ANL] 18 | % Names : barber.ver1.in [ANL] 19 | 20 | % Status : Unsatisfiable 21 | % Rating : 0.00 v2.0.0 22 | % Syntax : Number of clauses : 8 ( 0 non-Horn; 6 unit; 8 RR) 23 | % Number of atoms : 13 ( 0 equality) 24 | % Maximal clause size : 4 ( 2 average) 25 | % Number of predicates : 2 ( 0 propositional; 1-2 arity) 26 | % Number of functors : 5 ( 5 constant; 0-0 arity) 27 | % Number of variables : 4 ( 0 singleton) 28 | % Maximal term depth : 1 ( 1 average) 29 | % SPC : CNF_UNS_EPR 30 | 31 | % Comments : 32 | %-------------------------------------------------------------------------- 33 | cnf(one_shaved_then_all_shaved,axiom, 34 | ( ~ member(X) 35 | | ~ member(Y) 36 | | ~ shaved(X,Y) 37 | | shaved(members,X) )). 38 | 39 | cnf(all_shaved_then_one_shaved,axiom, 40 | ( ~ shaved(members,X) 41 | | ~ member(Y) 42 | | shaved(Y,X) )). 43 | 44 | cnf(guido,hypothesis, 45 | ( member(guido) )). 46 | 47 | cnf(lorenzo,hypothesis, 48 | ( member(lorenzo) )). 49 | 50 | cnf(petruchio,hypothesis, 51 | ( member(petruchio) )). 52 | 53 | cnf(cesare,hypothesis, 54 | ( member(cesare) )). 55 | 56 | cnf(guido_has_shaved_cesare,hypothesis, 57 | ( shaved(guido,cesare) )). 58 | 59 | cnf(prove_petruchio_has_shaved_lorenzo,negated_conjecture, 60 | ( ~ shaved(petruchio,lorenzo) )). 61 | 62 | %-------------------------------------------------------------------------- 63 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------ 2 | # 3 | # File : Makefile for FOD-PI example prover. 4 | # 5 | # Author: Stephan Schulz 6 | # 7 | # Changes 8 | # 9 | # <1> Mon Sep 19 13:50:35 CEST 2011 10 | # New 11 | # 12 | #------------------------------------------------------------------------ 13 | 14 | STAREXECPATH=$(HOME)/StarExec 15 | VERSION= 16 | 17 | 18 | all: 19 | 20 | clean: 21 | -rm -f *.pyc *~ 22 | 23 | 24 | testcov: *.py 25 | -rm -r .coverage COVERAGE 26 | for f in *.py ;do coverage-3.8 run -a $$f; done 27 | coverage-3.8 report > testcov 28 | mkdir COVERAGE 29 | coverage-3.8 annotate -d COVERAGE 30 | cat testcov 31 | 32 | 33 | tags: TAGS 34 | 35 | TAGS: *.py 36 | etags *.py 37 | 38 | 39 | 40 | distrib: clean 41 | cd ..; tar czf PyRes.tgz PyRes 42 | 43 | starexec: clean 44 | echo $(STAREXECPATH) 45 | rm -rf $(STAREXECPATH) 46 | mkdir -p $(STAREXECPATH)/bin 47 | find . -name ".#*" -exec rm {} \; 48 | cp *.py starexec_run_PyRes_default $(STAREXECPATH)/bin 49 | cp README $(STAREXECPATH) 50 | cd $(STAREXECPATH); zip -r PyRes$(VERSION).zip bin 51 | -------------------------------------------------------------------------------- /PyRes-1.0.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |

4 | 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 | --------------------------------------------------------------------------------