prover9.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510
  1. # Natural Language Toolkit: Interface to the Prover9 Theorem Prover
  2. #
  3. # Copyright (C) 2001-2019 NLTK Project
  4. # Author: Dan Garrette <dhgarrette@gmail.com>
  5. # Ewan Klein <ewan@inf.ed.ac.uk>
  6. #
  7. # URL: <http://nltk.org/>
  8. # For license information, see LICENSE.TXT
  9. """
  10. A theorem prover that makes use of the external 'Prover9' package.
  11. """
  12. from __future__ import print_function
  13. import os
  14. import subprocess
  15. import nltk
  16. from nltk.sem.logic import (
  17. Expression,
  18. ExistsExpression,
  19. AllExpression,
  20. NegatedExpression,
  21. AndExpression,
  22. IffExpression,
  23. OrExpression,
  24. EqualityExpression,
  25. ImpExpression,
  26. )
  27. from nltk.inference.api import BaseProverCommand, Prover
  28. #
  29. # Following is not yet used. Return code for 2 actually realized as 512.
  30. #
  31. p9_return_codes = {
  32. 0: True,
  33. 1: "(FATAL)", # A fatal error occurred (user's syntax error).
  34. 2: False, # (SOS_EMPTY) Prover9 ran out of things to do
  35. # (sos list exhausted).
  36. 3: "(MAX_MEGS)", # The max_megs (memory limit) parameter was exceeded.
  37. 4: "(MAX_SECONDS)", # The max_seconds parameter was exceeded.
  38. 5: "(MAX_GIVEN)", # The max_given parameter was exceeded.
  39. 6: "(MAX_KEPT)", # The max_kept parameter was exceeded.
  40. 7: "(ACTION)", # A Prover9 action terminated the search.
  41. 101: "(SIGSEGV)", # Prover9 crashed, most probably due to a bug.
  42. }
  43. class Prover9CommandParent(object):
  44. """
  45. A common base class used by both ``Prover9Command`` and ``MaceCommand``,
  46. which is responsible for maintaining a goal and a set of assumptions,
  47. and generating prover9-style input files from them.
  48. """
  49. def print_assumptions(self, output_format='nltk'):
  50. """
  51. Print the list of the current assumptions.
  52. """
  53. if output_format.lower() == 'nltk':
  54. for a in self.assumptions():
  55. print(a)
  56. elif output_format.lower() == 'prover9':
  57. for a in convert_to_prover9(self.assumptions()):
  58. print(a)
  59. else:
  60. raise NameError(
  61. "Unrecognized value for 'output_format': %s" % output_format
  62. )
  63. class Prover9Command(Prover9CommandParent, BaseProverCommand):
  64. """
  65. A ``ProverCommand`` specific to the ``Prover9`` prover. It contains
  66. the a print_assumptions() method that is used to print the list
  67. of assumptions in multiple formats.
  68. """
  69. def __init__(self, goal=None, assumptions=None, timeout=60, prover=None):
  70. """
  71. :param goal: Input expression to prove
  72. :type goal: sem.Expression
  73. :param assumptions: Input expressions to use as assumptions in
  74. the proof.
  75. :type assumptions: list(sem.Expression)
  76. :param timeout: number of seconds before timeout; set to 0 for
  77. no timeout.
  78. :type timeout: int
  79. :param prover: a prover. If not set, one will be created.
  80. :type prover: Prover9
  81. """
  82. if not assumptions:
  83. assumptions = []
  84. if prover is not None:
  85. assert isinstance(prover, Prover9)
  86. else:
  87. prover = Prover9(timeout)
  88. BaseProverCommand.__init__(self, prover, goal, assumptions)
  89. def decorate_proof(self, proof_string, simplify=True):
  90. """
  91. :see BaseProverCommand.decorate_proof()
  92. """
  93. if simplify:
  94. return self._prover._call_prooftrans(proof_string, ['striplabels'])[
  95. 0
  96. ].rstrip()
  97. else:
  98. return proof_string.rstrip()
  99. class Prover9Parent(object):
  100. """
  101. A common class extended by both ``Prover9`` and ``Mace <mace.Mace>``.
  102. It contains the functionality required to convert NLTK-style
  103. expressions into Prover9-style expressions.
  104. """
  105. _binary_location = None
  106. def config_prover9(self, binary_location, verbose=False):
  107. if binary_location is None:
  108. self._binary_location = None
  109. self._prover9_bin = None
  110. else:
  111. name = 'prover9'
  112. self._prover9_bin = nltk.internals.find_binary(
  113. name,
  114. path_to_bin=binary_location,
  115. env_vars=['PROVER9'],
  116. url='http://www.cs.unm.edu/~mccune/prover9/',
  117. binary_names=[name, name + '.exe'],
  118. verbose=verbose,
  119. )
  120. self._binary_location = self._prover9_bin.rsplit(os.path.sep, 1)
  121. def prover9_input(self, goal, assumptions):
  122. """
  123. :return: The input string that should be provided to the
  124. prover9 binary. This string is formed based on the goal,
  125. assumptions, and timeout value of this object.
  126. """
  127. s = ''
  128. if assumptions:
  129. s += 'formulas(assumptions).\n'
  130. for p9_assumption in convert_to_prover9(assumptions):
  131. s += ' %s.\n' % p9_assumption
  132. s += 'end_of_list.\n\n'
  133. if goal:
  134. s += 'formulas(goals).\n'
  135. s += ' %s.\n' % convert_to_prover9(goal)
  136. s += 'end_of_list.\n\n'
  137. return s
  138. def binary_locations(self):
  139. """
  140. A list of directories that should be searched for the prover9
  141. executables. This list is used by ``config_prover9`` when searching
  142. for the prover9 executables.
  143. """
  144. return [
  145. '/usr/local/bin/prover9',
  146. '/usr/local/bin/prover9/bin',
  147. '/usr/local/bin',
  148. '/usr/bin',
  149. '/usr/local/prover9',
  150. '/usr/local/share/prover9',
  151. ]
  152. def _find_binary(self, name, verbose=False):
  153. binary_locations = self.binary_locations()
  154. if self._binary_location is not None:
  155. binary_locations += [self._binary_location]
  156. return nltk.internals.find_binary(
  157. name,
  158. searchpath=binary_locations,
  159. env_vars=['PROVER9'],
  160. url='http://www.cs.unm.edu/~mccune/prover9/',
  161. binary_names=[name, name + '.exe'],
  162. verbose=verbose,
  163. )
  164. def _call(self, input_str, binary, args=[], verbose=False):
  165. """
  166. Call the binary with the given input.
  167. :param input_str: A string whose contents are used as stdin.
  168. :param binary: The location of the binary to call
  169. :param args: A list of command-line arguments.
  170. :return: A tuple (stdout, returncode)
  171. :see: ``config_prover9``
  172. """
  173. if verbose:
  174. print('Calling:', binary)
  175. print('Args:', args)
  176. print('Input:\n', input_str, '\n')
  177. # Call prover9 via a subprocess
  178. cmd = [binary] + args
  179. try:
  180. input_str = input_str.encode("utf8")
  181. except AttributeError:
  182. pass
  183. p = subprocess.Popen(
  184. cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=subprocess.PIPE
  185. )
  186. (stdout, stderr) = p.communicate(input=input_str)
  187. if verbose:
  188. print('Return code:', p.returncode)
  189. if stdout:
  190. print('stdout:\n', stdout, '\n')
  191. if stderr:
  192. print('stderr:\n', stderr, '\n')
  193. return (stdout.decode("utf-8"), p.returncode)
  194. def convert_to_prover9(input):
  195. """
  196. Convert a ``logic.Expression`` to Prover9 format.
  197. """
  198. if isinstance(input, list):
  199. result = []
  200. for s in input:
  201. try:
  202. result.append(_convert_to_prover9(s.simplify()))
  203. except:
  204. print('input %s cannot be converted to Prover9 input syntax' % input)
  205. raise
  206. return result
  207. else:
  208. try:
  209. return _convert_to_prover9(input.simplify())
  210. except:
  211. print('input %s cannot be converted to Prover9 input syntax' % input)
  212. raise
  213. def _convert_to_prover9(expression):
  214. """
  215. Convert ``logic.Expression`` to Prover9 formatted string.
  216. """
  217. if isinstance(expression, ExistsExpression):
  218. return (
  219. 'exists '
  220. + str(expression.variable)
  221. + ' '
  222. + _convert_to_prover9(expression.term)
  223. )
  224. elif isinstance(expression, AllExpression):
  225. return (
  226. 'all '
  227. + str(expression.variable)
  228. + ' '
  229. + _convert_to_prover9(expression.term)
  230. )
  231. elif isinstance(expression, NegatedExpression):
  232. return '-(' + _convert_to_prover9(expression.term) + ')'
  233. elif isinstance(expression, AndExpression):
  234. return (
  235. '('
  236. + _convert_to_prover9(expression.first)
  237. + ' & '
  238. + _convert_to_prover9(expression.second)
  239. + ')'
  240. )
  241. elif isinstance(expression, OrExpression):
  242. return (
  243. '('
  244. + _convert_to_prover9(expression.first)
  245. + ' | '
  246. + _convert_to_prover9(expression.second)
  247. + ')'
  248. )
  249. elif isinstance(expression, ImpExpression):
  250. return (
  251. '('
  252. + _convert_to_prover9(expression.first)
  253. + ' -> '
  254. + _convert_to_prover9(expression.second)
  255. + ')'
  256. )
  257. elif isinstance(expression, IffExpression):
  258. return (
  259. '('
  260. + _convert_to_prover9(expression.first)
  261. + ' <-> '
  262. + _convert_to_prover9(expression.second)
  263. + ')'
  264. )
  265. elif isinstance(expression, EqualityExpression):
  266. return (
  267. '('
  268. + _convert_to_prover9(expression.first)
  269. + ' = '
  270. + _convert_to_prover9(expression.second)
  271. + ')'
  272. )
  273. else:
  274. return str(expression)
  275. class Prover9(Prover9Parent, Prover):
  276. _prover9_bin = None
  277. _prooftrans_bin = None
  278. def __init__(self, timeout=60):
  279. self._timeout = timeout
  280. """The timeout value for prover9. If a proof can not be found
  281. in this amount of time, then prover9 will return false.
  282. (Use 0 for no timeout.)"""
  283. def _prove(self, goal=None, assumptions=None, verbose=False):
  284. """
  285. Use Prover9 to prove a theorem.
  286. :return: A pair whose first element is a boolean indicating if the
  287. proof was successful (i.e. returns value of 0) and whose second element
  288. is the output of the prover.
  289. """
  290. if not assumptions:
  291. assumptions = []
  292. stdout, returncode = self._call_prover9(
  293. self.prover9_input(goal, assumptions), verbose=verbose
  294. )
  295. return (returncode == 0, stdout)
  296. def prover9_input(self, goal, assumptions):
  297. """
  298. :see: Prover9Parent.prover9_input
  299. """
  300. s = 'clear(auto_denials).\n' # only one proof required
  301. return s + Prover9Parent.prover9_input(self, goal, assumptions)
  302. def _call_prover9(self, input_str, args=[], verbose=False):
  303. """
  304. Call the ``prover9`` binary with the given input.
  305. :param input_str: A string whose contents are used as stdin.
  306. :param args: A list of command-line arguments.
  307. :return: A tuple (stdout, returncode)
  308. :see: ``config_prover9``
  309. """
  310. if self._prover9_bin is None:
  311. self._prover9_bin = self._find_binary('prover9', verbose)
  312. updated_input_str = ''
  313. if self._timeout > 0:
  314. updated_input_str += 'assign(max_seconds, %d).\n\n' % self._timeout
  315. updated_input_str += input_str
  316. stdout, returncode = self._call(
  317. updated_input_str, self._prover9_bin, args, verbose
  318. )
  319. if returncode not in [0, 2]:
  320. errormsgprefix = '%%ERROR:'
  321. if errormsgprefix in stdout:
  322. msgstart = stdout.index(errormsgprefix)
  323. errormsg = stdout[msgstart:].strip()
  324. else:
  325. errormsg = None
  326. if returncode in [3, 4, 5, 6]:
  327. raise Prover9LimitExceededException(returncode, errormsg)
  328. else:
  329. raise Prover9FatalException(returncode, errormsg)
  330. return stdout, returncode
  331. def _call_prooftrans(self, input_str, args=[], verbose=False):
  332. """
  333. Call the ``prooftrans`` binary with the given input.
  334. :param input_str: A string whose contents are used as stdin.
  335. :param args: A list of command-line arguments.
  336. :return: A tuple (stdout, returncode)
  337. :see: ``config_prover9``
  338. """
  339. if self._prooftrans_bin is None:
  340. self._prooftrans_bin = self._find_binary('prooftrans', verbose)
  341. return self._call(input_str, self._prooftrans_bin, args, verbose)
  342. class Prover9Exception(Exception):
  343. def __init__(self, returncode, message):
  344. msg = p9_return_codes[returncode]
  345. if message:
  346. msg += '\n%s' % message
  347. Exception.__init__(self, msg)
  348. class Prover9FatalException(Prover9Exception):
  349. pass
  350. class Prover9LimitExceededException(Prover9Exception):
  351. pass
  352. ######################################################################
  353. # { Tests and Demos
  354. ######################################################################
  355. def test_config():
  356. a = Expression.fromstring('(walk(j) & sing(j))')
  357. g = Expression.fromstring('walk(j)')
  358. p = Prover9Command(g, assumptions=[a])
  359. p._executable_path = None
  360. p.prover9_search = []
  361. p.prove()
  362. # config_prover9('/usr/local/bin')
  363. print(p.prove())
  364. print(p.proof())
  365. def test_convert_to_prover9(expr):
  366. """
  367. Test that parsing works OK.
  368. """
  369. for t in expr:
  370. e = Expression.fromstring(t)
  371. print(convert_to_prover9(e))
  372. def test_prove(arguments):
  373. """
  374. Try some proofs and exhibit the results.
  375. """
  376. for (goal, assumptions) in arguments:
  377. g = Expression.fromstring(goal)
  378. alist = [Expression.fromstring(a) for a in assumptions]
  379. p = Prover9Command(g, assumptions=alist).prove()
  380. for a in alist:
  381. print(' %s' % a)
  382. print('|- %s: %s\n' % (g, p))
  383. arguments = [
  384. ('(man(x) <-> (not (not man(x))))', []),
  385. ('(not (man(x) & (not man(x))))', []),
  386. ('(man(x) | (not man(x)))', []),
  387. ('(man(x) & (not man(x)))', []),
  388. ('(man(x) -> man(x))', []),
  389. ('(not (man(x) & (not man(x))))', []),
  390. ('(man(x) | (not man(x)))', []),
  391. ('(man(x) -> man(x))', []),
  392. ('(man(x) <-> man(x))', []),
  393. ('(not (man(x) <-> (not man(x))))', []),
  394. ('mortal(Socrates)', ['all x.(man(x) -> mortal(x))', 'man(Socrates)']),
  395. ('((all x.(man(x) -> walks(x)) & man(Socrates)) -> some y.walks(y))', []),
  396. ('(all x.man(x) -> all x.man(x))', []),
  397. ('some x.all y.sees(x,y)', []),
  398. (
  399. 'some e3.(walk(e3) & subj(e3, mary))',
  400. [
  401. 'some e1.(see(e1) & subj(e1, john) & some e2.(pred(e1, e2) & walk(e2) & subj(e2, mary)))'
  402. ],
  403. ),
  404. (
  405. 'some x e1.(see(e1) & subj(e1, x) & some e2.(pred(e1, e2) & walk(e2) & subj(e2, mary)))',
  406. [
  407. 'some e1.(see(e1) & subj(e1, john) & some e2.(pred(e1, e2) & walk(e2) & subj(e2, mary)))'
  408. ],
  409. ),
  410. ]
  411. expressions = [
  412. r'some x y.sees(x,y)',
  413. r'some x.(man(x) & walks(x))',
  414. r'\x.(man(x) & walks(x))',
  415. r'\x y.sees(x,y)',
  416. r'walks(john)',
  417. r'\x.big(x, \y.mouse(y))',
  418. r'(walks(x) & (runs(x) & (threes(x) & fours(x))))',
  419. r'(walks(x) -> runs(x))',
  420. r'some x.(PRO(x) & sees(John, x))',
  421. r'some x.(man(x) & (not walks(x)))',
  422. r'all x.(man(x) -> walks(x))',
  423. ]
  424. def spacer(num=45):
  425. print('-' * num)
  426. def demo():
  427. print("Testing configuration")
  428. spacer()
  429. test_config()
  430. print()
  431. print("Testing conversion to Prover9 format")
  432. spacer()
  433. test_convert_to_prover9(expressions)
  434. print()
  435. print("Testing proofs")
  436. spacer()
  437. test_prove(arguments)
  438. if __name__ == '__main__':
  439. demo()