unicode-logic-kit 0.31.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. unicode_logic_kit/__init__.py +385 -0
  2. unicode_logic_kit/__main__.py +520 -0
  3. unicode_logic_kit/_deadline.py +219 -0
  4. unicode_logic_kit/ace/__init__.py +126 -0
  5. unicode_logic_kit/ace/_align.py +135 -0
  6. unicode_logic_kit/ace/chem_lexicon.py +128 -0
  7. unicode_logic_kit/ace/drs_reader.py +570 -0
  8. unicode_logic_kit/ace/mapping.py +666 -0
  9. unicode_logic_kit/ace/reverse_modal.py +138 -0
  10. unicode_logic_kit/ace/runner.py +551 -0
  11. unicode_logic_kit/ace/translate.py +452 -0
  12. unicode_logic_kit/ace/verbalize.py +1070 -0
  13. unicode_logic_kit/api.py +1284 -0
  14. unicode_logic_kit/atp/__init__.py +177 -0
  15. unicode_logic_kit/atp/_ascii_names.py +113 -0
  16. unicode_logic_kit/atp/_html.py +72 -0
  17. unicode_logic_kit/atp/_substructural_input.py +228 -0
  18. unicode_logic_kit/atp/_tff_problem.py +715 -0
  19. unicode_logic_kit/atp/_tptp_problem.py +1111 -0
  20. unicode_logic_kit/atp/_writer_support.py +289 -0
  21. unicode_logic_kit/atp/clingo_backend.py +1180 -0
  22. unicode_logic_kit/atp/cvc5_backend.py +1385 -0
  23. unicode_logic_kit/atp/eprover_backend.py +732 -0
  24. unicode_logic_kit/atp/finite_domain.py +1055 -0
  25. unicode_logic_kit/atp/fitch.py +1547 -0
  26. unicode_logic_kit/atp/fitch_search.py +551 -0
  27. unicode_logic_kit/atp/hets_backend.py +339 -0
  28. unicode_logic_kit/atp/hybrid_down.py +120 -0
  29. unicode_logic_kit/atp/incremental.py +250 -0
  30. unicode_logic_kit/atp/kripke_enum.py +741 -0
  31. unicode_logic_kit/atp/lambek.py +436 -0
  32. unicode_logic_kit/atp/leo3_backend.py +332 -0
  33. unicode_logic_kit/atp/linear.py +738 -0
  34. unicode_logic_kit/atp/lj.py +705 -0
  35. unicode_logic_kit/atp/logic_backends.py +566 -0
  36. unicode_logic_kit/atp/ltl_tableau.py +1084 -0
  37. unicode_logic_kit/atp/minizinc_backend.py +1402 -0
  38. unicode_logic_kit/atp/modal_tableau.py +1382 -0
  39. unicode_logic_kit/atp/nanocop_backend.py +410 -0
  40. unicode_logic_kit/atp/portfolio.py +489 -0
  41. unicode_logic_kit/atp/protocol.py +1803 -0
  42. unicode_logic_kit/atp/prover9_entailment.py +1153 -0
  43. unicode_logic_kit/atp/resolution.py +1376 -0
  44. unicode_logic_kit/atp/resolution_check.py +1114 -0
  45. unicode_logic_kit/atp/sequent.py +1050 -0
  46. unicode_logic_kit/atp/tableau.py +921 -0
  47. unicode_logic_kit/atp/tableau_check.py +543 -0
  48. unicode_logic_kit/atp/tptp_ncl.py +811 -0
  49. unicode_logic_kit/atp/tptp_tff.py +1546 -0
  50. unicode_logic_kit/atp/tstp.py +1333 -0
  51. unicode_logic_kit/atp/tstp_check.py +1096 -0
  52. unicode_logic_kit/atp/twee_backend.py +236 -0
  53. unicode_logic_kit/atp/twee_check.py +711 -0
  54. unicode_logic_kit/atp/twee_entailment.py +953 -0
  55. unicode_logic_kit/atp/vampire_entailment.py +540 -0
  56. unicode_logic_kit/atp/z3_arith.py +470 -0
  57. unicode_logic_kit/atp/z3_equivalence.py +36 -0
  58. unicode_logic_kit/atp/z3_fuzzy.py +362 -0
  59. unicode_logic_kit/atp/z3_input.py +500 -0
  60. unicode_logic_kit/atp/z3_models.py +208 -0
  61. unicode_logic_kit/chem/__init__.py +88 -0
  62. unicode_logic_kit/chem/_naming.py +284 -0
  63. unicode_logic_kit/chem/cache.py +185 -0
  64. unicode_logic_kit/chem/interop.py +244 -0
  65. unicode_logic_kit/chem/mol.py +525 -0
  66. unicode_logic_kit/chem/signature.py +112 -0
  67. unicode_logic_kit/comorphism.py +497 -0
  68. unicode_logic_kit/dl/__init__.py +384 -0
  69. unicode_logic_kit/dl/classification.py +227 -0
  70. unicode_logic_kit/dl/concepts.py +632 -0
  71. unicode_logic_kit/dl/datatypes.py +818 -0
  72. unicode_logic_kit/dl/owl_functional.py +2433 -0
  73. unicode_logic_kit/dl/owl_manchester.py +1637 -0
  74. unicode_logic_kit/dl/owl_reasoner.py +790 -0
  75. unicode_logic_kit/dl/parser.py +391 -0
  76. unicode_logic_kit/dl/tableau.py +4048 -0
  77. unicode_logic_kit/dl/translate.py +2704 -0
  78. unicode_logic_kit/drt/__init__.py +94 -0
  79. unicode_logic_kit/drt/export.py +179 -0
  80. unicode_logic_kit/drt/nodes.py +506 -0
  81. unicode_logic_kit/drt/parser.py +965 -0
  82. unicode_logic_kit/drt/resolve.py +195 -0
  83. unicode_logic_kit/drt/reverse.py +175 -0
  84. unicode_logic_kit/eval/__init__.py +106 -0
  85. unicode_logic_kit/eval/batch.py +382 -0
  86. unicode_logic_kit/eval/canonical.py +663 -0
  87. unicode_logic_kit/eval/chem_batch.py +606 -0
  88. unicode_logic_kit/eval/converses.py +200 -0
  89. unicode_logic_kit/eval/datasets/__init__.py +136 -0
  90. unicode_logic_kit/eval/datasets/_base.py +263 -0
  91. unicode_logic_kit/eval/datasets/_proofwriter_proof.py +422 -0
  92. unicode_logic_kit/eval/datasets/c3po.py +678 -0
  93. unicode_logic_kit/eval/datasets/folio.py +158 -0
  94. unicode_logic_kit/eval/datasets/fracas.py +418 -0
  95. unicode_logic_kit/eval/datasets/groves.py +191 -0
  96. unicode_logic_kit/eval/datasets/logicbench.py +467 -0
  97. unicode_logic_kit/eval/datasets/logicnli.py +303 -0
  98. unicode_logic_kit/eval/datasets/malls.py +133 -0
  99. unicode_logic_kit/eval/datasets/pfolio.py +594 -0
  100. unicode_logic_kit/eval/datasets/pmb.py +242 -0
  101. unicode_logic_kit/eval/datasets/prontoqa.py +611 -0
  102. unicode_logic_kit/eval/datasets/proofwriter.py +1431 -0
  103. unicode_logic_kit/eval/datasets/proverqa.py +674 -0
  104. unicode_logic_kit/eval/datasets/willow.py +478 -0
  105. unicode_logic_kit/eval/equivalence.py +466 -0
  106. unicode_logic_kit/eval/exercise_gen.py +533 -0
  107. unicode_logic_kit/eval/explain.py +791 -0
  108. unicode_logic_kit/eval/generality.py +750 -0
  109. unicode_logic_kit/eval/metric_hf.py +458 -0
  110. unicode_logic_kit/eval/predicate_match.py +343 -0
  111. unicode_logic_kit/eval/theory_check.py +1170 -0
  112. unicode_logic_kit/eval/validate.py +306 -0
  113. unicode_logic_kit/fol/__init__.py +177 -0
  114. unicode_logic_kit/fol/_atom_keys.py +510 -0
  115. unicode_logic_kit/fol/_fol_nodes.py +3586 -0
  116. unicode_logic_kit/fol/_free_parameters.py +105 -0
  117. unicode_logic_kit/fol/_ho_nodes.py +448 -0
  118. unicode_logic_kit/fol/_hybrid_nodes.py +308 -0
  119. unicode_logic_kit/fol/_identifiers.py +1091 -0
  120. unicode_logic_kit/fol/_lambek_nodes.py +112 -0
  121. unicode_logic_kit/fol/_linear_nodes.py +352 -0
  122. unicode_logic_kit/fol/_modal_nodes.py +1467 -0
  123. unicode_logic_kit/fol/_msfl_nodes.py +2196 -0
  124. unicode_logic_kit/fol/_numeral_symbols.py +231 -0
  125. unicode_logic_kit/fol/_so_nodes.py +200 -0
  126. unicode_logic_kit/fol/_symbol_names.py +81 -0
  127. unicode_logic_kit/fol/_team_nodes.py +181 -0
  128. unicode_logic_kit/fol/_tptp_symbols.py +551 -0
  129. unicode_logic_kit/fol/_truth_constants.py +117 -0
  130. unicode_logic_kit/fol/casl_export.py +1135 -0
  131. unicode_logic_kit/fol/casl_import.py +929 -0
  132. unicode_logic_kit/fol/derivation.py +367 -0
  133. unicode_logic_kit/fol/dialect_detect.py +70 -0
  134. unicode_logic_kit/fol/dialect_repair.py +537 -0
  135. unicode_logic_kit/fol/frames.py +637 -0
  136. unicode_logic_kit/fol/grammars/terminals.lark +31 -0
  137. unicode_logic_kit/fol/lambda_tools.py +297 -0
  138. unicode_logic_kit/fol/latex_input.py +429 -0
  139. unicode_logic_kit/fol/modal_translation.py +944 -0
  140. unicode_logic_kit/fol/msflparser.py +1033 -0
  141. unicode_logic_kit/fol/naming.py +422 -0
  142. unicode_logic_kit/fol/nodes.py +241 -0
  143. unicode_logic_kit/fol/normalforms.py +492 -0
  144. unicode_logic_kit/fol/pal.py +287 -0
  145. unicode_logic_kit/fol/prolog_export.py +566 -0
  146. unicode_logic_kit/fol/prolog_input.py +505 -0
  147. unicode_logic_kit/fol/prover9_input.py +1325 -0
  148. unicode_logic_kit/fol/qml.py +1760 -0
  149. unicode_logic_kit/fol/qmltp_input.py +525 -0
  150. unicode_logic_kit/fol/sanitize.py +221 -0
  151. unicode_logic_kit/fol/serialize.py +79 -0
  152. unicode_logic_kit/fol/signature.py +1290 -0
  153. unicode_logic_kit/fol/simplify_check.py +544 -0
  154. unicode_logic_kit/fol/spans.py +594 -0
  155. unicode_logic_kit/fol/tptp_input.py +1503 -0
  156. unicode_logic_kit/fol/tptp_repair.py +941 -0
  157. unicode_logic_kit/fol/unification.py +157 -0
  158. unicode_logic_kit/fol/verbalize.py +263 -0
  159. unicode_logic_kit/hets/__init__.py +163 -0
  160. unicode_logic_kit/hets/bridge.py +142 -0
  161. unicode_logic_kit/hets/client.py +748 -0
  162. unicode_logic_kit/hets/docker.py +420 -0
  163. unicode_logic_kit/hets/dol.py +712 -0
  164. unicode_logic_kit/hets/haskell_json.py +355 -0
  165. unicode_logic_kit/hets/owl_backend.py +794 -0
  166. unicode_logic_kit/hets/owl_cli.py +598 -0
  167. unicode_logic_kit/hets/symbols.py +512 -0
  168. unicode_logic_kit/hol/__init__.py +140 -0
  169. unicode_logic_kit/hol/_ho_common.py +323 -0
  170. unicode_logic_kit/hol/_isabelle_binders.py +125 -0
  171. unicode_logic_kit/hol/classical.py +812 -0
  172. unicode_logic_kit/hol/deepshallow/__init__.py +45 -0
  173. unicode_logic_kit/hol/deepshallow/_common.py +177 -0
  174. unicode_logic_kit/hol/deepshallow/conditional.py +225 -0
  175. unicode_logic_kit/hol/deepshallow/intuitionistic.py +181 -0
  176. unicode_logic_kit/hol/deepshallow/modal.py +217 -0
  177. unicode_logic_kit/hol/deepshallow/qml.py +406 -0
  178. unicode_logic_kit/hol/deepshallow/relevant.py +206 -0
  179. unicode_logic_kit/hol/free.py +753 -0
  180. unicode_logic_kit/hol/goedel.py +336 -0
  181. unicode_logic_kit/hol/ho_modal.py +1743 -0
  182. unicode_logic_kit/hol/intuitionistic.py +403 -0
  183. unicode_logic_kit/hol/isabelle_conditional.py +593 -0
  184. unicode_logic_kit/hol/isabelle_modal.py +1908 -0
  185. unicode_logic_kit/hol/isabelle_relevant.py +412 -0
  186. unicode_logic_kit/hol/isabelle_runner.py +1147 -0
  187. unicode_logic_kit/hol/isabelle_substructural.py +884 -0
  188. unicode_logic_kit/hol/lean.py +1018 -0
  189. unicode_logic_kit/hol/manyvalued.py +921 -0
  190. unicode_logic_kit/hol/secondorder.py +687 -0
  191. unicode_logic_kit/hol/thf_modal.py +941 -0
  192. unicode_logic_kit/hol/thirdorder.py +397 -0
  193. unicode_logic_kit/ilp/__init__.py +89 -0
  194. unicode_logic_kit/ilp/readback.py +389 -0
  195. unicode_logic_kit/ilp/separation.py +153 -0
  196. unicode_logic_kit/ilp/task.py +730 -0
  197. unicode_logic_kit/logic.py +163 -0
  198. unicode_logic_kit/mcp/__init__.py +28 -0
  199. unicode_logic_kit/mcp/__main__.py +5 -0
  200. unicode_logic_kit/mcp/chem_tools.py +1031 -0
  201. unicode_logic_kit/mcp/server.py +2453 -0
  202. unicode_logic_kit/mcp/syntax_spec.py +681 -0
  203. unicode_logic_kit/prob/__init__.py +53 -0
  204. unicode_logic_kit/prob/_bdd.py +225 -0
  205. unicode_logic_kit/prob/_column_gen.py +668 -0
  206. unicode_logic_kit/prob/distribution.py +686 -0
  207. unicode_logic_kit/prob/nilsson.py +470 -0
  208. unicode_logic_kit/py.typed +0 -0
  209. unicode_logic_kit/semantics/__init__.py +137 -0
  210. unicode_logic_kit/semantics/_modal_reject.py +156 -0
  211. unicode_logic_kit/semantics/action_models.py +466 -0
  212. unicode_logic_kit/semantics/asp_models.py +1200 -0
  213. unicode_logic_kit/semantics/conditional.py +580 -0
  214. unicode_logic_kit/semantics/dynamic_epistemic.py +95 -0
  215. unicode_logic_kit/semantics/free_logic.py +913 -0
  216. unicode_logic_kit/semantics/fuzzy.py +384 -0
  217. unicode_logic_kit/semantics/fuzzy_kripke.py +442 -0
  218. unicode_logic_kit/semantics/intuitionistic.py +581 -0
  219. unicode_logic_kit/semantics/kripke.py +1139 -0
  220. unicode_logic_kit/semantics/manyvalued.py +580 -0
  221. unicode_logic_kit/semantics/matrix.py +342 -0
  222. unicode_logic_kit/semantics/model_eval.py +1135 -0
  223. unicode_logic_kit/semantics/modelfinder.py +1036 -0
  224. unicode_logic_kit/semantics/nonmonotonic.py +372 -0
  225. unicode_logic_kit/semantics/relevant.py +331 -0
  226. unicode_logic_kit/semantics/secondorder.py +657 -0
  227. unicode_logic_kit/semantics/structures.py +352 -0
  228. unicode_logic_kit/semantics/tarski.py +975 -0
  229. unicode_logic_kit/semantics/team.py +315 -0
  230. unicode_logic_kit/semantics/team_translation.py +416 -0
  231. unicode_logic_kit/semantics/thirdorder.py +358 -0
  232. unicode_logic_kit/semantics/tnorm.py +85 -0
  233. unicode_logic_kit/semantics/truthtable.py +201 -0
  234. unicode_logic_kit-0.31.0.dist-info/METADATA +333 -0
  235. unicode_logic_kit-0.31.0.dist-info/RECORD +237 -0
  236. unicode_logic_kit-0.31.0.dist-info/WHEEL +4 -0
  237. unicode_logic_kit-0.31.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,1153 @@
1
+ """Entailment checking via the Prover9 theorem prover (LADR backend).
2
+
3
+ Builds a Prover9 ``.in`` file (``set(prolog_style_variables)``, one
4
+ ``formulas(assumptions)`` list of premises, one ``formulas(goals)`` list
5
+ holding the conclusion) and looks for ``THEOREM PROVED`` in Prover9's stdout.
6
+
7
+ **ASCII/legality sanitisation (problem-level seam).** ``Node.to_prover9()``
8
+ renders a predicate/function name completely verbatim (no transliteration,
9
+ no case change at all) and a Constant name transliterated to ASCII
10
+ (:func:`~unicode_logic_kit.fol._fol_nodes.constant_name_to_ascii`) but without
11
+ fixing a digit-leading result (and, for a name Prover9 would read as a
12
+ variable, in double quotes: see the next section). Prover9's own reader grammar for a symbol
13
+ token is ``NAME: /[A-Za-z_][A-Za-z0-9_]*/`` (see
14
+ :data:`unicode_logic_kit.fol.prover9_input`'s ``NAME`` terminal) — a
15
+ non-ASCII character or a digit-leading name is not a legal token there at
16
+ all. Neither gap could be reached before the toolkit's own identifier
17
+ grammar was widened to accept Unicode letters and digit-leading names; both
18
+ can now. Exactly like :mod:`atp._tptp_problem`, the fix has to live at
19
+ problem level, not inside ``Node.to_prover9()`` itself (see that module's
20
+ docstring for the full reasoning — independent per-node renaming loses
21
+ injectivity and never reaches a caller that needs to invert it):
22
+ :func:`_sanitize_for_prover9` walks every premise and the conclusion
23
+ TOGETHER, replaces only the names that are not already Prover9-legal with
24
+ an ASCII, non-digit-leading, whole-problem-injective replacement, and
25
+ returns a :class:`Prover9NameMap` recording exactly what was renamed. An
26
+ already-legal name — including an upper-case-initial predicate WITH arguments,
27
+ the kit's own ``Human(x)`` convention — passes through completely unchanged,
28
+ with ONE exception, a symbol written in arity-0 position that Prover9 would read
29
+ as a variable (next section: a CONSTANT, and a NULLARY predicate), so this
30
+ sanitisation step never alters the output for a formula this module was already
31
+ exporting correctly. A name that is no word at all (a space, a dot, a hyphen, a
32
+ non-ASCII letter) is replaced by an ASCII word, each character that is no letter,
33
+ digit or underscore by the reversible escape ``uXXXX`` the transliteration of a
34
+ non-ASCII character uses; a name that begins with ``$`` is refused (below).
35
+
36
+ **One name, several symbols (the key is kind, name and arity).** Prover9 keeps ONE
37
+ symbol per spelling, and refuses a file that uses a spelling at two arities, or both
38
+ as a relation and as a function (measured on Prover9 2026-8A: ``The following symbols
39
+ are used with multiple arities: P/2, P/1`` and ``...used as both relation and function
40
+ symbols: p/0``). The kit keeps them apart: ``P(x)`` and ``P(x, y)`` are two predicates, a
41
+ nullary ``p`` a third, the constant ``p`` and the function ``p(x)`` two more, and a
42
+ sort ``S`` is the unary predicate ``S`` (ONE symbol, as everywhere in this kit). The
43
+ writer therefore keys every symbol on ``(kind, name, arity)`` (kind is ``"predicate"``
44
+ or ``"function"``; a constant is a function of arity 0, a proposition a predicate of
45
+ arity 0) and gives the FIRST symbol of a spelling that spelling, the second and every
46
+ later one a replacement that is no other symbol's spelling (``P`` at arity 2 becomes
47
+ ``P2``, a numeric suffix as everywhere else). The map records each symbol
48
+ (:attr:`Prover9NameMap.symbols`); a name that has one symbol only is looked up as
49
+ before. The comparison symbols and the arithmetic operators written infix are no
50
+ symbols of this kind, and ``$true`` / ``$false`` as nullary atoms are Prover9's truth
51
+ constants ``$T`` and ``$F``.
52
+
53
+ **Numerals.** A numeral is an uninterpreted constant here, as it is for the Z3 route
54
+ (``Number.to_z3`` names it by its value, so ``⊢ 1 ≠ 2`` is not valid for Z3 and not
55
+ provable for Prover9: Prover9 has no arithmetic either). It is ONE constant per VALUE,
56
+ written in double quotes: ``Number(1)`` and ``Number(1.0)`` are equal nodes and are both
57
+ ``"1"``, ``2.5`` is ``"2.5"`` and ``-1`` is ``"-1"`` (see
58
+ :meth:`~unicode_logic_kit.fol._fol_nodes.Number.to_prover9`), so ``P(1) ⊢ P(1.0)`` is
59
+ written ``P("1")`` and ``P("1")``. Every numeral is quoted, a non-negative integer too:
60
+ Mace4, which reads the same formula lists, takes a bare integer for a domain element of its
61
+ own and all of them for distinct elements (``1 != 2`` has no countermodel there), which the
62
+ kit's numerals are not; a quoted symbol is a plain constant for both programs. (Mace4 does not
63
+ read the whole file this writer writes: it stops at ``set(auto_denials)`` and at the
64
+ ``clear(print_...)`` flags with "Flag not recognized", measured on Mace4 2026-8A. With those
65
+ lines left out, or with ``set(prolog_style_variables)`` alone before the lists, it reads the
66
+ problem and finds a countermodel of an invalid one. The kit has no Mace4 route.) A number and a
67
+ constant spelled like it (``Number(1)`` and ``Constant('1')``, ``Number(1.0)`` and
68
+ ``Constant('1.0')``) are ONE symbol for Z3 and for the TPTP writers, so the pair is
69
+ refused by name here too.
70
+
71
+ **Upper-case constants (the variable rule).** Every file written here sets
72
+ ``prolog_style_variables``, under which Prover9 reads a symbol in TERM position
73
+ as a VARIABLE iff it begins with an upper-case letter (LADR ``symbols.c``
74
+ ``variable_name``: ``*s >= 'A' && *s <= 'Z'``; the manual: "If this flag is set,
75
+ variables in clauses start with (upper case) 'A' through 'Z'"). That applies to
76
+ every arity-0 symbol in term position, so a :class:`Constant` named ``Gaseous``
77
+ (an ontology individual looks exactly like that) written verbatim makes
78
+ ``P(Gaseous)`` read as ``for all X, P(X)`` and the route prove non-theorems.
79
+ :func:`_sanitize_for_prover9` therefore treats such a constant as NOT legal and
80
+ renames it, like a non-ASCII name, to a lower-case-initial token that is
81
+ injective over the whole problem; the rename is recorded in
82
+ :attr:`Prover9NameMap.constants` (and mirrored in ``mapping`` when the name is
83
+ not also a predicate/function name). What is NOT renamed: a predicate or
84
+ function WITH arguments (``Human(x)``, ``Gaseous(c)`` -- the symbol is not a
85
+ constant term, only its arguments are read for variables), so the kit's
86
+ Capitalised predicate convention is untouched, and the same word may be a
87
+ predicate and a constant at once (an OWL pun: class ``Person`` and individual
88
+ ``Person``) with the two getting different tokens. A leading underscore is
89
+ treated like an upper-case letter: the LADR source reads only ``A``..``Z`` and
90
+ ``_x`` is a constant to Prover9 2026-8A (measured, with the flag and without it), and to this
91
+ kit's own Prover9 reader, but the Prolog convention reads it as a variable, and the rename is
92
+ harmless where Prover9 would not have needed it.
93
+
94
+ A bare NULLARY predicate is the same shape. LADR applies ``set_vars_recurse`` to
95
+ the atom term itself, and a nullary atom IS an arity-0 term, so an upper-case
96
+ ``Rain`` is read as a variable too (measured on Prover9 2026-8A: the bare
97
+ ``Rain & Wind`` is refused, "cannot be used as atomic formulas, because they are
98
+ variables"). It is renamed by the same mechanism as a constant, to a
99
+ lower-case-initial token that is injective over the whole problem and recorded in
100
+ :attr:`Prover9NameMap.nullary_predicates` (mirrored in ``mapping`` when the word
101
+ has no other role): ``Atom("Rain", [])`` is written ``rain``. A predicate WITH
102
+ arguments (``Rain(x)``) stays as it is. A nullary predicate and a constant of the
103
+ same spelling get two different tokens, because Prover9 keeps one symbol per
104
+ spelling and refuses a file in which it is both a proposition and a constant.
105
+
106
+ The single ``Constant.to_prover9()`` and ``Atom.to_prover9()`` cannot rename (they
107
+ have no view of the other formulas), so they write such a name in double quotes
108
+ instead, ``P("Gaseous")`` and ``"Rain"``: a double-quoted symbol is never a
109
+ variable in Prover9. This writer never reaches that branch, because after the
110
+ rename no constant or proposition is variable-shaped.
111
+
112
+ **``$``-words.** A name that begins with ``$`` (a predicate, a function, a constant
113
+ or a sort) is refused by name: Prover9 keeps those words for itself (``$T``, ``$F``,
114
+ ``$ANSWER``), so a symbol spelled like one is not a name of the user's.
115
+
116
+ **The quantifier words.** ``all`` and ``exists`` are not names either. LADR reads
117
+ ``exists(X) & ...`` as the quantifier ``exists X`` followed by a stray ``&`` (measured on
118
+ Prover9 2026-8A: it echoes ``(exists W exists W &(Q(c) & R(c)))`` and refuses the file with
119
+ ``symbols used with multiple arities: &/1, &/2``), so a sort named ``exists`` (whose guard
120
+ atom is ``exists(X)``), or any predicate or function of that name, is not read as a symbol.
121
+ The writer renames such a symbol like one whose spelling is taken (``exists2``). The single
122
+ renderers write the word in double quotes. ``v``, the third identifier-shaped word of
123
+ Prover9's operator table, is read as an ordinary symbol in every position tried.
124
+
125
+ **Reserved words by arity.** Three more spellings are syntax to LADR or to a reader of its
126
+ files only at ONE number of arguments (measured on Prover9 2026-8A, with a sweep over
127
+ the words of its source): ``if`` with three arguments (LADR reads the first argument as a
128
+ formula, so ``(all W if(W, a, a))`` is refused and ``if(a, b, c)`` makes ``a`` a relation
129
+ symbol), ``end_of_list`` with none (the bare word ends a list) and ``formulas`` with one (it
130
+ is also the header of a list). The writer gives the symbol of such a name and arity a token
131
+ of its own and leaves the same word at another arity alone; the single renderers write it
132
+ in double quotes (:data:`~unicode_logic_kit.fol._fol_nodes._PROVER9_RESERVED_SYMBOLS`).
133
+
134
+ **Counting quantifiers.** ``∃≥n x φ`` is written as ``n`` distinct witnesses, which
135
+ need names. Every name the writer mints for one is fresh against EVERY name of the
136
+ whole problem, of every kind (predicate, function, constant, sort, variable), compared
137
+ case-folded because Prover9 writes a variable in upper case and reads ``X0`` and ``x0`` as
138
+ one (:func:`~unicode_logic_kit.fol._identifiers.symbol_names`), not only against the matrix it
139
+ expands, so that the expansion never rebinds a variable of the formula around it
140
+ (``(all X0 (A(X0) -> (exists X0 ...)))``).
141
+
142
+ **Re-bound binders.** LADR renames a variable that a quantifier binds inside the scope of a
143
+ quantifier of the same name (``(all W (all W P(W, x0)))``), to a symbol it picks itself,
144
+ the first of ``x0``, ``x1``, ... that is no variable in scope; it does not look at the
145
+ constants of the formula, so a constant ``x0`` is then bound by the quantifier (measured
146
+ on Prover9 2026-8A: that formula is clausified to ``P(A, A)``, and the premise
147
+ ``∀w ∀w P(w, x0)`` proves ``P(alpha, alpha)``, which it does not entail). The writer
148
+ renames a binder that sits inside the scope of one of its own name itself, to a fresh variable
149
+ (:func:`_rename_rebound_binders`; names are compared as Prover9 reads them, in upper case),
150
+ so LADR has nothing to rename, and keeps the spellings LADR picks (``x`` or ``y`` followed
151
+ by digits, with no leading zero) away from the constants and propositions of the problem
152
+ (:data:`_LADR_MINTED_VARIABLE`: ``x0`` is written ``x0_``). Two quantifiers of one name that
153
+ are siblings are no re-binding, which was measured as well, and are written as they are. The
154
+ single ``Node.to_prover9()`` makes the same renaming of the node it writes, with this very
155
+ function (and the same counting witnesses), so the text it writes means the node as well.
156
+ The Skolem names LADR gives (``c1``, ``f1``, ...) skip every symbol the file has (measured:
157
+ a constant ``c1`` is left alone and the Skolem constant becomes ``c2``), so a user symbol of
158
+ that spelling keeps it.
159
+
160
+ **Free variables.** A free variable of a problem is a PARAMETER: one unknown element, the
161
+ same in every premise and in the conclusion, which is what Z3 and the tableau read and the
162
+ assignment-wise consequence relation of the textbooks (``P(x) ⊢ P(alpha)`` is not valid:
163
+ universe {0, 1}, ``x`` = 1, ``alpha`` = 0, ``P`` = {1}). Prover9 would close each formula
164
+ universally on its own (``P(X)`` is ``∀X P(X)``), which is another question and proves it. The
165
+ writer therefore replaces every free variable, problem-wide, by a constant of a name no other
166
+ symbol of the problem has (the variable's own name when it is free), written like every other
167
+ constant, and records it in :attr:`Prover9NameMap.free_variables` (variable name to token;
168
+ :meth:`Prover9NameMap.reverse` gives the variable's name back). The text written has no free
169
+ variable: its closure is the identity.
170
+
171
+ **Łukasiewicz connectives.** ``p ∨ ¬p`` with the weak disjunction and the Łukasiewicz negation
172
+ is the maximum of ``p`` and ``1 − p``, which is ``1/2`` at ``p = 1/2``: not valid, while its
173
+ classical image is. Their classical collapse is a different logic, so the writer refuses a
174
+ formula that holds one, by name, as a single ``Node.to_prover9()`` does (``to_fol`` collapses
175
+ explicitly, and the result is written as any classical formula).
176
+
177
+ There is currently no Prover9 "detailed" route reading a proof or
178
+ countermodel back out of Prover9's own output (:func:`check_logical_entailment`
179
+ and :class:`~unicode_logic_kit.atp.protocol.Prover9Backend` both report a bare
180
+ ``bool``/PROVED-or-UNKNOWN verdict, nothing that carries a Prover9-chosen
181
+ symbol name back to the caller) — so there is no Rückweg to wire up here
182
+ today. :class:`Prover9NameMap` still exists and is still returned by
183
+ :func:`generate_prover9_input_with_mapping` (mirroring
184
+ :mod:`atp._tptp_problem`'s ``..._with_mapping``/plain-wrapper split) so a
185
+ future detailed route has the same reversible mapping available without
186
+ redesigning this module.
187
+
188
+ **A refused problem.** :func:`check_logical_entailment` returns ``False`` for
189
+ every run without a ``THEOREM PROVED`` line, and by default that includes a
190
+ problem Prover9 refused to read (its fatal-error exit, code 1 in the Prover9
191
+ manual) and a binary that could not be started (exit 127 "not found" or 126 "not
192
+ executable", which is what ``wsl.exe <path>`` reports for a path that does not
193
+ exist inside WSL). ``raise_on_rejection=True`` separates the two by raising
194
+ :class:`Prover9Rejected` with Prover9's own message;
195
+ :class:`~unicode_logic_kit.atp.protocol.Prover9Backend` passes it and reports
196
+ the refusal as an ERROR verdict rather than "found no proof". Prover9's output
197
+ is decoded as UTF-8 with undecodable bytes replaced: its fatal message quotes the
198
+ input around the error and can cut a multi-byte character in half, which must
199
+ reach the caller as a refusal with a message, not as a decoding error.
200
+
201
+ **Prover9 inside WSL.** ``use_wsl=True`` (the Prover9 backend reads it from the
202
+ ``use_wsl`` option and ``$UFK_PROVER9_WSL=1``, the way the Vampire backend does)
203
+ runs ``wsl.exe <prover9_path> -f <file>``, where ``prover9_path`` is the path
204
+ INSIDE WSL and the Windows temp file is translated with ``wslpath``. The
205
+ translation happens before the timeout window opens: a slow ``wslpath`` is a
206
+ failure to start (``OSError``), never a Prover9 timeout. Prover9 never reads the
207
+ caller's standard input (``stdin`` is the null device): it reads the problem from
208
+ stdin when it is not given ``-f``, and would block on an inherited pipe.
209
+
210
+ **A run the kit stopped.** ``timeout`` (seconds, default 30) is the wall-clock
211
+ budget handed to the subprocess; by default a run it cuts off is ``False`` like
212
+ any run without a proof. ``raise_on_timeout=True`` separates it by raising
213
+ :class:`Prover9TimedOut`, which the backend reports as UNKNOWN / ``"timeout"``
214
+ (its own ``timeout`` argument, in milliseconds, is the budget) instead of "found
215
+ no proof". Prover9's own exit codes 2-7 stay "found no proof": the kit writes no
216
+ ``max_seconds`` into the problem, so none of them is the kit's budget.
217
+
218
+ **Many-sorted (MSFOL) soundness.** A sort ``S`` is the extension of the unary
219
+ predicate ``S`` (one symbol), never empty, and a sorted constant ``c:S`` lies in
220
+ it. The writer LOWERS every sorted node first (the auto-reduction
221
+ ``fol.nodes.to_fol``: ``∀x:S φ`` is ``∀x (S(x) → φ)``, ``c:S`` is the plain constant
222
+ ``c``), which by itself says neither that the sort is non-empty nor that ``c`` is in
223
+ it, so it states both as their own extra lines in ``formulas(assumptions)`` —
224
+ alongside the premises, i.e. Prover9 may assume them freely, exactly what an
225
+ entailment's premise side means; never inside ``formulas(goals)``, and never folded into
226
+ ``Node.to_prover9()`` itself, which stays polarity-blind:
227
+
228
+ * ``unicode_logic_kit.fol._msfl_nodes.nonempty_sort_axioms(premises +
229
+ [conclusion])``: one ``∃x S(x)`` per sort;
230
+ * ``unicode_logic_kit.fol._msfl_nodes.sort_membership_axioms`` of the same sentences, one
231
+ atom ``S(c)`` per sorted constant ``c:S``. A sorted and a plain constant of one
232
+ name are one symbol and get one token; a constant with two sorts gets two atoms.
233
+ Without them ``∀x:Human Mortal(x) ⊢ Mortal(socrates:Human)`` is not proved, because
234
+ the plain text forgets that ``socrates`` is a ``Human``.
235
+
236
+ The lowered formulas and these axioms then go through :func:`_sanitize_for_prover9`
237
+ TOGETHER, so a sort name is a name like any other: ``S(x)`` in the guard of a
238
+ quantifier, in an axiom and in a plain atom ``S(c)`` is one symbol, a non-ASCII sort
239
+ is written under an ASCII replacement, a sort named like a constant of the problem
240
+ (``person`` and ``person``) gets the constant another token, and a predicate ``S`` of
241
+ another arity is another symbol. All of this is empty for an unsorted problem, so the
242
+ generated text is byte-identical to before.
243
+ """
244
+
245
+ import os
246
+ import re
247
+ import subprocess
248
+ import tempfile
249
+ from dataclasses import dataclass, field, replace
250
+ from typing import Any, Dict, List, Tuple, cast
251
+
252
+ from ..fol._fol_nodes import (
253
+ _PROVER9_QUANTIFIERS, _PROVER9_RESERVED_SYMBOLS, _number_text, _prover9_reads_as_variable,
254
+ )
255
+ from ..fol._identifiers import fresh_variables, symbol_names
256
+ from ..fol._msfl_nodes import (
257
+ _LUK_NO_CLASSICAL_EXPORT, _SORTED_NODE_TYPES, LukEquivalence, LukImplication, LukNegation,
258
+ SortedConstant, StrongConjunction, StrongDisjunction, WeakConjunction, WeakDisjunction,
259
+ free_variables, nonempty_sort_axioms, sort_membership_axioms, substitute,
260
+ )
261
+ from ..fol._numeral_symbols import numeral_name
262
+ from ..fol._team_nodes import SlashedExists
263
+ from ..fol._tptp_symbols import check_variable_names, is_tptp_boolean_atom
264
+ from ..fol.nodes import (
265
+ Atom, Constant, Contrast, Count, Function, Measure, Node, Number, And, Quantifier, Variable,
266
+ )
267
+ from ..fol.prover9_input import _RESERVED_SYMBOLS as _READER_RESERVED_SYMBOLS
268
+ from ._ascii_names import ascii_safe_base
269
+ from .vampire_entailment import _to_wsl_path
270
+
271
+ __all__ = ["check_logical_entailment", "Prover9NameMap", "Prover9Rejected",
272
+ "Prover9TimedOut", "generate_prover9_input_with_mapping"]
273
+
274
+
275
+ # ---------------------------------------------------------------------------
276
+ # ASCII/legality sanitisation — see the module docstring.
277
+ # ---------------------------------------------------------------------------
278
+
279
+ #: The text the refusals of the writer start with.
280
+ _WRITER = "generate_prover9_input_with_mapping"
281
+
282
+ _PREDICATE = "predicate"
283
+ _FUNCTION = "function"
284
+
285
+ #: One symbol of a problem as the kit sees it: ``(kind, name, arity)``, kind being
286
+ #: ``"predicate"`` or ``"function"`` (a constant is a function of arity 0, a
287
+ #: proposition a predicate of arity 0, a sort the predicate of that name at arity 1).
288
+ Symbol = Tuple[str, str, int]
289
+
290
+ # A raw kit-level name that is ALREADY a legal Prover9 NAME token (matches
291
+ # fol/prover9_input.py's own ``NAME: /[A-Za-z_][A-Za-z0-9_]*/`` terminal) and
292
+ # is pure ASCII (Node.to_prover9 never transliterates a predicate/function
293
+ # name, only a Constant, and even that doesn't fix digit-leading — see the
294
+ # module docstring). Names the widened parser can now produce never contain
295
+ # anything outside unicode letters/digits/underscore/combining marks, so
296
+ # this is the exact complement of "needs a replacement".
297
+ _PROVER9_SAFE_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
298
+
299
+ _NOT_A_WORD_CHARACTER = re.compile(r"[^A-Za-z0-9_]")
300
+
301
+
302
+ def _is_prover9_safe(name: str) -> bool:
303
+ return bool(name) and name.isascii() and bool(_PROVER9_SAFE_RE.fullmatch(name))
304
+
305
+
306
+ #: The words LADR reads as SYNTAX and never as a name: the identifier-shaped
307
+ #: members of the reserved set the kit's own Prover9 reader keeps
308
+ #: (``fol.prover9_input._RESERVED_SYMBOLS``: ``all``, ``exists`` and the infix
309
+ #: operator ``v``; the rest of that set is punctuation, which no name can be).
310
+ #: A rename that lands on one of these is a text Prover9 rejects or reads as
311
+ #: something else -- ``Constant("All")`` lower-cases to ``all`` -- so the
312
+ #: renamer treats each as TAKEN and the new token gets its numeric suffix
313
+ #: (``all2``), like any other token that is already in use.
314
+ _PROVER9_KEYWORDS = frozenset(
315
+ symbol for symbol in _READER_RESERVED_SYMBOLS if _PROVER9_SAFE_RE.fullmatch(symbol))
316
+
317
+
318
+ def _lowercase_initial(base: str) -> str:
319
+ """Force a lowercase-initial result for a SYNTHESISED (previously
320
+ illegal) name — never applied to an already-legal passthrough name, so
321
+ this never touches the kit's own upper-case-initial predicate
322
+ convention (see the module docstring's ``Atom("Rain", [])`` note).
323
+ Lowercase-initial avoids Prover9's own ``prolog_style_variables``
324
+ ambiguity (an upper-case- or underscore-initial symbol reads as a
325
+ VARIABLE) for names this module is choosing fresh, rather than
326
+ reproducing that ambiguity for brand-new tokens nobody has to preserve
327
+ the case of.
328
+ """
329
+ if base and base[0] == "_":
330
+ # An underscore-initial token is read as a variable too (module
331
+ # docstring, "Upper-case constants").
332
+ return "c" + base
333
+ return base[0].lower() + base[1:] if base else base
334
+
335
+
336
+ def _ascii_token_base(name: str) -> str:
337
+ """The word a name that is no legal token is replaced by (before the
338
+ lower-casing and the numeric suffix): the name transliterated to ASCII with a
339
+ non-digit start (:func:`~unicode_logic_kit.atp._ascii_names.ascii_safe_base`),
340
+ then every character that is still no letter, digit or underscore (a space, a
341
+ dot, a hyphen) as the reversible ``uXXXX`` escape that transliteration uses for
342
+ a non-ASCII character."""
343
+ return _NOT_A_WORD_CHARACTER.sub(lambda m: "u%04x" % ord(m.group()),
344
+ ascii_safe_base(name, "s"))
345
+
346
+
347
+ #: The names LADR gives to the variables it renames itself: ``x0``, ``x1``, ... when it
348
+ #: clausifies a formula that binds a name inside the scope of a binder of the same name
349
+ #: (LADR's ``cnf.c``: ``unique_qvars`` and ``skolem``; the same source has ``y0``, ``y1``,
350
+ #: ... in its ``eliminate_rebinding``, which the clausification of a problem file was
351
+ #: measured not to reach). LADR picks the first such name that is no variable IN SCOPE, and
352
+ #: does not look at the constants of the formula, so a constant (or a proposition) spelled
353
+ #: like one is bound by the quantifier that took the name over (measured on Prover9
354
+ #: 2026-8A: ``(all W (all W P(W, x0)))`` is clausified to ``P(A, A)``, and so is the same
355
+ #: formula with a constant ``x1`` under three nested binders of one name). The writer never
356
+ #: leaves a binder inside the scope of one of the same name (:func:`_rename_rebound_binders`),
357
+ #: so LADR has nothing to rename; the spelling is kept away from the constants and
358
+ #: propositions of the problem all the same, so that no name of the user's is ever the one
359
+ #: LADR would choose. LADR counts from 0 and writes no leading zero. The Skolem names
360
+ #: LADR gives (``c1``, ``c2``, ... and ``f1``, ``f2``, ...) skip every symbol the file
361
+ #: already has, and are not a case of this (measured: a constant ``c1`` of the problem is
362
+ #: left alone and the Skolem constant becomes ``c2``).
363
+ _LADR_MINTED_VARIABLE = re.compile(r"[xy](?:0|[1-9][0-9]*)")
364
+
365
+
366
+ def _is_ladr_minted(name: str) -> bool:
367
+ """Whether ``name`` is spelled like a variable LADR mints for itself
368
+ (:data:`_LADR_MINTED_VARIABLE`)."""
369
+ return _LADR_MINTED_VARIABLE.fullmatch(name) is not None
370
+
371
+
372
+ def _keeps_spelling(name: str, arity: int) -> bool:
373
+ """Whether a symbol of this name and arity is written under its own name: it is
374
+ a legal token, not an arity-0 symbol Prover9 would read as a variable, not an
375
+ arity-0 symbol spelled like a variable LADR renames to (:data:`_LADR_MINTED_VARIABLE`),
376
+ not a word reserved at this arity (:data:`_PROVER9_RESERVED_SYMBOLS`: ``if`` with three
377
+ arguments, ``end_of_list`` with none, ``formulas`` with one), and not one of the
378
+ quantifier words ``all`` and ``exists``, which LADR reads as a quantifier whenever the
379
+ symbol stands first and is applied to a variable (so ``exists(X) & ...`` is no atom: the
380
+ guard of a sort named ``exists`` is exactly that)."""
381
+ return (_is_prover9_safe(name) and name not in _PROVER9_QUANTIFIERS
382
+ and (name, arity) not in _PROVER9_RESERVED_SYMBOLS
383
+ and not (arity == 0 and (_prover9_reads_as_variable(name) or _is_ladr_minted(name))))
384
+
385
+
386
+ def _reserve_token(base: str, used: set, arity: int) -> str:
387
+ """The first token of ``base``, ``base2``, ``base3``, ... that is not in ``used`` and is
388
+ no word reserved at this arity, reserved (the numeric-suffix scheme of
389
+ :func:`~unicode_logic_kit.atp._ascii_names.reserve_rendered`), and, for a symbol of arity 0,
390
+ is not spelled like a variable LADR renames to either (:data:`_LADR_MINTED_VARIABLE`).
391
+ Such a base gets a trailing underscore first (``x0`` becomes ``x0_``, then ``x0_2``):
392
+ every ``x`` or ``y`` followed by digits is that kind of name, so no numeric suffix
393
+ could leave the family."""
394
+ nullary = arity == 0
395
+ if nullary and _is_ladr_minted(base):
396
+ base += "_"
397
+ candidate, index = base, 2
398
+ while candidate in used or (candidate, arity) in _PROVER9_RESERVED_SYMBOLS:
399
+ if nullary and base in ("x", "y"):
400
+ base += "_"
401
+ candidate = base
402
+ continue
403
+ candidate = f"{base}{index}"
404
+ index += 1
405
+ used.add(candidate)
406
+ return candidate
407
+
408
+
409
+ def _symbol_what(kind: str, name: str, arity: int, sorts) -> str:
410
+ """What a symbol is called in a refusal."""
411
+ if kind == _PREDICATE and arity == 1 and name in sorts:
412
+ return "the sort"
413
+ if arity == 0:
414
+ return "the proposition" if kind == _PREDICATE else "the constant"
415
+ return "the predicate" if kind == _PREDICATE else "the function"
416
+
417
+
418
+ def _dollar_refusal(kind: str, name: str, arity: int, sorts) -> NotImplementedError:
419
+ return NotImplementedError(
420
+ f"{_WRITER}: {_symbol_what(kind, name, arity, sorts)} {name!r} is a '$'-word. "
421
+ "Prover9 keeps the words that begin with '$' for itself (its truth constants are "
422
+ "$T and $F, its answer literal $ANSWER), so a symbol spelled like that is not a "
423
+ "name of the user's and would be read as one of those. The nullary atoms $true "
424
+ "and $false are the truth constants and are written $T and $F; rename any other "
425
+ "symbol before exporting this problem.")
426
+
427
+
428
+ def _comparison_refusal(name: str, arity: int) -> NotImplementedError:
429
+ return NotImplementedError(
430
+ f"{_WRITER}: the comparison {name!r} is written infix and takes exactly two "
431
+ f"arguments, but it is applied to {arity}; Prover9 has no other reading of it. "
432
+ "Rename the predicate before exporting this problem.")
433
+
434
+
435
+ def _numeral_clash_refusal(text: str, name: str) -> NotImplementedError:
436
+ return NotImplementedError(
437
+ f"{_WRITER}: the number {text} and the constant {name!r} are spelled alike: they "
438
+ "are ONE symbol for the Z3 route (and the TPTP writers refuse the pair), and this "
439
+ "writer would write them as two, a number as its value in double quotes and "
440
+ "a constant renamed to a word, so the problem would not mean what the formulas "
441
+ "say. Rename the constant before exporting this problem.")
442
+
443
+
444
+ @dataclass
445
+ class Prover9NameMap:
446
+ """The renamings :func:`_sanitize_for_prover9` chose for one problem.
447
+
448
+ The key of a symbol is ``(kind, name, arity)`` (see the module docstring):
449
+ :attr:`symbols` maps every symbol of the problem to the token it is written
450
+ as. Prover9 keeps ONE symbol per spelling, so the first symbol of a spelling
451
+ keeps it and a later one with the same name (another arity, or a predicate
452
+ next to a function or a constant) gets a token that no other symbol has. A
453
+ SINGLE flat namespace across predicate/function/constant names (unlike
454
+ :mod:`atp._tptp_problem`'s predicate-vs-term split): Prover9's own export
455
+ never case-folds, so an already-legal name can never collide with another
456
+ already-legal name the way TPTP's first-letter fold can, and being
457
+ conservative about a SYNTHESISED replacement never colliding with ANY other
458
+ name in the problem — predicate, function, or constant alike — costs nothing
459
+ but an occasional extra numeric suffix. ``mapping`` is the original-kit-name ->
460
+ token dict (for a name with several symbols, the token of the first one that was
461
+ not renamed because of the variable rule); an original name that was already
462
+ legal maps to itself.
463
+ """
464
+
465
+ mapping: Dict[str, str] = field(default_factory=dict)
466
+ used: set = field(default_factory=set)
467
+ #: Constants whose own name Prover9 would read as a VARIABLE (upper-case or
468
+ #: underscore initial, see the module docstring), original name -> token.
469
+ #: Kept apart from ``mapping`` because the same word can also be a
470
+ #: predicate/function name, which is NOT renamed; ``mapping`` carries the
471
+ #: same entry too whenever the word has no such second role.
472
+ constants: Dict[str, str] = field(default_factory=dict)
473
+ #: NULLARY predicates whose own name Prover9 would read as a VARIABLE (the
474
+ #: same rule: an atom with no arguments is an arity-0 term), original name ->
475
+ #: token. Kept apart from ``mapping`` and from ``constants`` for the same
476
+ #: reason: ``Rain`` may also be a predicate WITH arguments (not renamed) and a
477
+ #: constant (renamed to its own token); ``mapping`` carries the same entry
478
+ #: whenever the word has no other role.
479
+ nullary_predicates: Dict[str, str] = field(default_factory=dict)
480
+ #: Every symbol of the problem, ``(kind, name, arity)`` -> the token it is
481
+ #: written as, in the order the symbols were first met.
482
+ symbols: Dict[Symbol, str] = field(default_factory=dict)
483
+ #: Every numeral of the problem, the text of its value (``2.5``; ``1`` for both
484
+ #: ``Number(1)`` and ``Number(1.0)``) -> the text it is written as (``'"2.5"'``).
485
+ numerals: Dict[str, str] = field(default_factory=dict)
486
+ #: Every FREE variable of the problem, its name -> the token of the constant it is
487
+ #: written as (see the module docstring, "Free variables"). Empty for a problem
488
+ #: whose formulas are all closed.
489
+ free_variables: Dict[str, str] = field(default_factory=dict)
490
+ _seen: Dict[Symbol, None] = field(default_factory=dict)
491
+ _claimed: Dict[str, Symbol] = field(default_factory=dict)
492
+ _numeral_texts: set = field(default_factory=set)
493
+ _sorts: frozenset = frozenset()
494
+
495
+ def collect_symbol(self, kind: str, name: str, arity: int) -> None:
496
+ """First pass: register the symbol ``(kind, name, arity)``. A symbol that is
497
+ written under its own name keeps it if no earlier symbol has it (order-independent
498
+ for the renamed ones — see :class:`~atp._tptp_problem._Renamer`'s docstring: the
499
+ name is reserved immediately, so that a synthesised token never takes it); a
500
+ symbol of an already claimed spelling, an illegal name, or an arity-0 name that
501
+ Prover9 would read as a variable waits for :meth:`finalize`.
502
+
503
+ Raises:
504
+ NotImplementedError: the name begins with ``$``."""
505
+ if name.startswith("$"):
506
+ raise _dollar_refusal(kind, name, arity, self._sorts)
507
+ key = (kind, name, arity)
508
+ if key in self._seen:
509
+ return
510
+ self._seen[key] = None
511
+ if _keeps_spelling(name, arity) and name not in self._claimed:
512
+ self._claimed[name] = key
513
+ self.used.add(name)
514
+ self.symbols[key] = name
515
+
516
+ def collect_numeral(self, value) -> None:
517
+ """First pass: register the numeral ``value`` (written as
518
+ :meth:`~unicode_logic_kit.fol._fol_nodes.Number.to_prover9` writes it): one entry per
519
+ VALUE, so ``1`` and ``1.0`` are one numeral. The texts a constant must not carry
520
+ (they would be this symbol under another spelling) are the name of the value, the
521
+ text the node prints and ``str`` of the value."""
522
+ key = numeral_name(value)
523
+ self._numeral_texts.update((key, _number_text(value), str(value)))
524
+ self.numerals.setdefault(key, Number(value).to_prover9())
525
+
526
+ def collect(self, name: str, constant: bool = False, nullary: bool = False) -> None:
527
+ """First pass for ONE name: a constant (``constant=True``), a nullary
528
+ predicate (``nullary=True``) or, by default, a predicate with arguments.
529
+ :meth:`collect_symbol` is the form that carries the kind and the arity."""
530
+ if constant:
531
+ self.collect_symbol(_FUNCTION, name, 0)
532
+ elif nullary:
533
+ self.collect_symbol(_PREDICATE, name, 0)
534
+ else:
535
+ self.collect_symbol(_PREDICATE, name, 1)
536
+
537
+ def reserve_sort(self, sort: str) -> None:
538
+ """Keep every synthesised token off the name of a sort of the problem. A sort is
539
+ the unary predicate of its name and is collected as that symbol, which reserves
540
+ its name; this is for a caller that knows a sort name and not the atom."""
541
+ if _is_prover9_safe(sort):
542
+ self.used.add(sort)
543
+
544
+ def finalize(self) -> None:
545
+ """Second pass: give a token to every symbol that is not written under its own
546
+ name, now that every legal name of the WHOLE problem is reserved (and the LADR
547
+ keywords, which no name may be: :data:`_PROVER9_KEYWORDS`).
548
+
549
+ Raises:
550
+ NotImplementedError: a number and a constant are spelled alike."""
551
+ for kind, name, arity in self._seen:
552
+ if kind == _FUNCTION and arity == 0 and name in self._numeral_texts:
553
+ raise _numeral_clash_refusal(name, name)
554
+ self.used.update(_PROVER9_KEYWORDS)
555
+ illegal: List[Symbol] = []
556
+ variable_like: List[Symbol] = []
557
+ nullary_like: List[Symbol] = []
558
+ clashing: List[Symbol] = []
559
+ for key in self._seen:
560
+ if key in self.symbols:
561
+ continue
562
+ kind, name, arity = key
563
+ if not _is_prover9_safe(name):
564
+ illegal.append(key)
565
+ elif arity == 0 and _prover9_reads_as_variable(name):
566
+ (variable_like if kind == _FUNCTION else nullary_like).append(key)
567
+ else:
568
+ clashing.append(key)
569
+ for key in illegal:
570
+ self.symbols[key] = _reserve_token(
571
+ _lowercase_initial(_ascii_token_base(key[1])), self.used, key[2])
572
+ for key in variable_like + nullary_like:
573
+ self.symbols[key] = _reserve_token(_lowercase_initial(key[1]), self.used, key[2])
574
+ for key in clashing:
575
+ self.symbols[key] = _reserve_token(key[1], self.used, key[2])
576
+ # The views by name, in the order of first occurrence.
577
+ first_plain: Dict[str, str] = {}
578
+ first_any: Dict[str, str] = {}
579
+ for key in self._seen:
580
+ kind, name, arity = key
581
+ token = self.symbols[key]
582
+ first_any.setdefault(name, token)
583
+ if arity == 0 and _is_prover9_safe(name) and _prover9_reads_as_variable(name):
584
+ (self.constants if kind == _FUNCTION else self.nullary_predicates)[name] = token
585
+ else:
586
+ first_plain.setdefault(name, token)
587
+ for name, token in first_any.items():
588
+ self.mapping[name] = first_plain.get(name, token)
589
+
590
+ def get(self, name: str) -> str:
591
+ """The token for a predicate/function name (or a constant that needed
592
+ no variable-rule rename): the first symbol of that name; see
593
+ :meth:`get_symbol` for one of several."""
594
+ return self.mapping[name]
595
+
596
+ def get_symbol(self, kind: str, name: str, arity: int) -> str:
597
+ """The token of the symbol ``(kind, name, arity)``."""
598
+ return self.symbols[(kind, name, arity)]
599
+
600
+ def get_constant(self, name: str) -> str:
601
+ """The token for ``name`` in CONSTANT position (arity-0 term)."""
602
+ return self.symbols[(_FUNCTION, name, 0)]
603
+
604
+ def get_nullary(self, name: str) -> str:
605
+ """The token for the predicate ``name`` written WITHOUT arguments."""
606
+ return self.symbols[(_PREDICATE, name, 0)]
607
+
608
+ def get_sort(self, sort: str) -> str:
609
+ """The token for the sort ``sort``: its guard predicate, the unary predicate
610
+ of that name."""
611
+ return self.symbols[(_PREDICATE, sort, 1)]
612
+
613
+ def reverse(self) -> Dict[str, str]:
614
+ """Flat token -> original dict (Prover9 never case-folds, so the
615
+ token stored in ``symbols`` is exactly what would come back from any
616
+ future reader of Prover9's own output — no fold/cap asymmetry to
617
+ account for, unlike TPTP's predicate namespace). Tokens are unique
618
+ over the whole problem, so the symbols merge without a clash. The token
619
+ of the constant a free variable was replaced by (:attr:`free_variables`) maps
620
+ back to the NAME OF THE VARIABLE, which is what the caller wrote."""
621
+ reverse = {token: name for (_kind, name, _arity), token in self.symbols.items()}
622
+ reverse.update({token: name for name, token in self.free_variables.items()})
623
+ return reverse
624
+
625
+
626
+ def _is_builtin_function(node: Function) -> bool:
627
+ """Whether ``node`` is written with a notation of its own (``(a + b)``, ``-(a)``,
628
+ ``-(a, b)``) rather than as a symbol of the problem."""
629
+ return node.name in Function.INFIX_OPS and (
630
+ len(node.args) == 2 or (node.name == "-" and len(node.args) == 1))
631
+
632
+
633
+ def _is_infix_atom(node: Atom) -> bool:
634
+ return node.predicate in Atom.INFIX_PREDS_P9 and len(node.args) == 2
635
+
636
+
637
+ def _sanitize_node_for_prover9(node: Node, names: Prover9NameMap) -> Node:
638
+ """Rebuild ``node`` with every non-Prover9-legal symbol name replaced.
639
+
640
+ Mirrors :func:`atp._tptp_problem._sanitize_node_for_tptp`'s structural
641
+ recursion (``Node.map_children`` for everything that is not itself an
642
+ Atom/Function/Constant); an already-legal name comes back as the exact
643
+ same string, so ``Node.to_prover9()`` on the result is byte-identical to
644
+ ``Node.to_prover9()`` on the original wherever every name involved was
645
+ already legal (R1).
646
+ """
647
+ if isinstance(node, Atom):
648
+ if _is_infix_atom(node) or is_tptp_boolean_atom(node):
649
+ pred = node.predicate
650
+ else:
651
+ pred = names.get_symbol(_PREDICATE, node.predicate, len(node.args))
652
+ return Atom(pred, [_sanitize_node_for_prover9(a, names) for a in node.args])
653
+ if isinstance(node, Function):
654
+ if _is_builtin_function(node):
655
+ name = node.name
656
+ else:
657
+ name = names.get_symbol(_FUNCTION, node.name, len(node.args))
658
+ return Function(name, [_sanitize_node_for_prover9(a, names) for a in node.args])
659
+ if isinstance(node, Constant):
660
+ return Constant(names.get_constant(node.name))
661
+ if isinstance(node, SortedConstant):
662
+ # Renders as the plain Constant of the same name (to_fol), and is that
663
+ # constant: it takes the same token, whatever the name (a variable-shaped,
664
+ # non-ASCII or digit-leading name is renamed exactly like a plain constant),
665
+ # so the premise occurrence and its membership atom are one symbol.
666
+ return SortedConstant(names.get_constant(node.name), names.get_sort(node.sort))
667
+ rebuilt = node.map_children(lambda c: _sanitize_node_for_prover9(c, names))
668
+ if isinstance(node, _SORTED_NODE_TYPES):
669
+ # The sort is the unary predicate of its name, one symbol with the guard it lowers to.
670
+ return replace(cast(Any, rebuilt), sort=names.get_sort(node.sort))
671
+ return rebuilt
672
+
673
+
674
+ def _collect_names_for_prover9(node: Node, names: Prover9NameMap) -> None:
675
+ """First pass (see :meth:`Prover9NameMap.collect_symbol`): register every
676
+ symbol ``node`` (and its descendants) uses, without rewriting anything yet."""
677
+ for n in node.walk():
678
+ if isinstance(n, Atom):
679
+ if _is_infix_atom(n) or is_tptp_boolean_atom(n):
680
+ continue
681
+ if n.predicate in Atom.INFIX_PREDS_P9:
682
+ raise _comparison_refusal(n.predicate, len(n.args))
683
+ names.collect_symbol(_PREDICATE, n.predicate, len(n.args))
684
+ elif isinstance(n, Function):
685
+ if not _is_builtin_function(n):
686
+ names.collect_symbol(_FUNCTION, n.name, len(n.args))
687
+ elif isinstance(n, (Constant, SortedConstant)):
688
+ names.collect_symbol(_FUNCTION, n.name, 0)
689
+ elif isinstance(n, Number):
690
+ names.collect_numeral(n.value)
691
+ if isinstance(n, _SORTED_NODE_TYPES):
692
+ names.collect_symbol(_PREDICATE, n.sort, 1)
693
+
694
+
695
+ def _sanitize_for_prover9(formulas: List[Node], sorts=()) -> Tuple[List[Node], Prover9NameMap]:
696
+ """Sanitise every formula's predicate/function/constant names for Prover9.
697
+
698
+ Returns ``(sanitised_formulas, mapping)`` — see the module docstring.
699
+ The single namespace is shared across ALL of ``formulas``, so the same
700
+ original name maps to the same token everywhere (R2), and a synthesised
701
+ token can never collide with any name anywhere in the problem,
702
+ regardless of where each one appears
703
+ (:meth:`Prover9NameMap.collect_symbol`/:meth:`~Prover9NameMap.finalize`'s
704
+ two-pass split — see :class:`atp._tptp_problem._Renamer`'s docstring for
705
+ why a single combined pass would be order-dependent). ``sorts`` names the
706
+ sorts of the problem, for the wording of a refusal only.
707
+ """
708
+ names = Prover9NameMap()
709
+ names._sorts = frozenset(sorts)
710
+ for f in formulas:
711
+ _collect_names_for_prover9(f, names)
712
+ names.finalize()
713
+ sanitised = [_sanitize_node_for_prover9(f, names) for f in formulas]
714
+ return sanitised, names
715
+
716
+
717
+ _LUKASIEWICZ_NODES = (WeakConjunction, WeakDisjunction, StrongConjunction, StrongDisjunction,
718
+ LukNegation, LukImplication, LukEquivalence)
719
+
720
+
721
+ def _refuse_lukasiewicz(formula: Node) -> None:
722
+ """Refuse a formula that holds a Łukasiewicz connective, by name.
723
+
724
+ Their classical collapse (``to_msfol``) is a different LOGIC, not a spelling of the
725
+ same one: the weak disjunction ``p ∨ ¬p`` is the maximum of ``p`` and ``1 − p``, which is
726
+ ``1/2`` at ``p = 1/2``, so it is not valid, while its classical image is. A single
727
+ ``Node.to_prover9()`` refuses these nodes for that reason, and so does this writer.
728
+
729
+ Raises:
730
+ NotImplementedError: a Łukasiewicz node occurs in ``formula``.
731
+ """
732
+ for node in formula.walk():
733
+ if isinstance(node, _LUKASIEWICZ_NODES):
734
+ raise NotImplementedError(
735
+ f"{_WRITER}: {type(node).__name__} — " + _LUK_NO_CLASSICAL_EXPORT)
736
+
737
+
738
+ def _fresh_constant_names(names, taken: set) -> Dict[str, str]:
739
+ """For each variable name in ``names``, a constant name that is no name of ``taken``
740
+ (compared case-folded; the names chosen are added to ``taken``): the variable's own
741
+ name when it is free, else that name with ``_1``, ``_2``, ... appended."""
742
+ chosen: Dict[str, str] = {}
743
+ for name in names:
744
+ candidate, index = name, 1
745
+ while candidate.casefold() in taken:
746
+ candidate = f"{name}_{index}"
747
+ index += 1
748
+ taken.add(candidate.casefold())
749
+ chosen[name] = candidate
750
+ return chosen
751
+
752
+
753
+ def _replace_free_variables(formulas: List[Node]) -> Tuple[List[Node], Dict[str, str]]:
754
+ """``formulas`` with every free variable replaced, problem-wide, by a fresh constant.
755
+
756
+ A free variable of a problem is a PARAMETER of it: one unknown element, the same in
757
+ every premise and in the conclusion (the assignment-wise consequence relation:
758
+ ``Γ ⊨ φ`` iff every structure AND assignment that satisfies ``Γ`` satisfies ``φ``), so
759
+ ``P(x) ⊢ P(alpha)`` is not valid. Prover9 would read each formula on its own as closed
760
+ universally (``P(X)`` is ``∀X P(X)``), which is another question; and
761
+ ``Γ(x) ⊨ φ(x)`` assignment-wise holds iff ``Γ(c) ⊨ φ(c)`` for a constant ``c`` that no
762
+ symbol of the problem is called. The constant of a free variable carries the variable's
763
+ own name when no other symbol of the problem (of any kind, case-folded) has it.
764
+
765
+ Returns the formulas and the variable name -> constant name table (the names are
766
+ the kit's; the writer's own renaming of a constant applies to them afterwards, so a
767
+ free variable ``X`` or ``x0`` is written as a constant Prover9 reads as one).
768
+ """
769
+ # A slashed quantifier has no classical export and is refused when it is written; a substitution
770
+ # into its slash set would turn it into a plain quantifier first, so a formula that holds one is
771
+ # left as it is.
772
+ free_per_formula = [set() if any(isinstance(n, SlashedExists) for n in f.walk())
773
+ else {v.name for v in free_variables(f) if isinstance(v, Variable)}
774
+ for f in formulas]
775
+ order: List[str] = []
776
+ for formula, free in zip(formulas, free_per_formula):
777
+ for node in formula.walk():
778
+ if isinstance(node, Variable) and node.name in free and node.name not in order:
779
+ order.append(node.name)
780
+ if not order:
781
+ return list(formulas), {}
782
+ # The names that are taken are those of the problem without the free occurrences: a
783
+ # free variable ``x`` is not a reason to call its own constant anything else.
784
+ stand_in = Number(0)
785
+ stripped = []
786
+ for formula, free in zip(formulas, free_per_formula):
787
+ for name in free:
788
+ formula = substitute(formula, Variable(name), stand_in)
789
+ stripped.append(formula)
790
+ constants = _fresh_constant_names(order, set(symbol_names(*stripped, fold=str.casefold)))
791
+ replaced = []
792
+ for formula, free in zip(formulas, free_per_formula):
793
+ for name in sorted(free):
794
+ formula = substitute(formula, Variable(name), Constant(constants[name]))
795
+ replaced.append(formula)
796
+ return replaced, constants
797
+
798
+
799
+ def _rename_rebound_binders(node: Node, avoid: set, scope: frozenset = frozenset()) -> Node:
800
+ """``node`` with every binder that sits inside the scope of a binder of the same name
801
+ renamed to a fresh variable (alpha-conversion).
802
+
803
+ LADR renames such a variable itself, to a symbol it chooses (``x0``, ``x1``, ...,
804
+ :data:`_LADR_MINTED_VARIABLE`), and takes a constant of that spelling for it; a text
805
+ in which no binder is re-bound leaves LADR nothing to choose. Names are compared as
806
+ Prover9 reads them (the upper-case of a variable name is what is written). A binder
807
+ whose name is not in the scope of another stays as it is, so the text of a formula
808
+ without re-bound binders is the text it always was (two sibling quantifiers on one
809
+ name are not re-bound: ``(all X P(X)) & (all X Q(X))``). The new name is one of
810
+ ``avoid`` — every name of the whole problem, case-folded — and is added to it.
811
+
812
+ ``scope`` is the set of upper-case names that already bind where ``node`` stands (the free
813
+ variables of a formula that Prover9 closes universally). The traversal keeps its own stack
814
+ and carries the renaming that holds below a renamed binder as a table (old name -> new name),
815
+ which is what substituting into the body would do: a formula nested thousands of levels deep
816
+ is renamed as readily as a shallow one, without the recursion limit. The fresh names are
817
+ minted in the order of the text, outermost binder first, left to right.
818
+ """
819
+ results: List[Node] = []
820
+ work: list = [("enter", node, scope, {})]
821
+ while work:
822
+ step = work.pop()
823
+ if step[0] == "enter":
824
+ _, current, in_scope, renames = step
825
+ if isinstance(current, Quantifier):
826
+ variable, below = current.variable, renames
827
+ if variable.name.upper() in in_scope:
828
+ first = variable.name[:1].lower()
829
+ letter = first if first.isascii() and first.isalpha() else "x"
830
+ fresh = Variable(fresh_variables(1, letter=letter, avoid=avoid)[0])
831
+ avoid.add(fresh.name.casefold())
832
+ below = {**renames, variable.name: fresh.name}
833
+ variable = fresh
834
+ work.append(("binder", current.type, variable))
835
+ work.append(("enter", current.formula, in_scope | {variable.name.upper()}, below))
836
+ elif isinstance(current, Variable):
837
+ results.append(Variable(renames[current.name]) if current.name in renames else current)
838
+ else:
839
+ children = current._child_nodes()
840
+ work.append(("rebuild", current, len(children)))
841
+ work.extend(("enter", child, in_scope, renames) for child in reversed(children))
842
+ elif step[0] == "binder":
843
+ results.append(Quantifier(step[1], step[2], results.pop()))
844
+ else:
845
+ _, current, count = step
846
+ rebuilt = iter(results[len(results) - count:])
847
+ del results[len(results) - count:]
848
+ results.append(current.map_children(lambda child: next(rebuilt)))
849
+ return results[0]
850
+
851
+
852
+ def _expand_for_prover9(node: Node, avoid: set) -> Node:
853
+ """The reduction :func:`~unicode_logic_kit.fol._msfl_nodes._reduce_nl_nodes` makes,
854
+ with the counting witnesses minted fresh against ``avoid`` (every name of the whole
855
+ problem, case-folded: Prover9 writes a variable in upper case, so ``X0`` and ``x0`` are
856
+ one; the names minted are added to it), and a ``Measure`` as the binary function
857
+ ``measure`` it is written as, so that it is a symbol of the problem like any other."""
858
+ node = node.map_children(lambda c: _expand_for_prover9(c, avoid))
859
+ if isinstance(node, Contrast):
860
+ return And(node.left, node.right)
861
+ if isinstance(node, Count):
862
+ return node._expand(avoid)
863
+ if isinstance(node, Measure):
864
+ return Function("measure", (node.entity, node.dimension))
865
+ return node
866
+
867
+
868
+ def _lower_for_prover9(node: Node, avoid: set) -> Node:
869
+ """``node`` as plain first-order logic: ``to_fol``'s reduction (sorted quantifiers
870
+ guarded, sorted constants plain), with the counting witnesses of
871
+ :func:`_expand_for_prover9`. The sort facts are NOT part of it (they are the
872
+ problem's own assumptions)."""
873
+ return _expand_for_prover9(node.to_msfol()._relativize([]), avoid)
874
+
875
+
876
+ def generate_prover9_input_with_mapping(premises: List[Node], conclusion: Node
877
+ ) -> Tuple[str, Prover9NameMap]:
878
+ """Like :func:`_generate_prover9_input`, but also returns the
879
+ :class:`Prover9NameMap` recording every ASCII-legality rename applied.
880
+
881
+ No current caller in this module reads a Prover9-chosen symbol name back
882
+ out of its output (see the module docstring), but this is the function
883
+ a future "detailed" Prover9 route would call instead of
884
+ :func:`_generate_prover9_input`, exactly the way
885
+ :mod:`atp.vampire_entailment`'s detailed route uses
886
+ :func:`atp._tptp_problem.generate_tptp_problem_with_mapping`.
887
+
888
+ Raises:
889
+ NotImplementedError: two variables of ONE formula that Prover9 reads as one
890
+ (``x`` and ``X`` are both written ``X``: ``∀x ∃X R(x, X)`` would be
891
+ ``(all X (exists X R(X, X)))``). The check is the one the TPTP writers
892
+ use (:func:`unicode_logic_kit.fol._tptp_symbols.check_variable_names`), and
893
+ it refuses every such pair of one formula, a harmless one too (two
894
+ quantifiers side by side, ``(∀x P(x)) ∧ (∀X Q(X))``). A single
895
+ ``Node.to_prover9()`` has no whole-problem view: it renames a binder inside
896
+ the scope of another (``∀x ∃X R(x, X)`` is written ``(all X (exists X0 R(X, X0)))``),
897
+ and refuses by name only the pairs that no renaming of a binder repairs.
898
+ A symbol named with a ``$`` (Prover9's own words), a comparison symbol
899
+ at other than two arguments, a number spelled like a constant of the
900
+ problem, and a Łukasiewicz connective (it has no classical reading) are
901
+ refused too (see the module docstring).
902
+
903
+ A free variable of the problem is written as a constant, the same in every formula, and
904
+ recorded in :attr:`Prover9NameMap.free_variables`; a binder inside the scope of a binder
905
+ of its own name is renamed (see the module docstring).
906
+ """
907
+ originals = list(premises) + [conclusion]
908
+ for formula in originals:
909
+ _refuse_lukasiewicz(formula)
910
+ check_variable_names(formula, where=_WRITER, subject="problem", dialect="prover9")
911
+
912
+ # A free variable is one unknown element of the whole problem: a constant (see
913
+ # _replace_free_variables), before anything else reads the formulas.
914
+ originals, free_constants = _replace_free_variables(originals)
915
+
916
+ # Sorted nodes are lowered FIRST: the guard images and the sort facts are plain
917
+ # first-order sentences, and go through the same sanitiser as every other symbol.
918
+ nonempty = list(nonempty_sort_axioms(*originals))
919
+ membership = list(sort_membership_axioms(*originals))
920
+ # Every name of the whole problem, of every kind, case-folded (Prover9 reads ``x0`` and
921
+ # ``X0`` as one variable): what the witnesses of a counting quantifier and the fresh
922
+ # binders below must differ from.
923
+ avoid = set(symbol_names(*originals, *nonempty, *membership, fold=str.casefold))
924
+ lowered = [_lower_for_prover9(f, avoid) for f in originals]
925
+ # No binder may sit inside the scope of one of its own name (see _rename_rebound_binders).
926
+ # The sort facts are one binder each and are written as they are.
927
+ lowered = [_rename_rebound_binders(f, avoid) for f in lowered]
928
+ sorts = {n.sort for f in originals for n in f.walk() if isinstance(n, _SORTED_NODE_TYPES)}
929
+ sanitised, mapping = _sanitize_for_prover9(lowered + nonempty + membership, sorts=sorts)
930
+ mapping.free_variables = {
931
+ name: mapping.symbols[(_FUNCTION, constant, 0)]
932
+ for name, constant in free_constants.items()
933
+ if (_FUNCTION, constant, 0) in mapping.symbols}
934
+ count = len(premises)
935
+ sanitised_premises, sanitised_conclusion = sanitised[:count], sanitised[count]
936
+ sanitised_nonempty = sanitised[count + 1:count + 1 + len(nonempty)]
937
+ sanitised_membership = sanitised[count + 1 + len(nonempty):]
938
+
939
+ lines = []
940
+ lines.append("set(prolog_style_variables).")
941
+ lines.append("set(auto_denials).")
942
+ lines.append("clear(print_initial_clauses).")
943
+ lines.append("clear(print_kept).")
944
+ lines.append("clear(print_given).")
945
+ lines.append("")
946
+
947
+ lines.append("formulas(assumptions).")
948
+ for premise in sanitised_premises:
949
+ lines.append(f" {premise.to_prover9()}.")
950
+ # Many-sorted background facts — see the module docstring: non-emptiness of every
951
+ # sort, and the membership S(c) of every sorted constant. Both are built from the
952
+ # ORIGINAL sentences and sanitised with the rest, so the sort predicate and the
953
+ # constant of each fact are the very tokens the premises use.
954
+ for axiom in sanitised_nonempty:
955
+ lines.append(f" {axiom.to_prover9()}.")
956
+ for fact in sanitised_membership:
957
+ lines.append(f" {fact.to_prover9()}.")
958
+ lines.append("end_of_list.")
959
+ lines.append("")
960
+
961
+ lines.append("formulas(goals).")
962
+ lines.append(f" {sanitised_conclusion.to_prover9()}.")
963
+ lines.append("end_of_list.")
964
+
965
+ return "\n".join(lines), mapping
966
+
967
+
968
+ def _generate_prover9_input(premises: List[Node], conclusion: Node) -> str:
969
+ """
970
+ Generates a Prover9 input string from given premises and conclusion.
971
+
972
+ Every predicate/function/constant name is first made Prover9-ASCII-legal
973
+ (see the module docstring's sanitisation section) — a name that was
974
+ already legal renders byte-identically to before that step existed.
975
+
976
+ Args:
977
+ premises (list[Node]): List of premise formulas in FOL.
978
+ conclusion (Node): Conclusion formula in FOL.
979
+
980
+ Returns:
981
+ str: Formatted Prover9 input string.
982
+ """
983
+ text, _mapping = generate_prover9_input_with_mapping(premises, conclusion)
984
+ return text
985
+
986
+
987
+ #: Prover9's documented exit code for "a fatal error occurred (the user's syntax
988
+ #: error, or Prover9's own bug)" -- LADR/Prover9 manual, "Exit codes". The other
989
+ #: codes are ordinary ends of a search: 2 the sos list ran empty, 3 max_megs,
990
+ #: 4 max_seconds, 5 max_given, 6 max_kept, 7 an action, and 0 a proof.
991
+ _PROVER9_FATAL_EXIT = 1
992
+
993
+ #: The shell's own exit codes for "the command was found but cannot run" (126)
994
+ #: and "no such command" (127). Prover9 never exits with them. Behind
995
+ #: ``wsl.exe <path> ...`` a path that does not exist inside WSL ends the run this
996
+ #: way, with the shell's message and no Prover9 output at all: that is a binary
997
+ #: that was never started, not a search that ended without a proof.
998
+ _SHELL_NOT_STARTED_EXITS = frozenset({126, 127})
999
+
1000
+
1001
+ class Prover9Rejected(RuntimeError):
1002
+ """Prover9 refused to read the problem (or died) instead of searching.
1003
+
1004
+ Raised by :func:`check_logical_entailment` ONLY when called with
1005
+ ``raise_on_rejection=True``; the default keeps the historic contract of
1006
+ returning ``False``. ``returncode`` is Prover9's exit code and ``output``
1007
+ its own message (stderr, else stdout), so the caller can quote it.
1008
+ """
1009
+
1010
+ def __init__(self, returncode: int, output: str):
1011
+ super().__init__(f"Prover9 exited with code {returncode}: {output.strip()[:300]}")
1012
+ self.returncode = returncode
1013
+ self.output = output
1014
+
1015
+
1016
+ class Prover9TimedOut(RuntimeError):
1017
+ """The kit stopped Prover9 because its wall-clock budget ran out.
1018
+
1019
+ Raised by :func:`check_logical_entailment` ONLY when called with
1020
+ ``raise_on_timeout=True``; the default keeps the historic contract of
1021
+ returning ``False``. ``timeout`` is the budget in seconds.
1022
+ """
1023
+
1024
+ def __init__(self, timeout: float):
1025
+ super().__init__(f"Prover9 was stopped after its {timeout:g}-second budget "
1026
+ "ran out; no proof was found in that time")
1027
+ self.timeout = timeout
1028
+
1029
+
1030
+ def _prover9_rejected(returncode: int, stdout: str, stderr: str) -> bool:
1031
+ """True iff this exit is a refusal, not the end of a search: Prover9's
1032
+ fatal-error exit, or the shell's "cannot run the binary" exit (126, 127)."""
1033
+ return (returncode == _PROVER9_FATAL_EXIT or returncode in _SHELL_NOT_STARTED_EXITS
1034
+ or "Fatal error" in stdout or "Fatal error" in stderr)
1035
+
1036
+
1037
+ def _prover9_command(prover9_path: str, problem_file: str, use_wsl: bool) -> list:
1038
+ """The command line that runs Prover9 on ``problem_file``.
1039
+
1040
+ Natively ``[prover9_path, "-f", problem_file]``. With ``use_wsl`` it is
1041
+ ``["wsl.exe", prover9_path, "-f", <wslpath of problem_file>]``: the path of the
1042
+ binary is the one INSIDE WSL, and the Windows temp file is translated to its
1043
+ ``/mnt/...`` form so that the Linux Prover9 can read it. The translation runs a
1044
+ process of its own, and a timeout of THAT process is no Prover9 timeout, so the
1045
+ caller builds the command before it opens the Prover9 time window.
1046
+
1047
+ Raises:
1048
+ OSError: ``wsl.exe`` cannot be started, does not answer, or cannot translate
1049
+ the path; the Prover9 backend reports it as an error verdict.
1050
+ """
1051
+ if not use_wsl:
1052
+ return [prover9_path, "-f", problem_file]
1053
+ try:
1054
+ wsl_file = _to_wsl_path(problem_file)
1055
+ except subprocess.TimeoutExpired:
1056
+ raise OSError("wslpath did not answer within its time limit while translating "
1057
+ f"{problem_file!r}; is WSL available?") from None
1058
+ except RuntimeError as exc:
1059
+ raise OSError(str(exc)) from None
1060
+ return ["wsl.exe", prover9_path, "-f", wsl_file]
1061
+
1062
+
1063
+ def _run_prover9(input: str, prover9_path: str, timeout: int = 30,
1064
+ raise_on_rejection: bool = False,
1065
+ raise_on_timeout: bool = False,
1066
+ use_wsl: bool = False) -> bool:
1067
+ """Run the prover9 command line tool.
1068
+
1069
+ ``raise_on_rejection=False`` (the default) is the historic contract: any
1070
+ run without a ``THEOREM PROVED`` line -- a search that ended without a
1071
+ proof, a timeout, AND a problem Prover9 refused to read -- is ``False``.
1072
+ With ``True`` the last of those raises :class:`Prover9Rejected` instead, so
1073
+ a caller that can report an ERROR does not mistake a refused problem for
1074
+ one that was searched and not proved; a binary that could not be started
1075
+ (the shell's exit 126 or 127) is such a refusal too. ``raise_on_timeout=True``
1076
+ does the same for a run the kit cut off after ``timeout`` seconds: it raises
1077
+ :class:`Prover9TimedOut` instead of reporting ``False``.
1078
+
1079
+ ``use_wsl=True`` runs a Linux Prover9 through ``wsl.exe`` (see
1080
+ :func:`_prover9_command`). The problem file is written as UTF-8, Prover9 never
1081
+ sees the caller's standard input, and its output is decoded as UTF-8 with
1082
+ undecodable bytes replaced, because its fatal message can cut a multi-byte
1083
+ character of the input in half.
1084
+ """
1085
+
1086
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.in', delete=False,
1087
+ encoding='utf-8', newline='\n') as temp_file:
1088
+ temp_file.write(input)
1089
+ temp_filename = temp_file.name
1090
+
1091
+ try:
1092
+ command = _prover9_command(prover9_path, temp_filename, use_wsl)
1093
+ result = subprocess.run(
1094
+ command,
1095
+ capture_output=True,
1096
+ text=True,
1097
+ encoding='utf-8',
1098
+ errors='replace',
1099
+ stdin=subprocess.DEVNULL,
1100
+ timeout=timeout
1101
+ )
1102
+ success = "THEOREM PROVED" in result.stdout
1103
+ if (not success and raise_on_rejection
1104
+ and _prover9_rejected(result.returncode, result.stdout or "",
1105
+ result.stderr or "")):
1106
+ raise Prover9Rejected(result.returncode,
1107
+ (result.stderr or "").strip() or (result.stdout or ""))
1108
+ except subprocess.TimeoutExpired:
1109
+ if raise_on_timeout:
1110
+ raise Prover9TimedOut(timeout) from None
1111
+ success = False
1112
+ finally:
1113
+ # Always remove the temp file, even when subprocess.run raises (e.g.
1114
+ # FileNotFoundError for a wrong prover9_path); the exception still
1115
+ # propagates to the caller.
1116
+ try:
1117
+ os.unlink(temp_filename)
1118
+ except OSError:
1119
+ pass
1120
+
1121
+ return success
1122
+
1123
+
1124
+ def check_logical_entailment(premises: list[Node], conclusion: Node, prover9_path: str,
1125
+ raise_on_rejection: bool = False,
1126
+ timeout: int = 30,
1127
+ raise_on_timeout: bool = False,
1128
+ use_wsl: bool = False) -> bool:
1129
+ """Checks if a conclusion entails from the defined premises by using prover9.
1130
+
1131
+ ``False`` means "no proof was found" -- including, by default, a problem
1132
+ Prover9 refused to read and a run the kit stopped. ``raise_on_rejection=True``
1133
+ separates the first: a refused problem (Prover9's fatal-error exit, or a
1134
+ binary that could not be started) raises :class:`Prover9Rejected` carrying
1135
+ Prover9's own message, which is what
1136
+ :class:`~unicode_logic_kit.atp.protocol.Prover9Backend` reports as an ERROR
1137
+ verdict instead of an unknown that reads like a timeout.
1138
+
1139
+ ``timeout`` is the wall-clock budget in SECONDS (default 30) the subprocess
1140
+ may use; ``raise_on_timeout=True`` separates a run cut off by it by raising
1141
+ :class:`Prover9TimedOut`, which the backend reports as UNKNOWN / ``"timeout"``.
1142
+
1143
+ ``use_wsl=True`` runs a Linux Prover9 through ``wsl.exe``; ``prover9_path`` is
1144
+ then the path inside WSL (for example
1145
+ ``/mnt/d/prover9/Prover9-LADR-2026-8A/bin/prover9``). See :func:`_run_prover9`.
1146
+ """
1147
+
1148
+ prover9_input = _generate_prover9_input(premises, conclusion)
1149
+ success = _run_prover9(prover9_input, prover9_path, timeout=timeout,
1150
+ raise_on_rejection=raise_on_rejection,
1151
+ raise_on_timeout=raise_on_timeout,
1152
+ use_wsl=use_wsl)
1153
+ return success