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,1111 @@
1
+ """Shared TPTP ``fof`` problem generation for the external TPTP-speaking backends.
2
+
3
+ :mod:`atp.vampire_entailment`, :mod:`atp.eprover_backend` (E and
4
+ Zipperposition), and :mod:`atp.twee_entailment` each build the IDENTICAL TPTP
5
+ problem shape from ``(premises, conclusion)``: one ``fof(premise_<i>, axiom,
6
+ ...).`` line per premise (1-based) plus one ``fof(goal, conjecture, ...).``
7
+ line — three copies of the same seven lines that had already started to
8
+ drift apart in their docstrings. :func:`generate_tptp_problem` is the single
9
+ place that shape is written; the three backends' own ``_generate_*_input``
10
+ functions now delegate here (same name, same signature, same output — no
11
+ behaviour change for their callers or their tests).
12
+
13
+ **ASCII/legality sanitisation (problem-level seam).** ``Node.to_tptp()``
14
+ renders a predicate/function/constant name close to verbatim — only a
15
+ Constant is transliterated to ASCII (:func:`constant_name_to_ascii`), and
16
+ only the first character of any of the three is folded for TPTP's
17
+ lowercase-initial rule (:func:`tptp_fold_first_letter`). Neither step fixes
18
+ a DIGIT-LEADING name (``2008SummerOlympics`` stays digit-leading, which
19
+ TPTP's ``lower_word: [a-z][A-Za-z0-9_]*`` grammar forbids), nor an ASCII
20
+ character that is no word character at all (``has-part``, ``a.b``,
21
+ ``owl:Thing``, ``$f``: Vampire reads ``has-part(X)`` as a parse error and E
22
+ stops at the ``-``), nor a leading underscore, and neither
23
+ ``Atom.to_tptp`` nor ``Function.to_tptp`` transliterates non-ASCII at all —
24
+ gaps the toolkit's identifier grammar could not reach before it was widened
25
+ to accept Unicode letters and digit-leading names, but can now. Fixing
26
+ either INSIDE a node's own ``to_tptp()`` would be wrong: each node would
27
+ rename independently, two distinct kit-level names could collide on their
28
+ fix with no whole-problem view to catch it, and the rewrite could never
29
+ reach a caller that needs to translate a prover's answer back. So the fix
30
+ lives here instead, exactly where the pre-existing case-fold collision
31
+ guard below already lives: :func:`_sanitize_for_tptp` walks every premise
32
+ and the conclusion TOGETHER, replaces only the names that are not already
33
+ TPTP-legal (checked BEFORE any fold — see :func:`_is_tptp_safe`) with an
34
+ ASCII, letter-initial, whole-problem-injective replacement of ``[A-Za-z0-9_]``
35
+ only (every other character becomes the code-point escape ``uXXXX`` the
36
+ transliteration already uses: ``has-part`` is ``hasu002dpart``, see
37
+ :func:`~unicode_logic_kit.atp.tptp_tff._tptp_word_base`), and returns
38
+ a :class:`TptpNameMap` recording exactly what was renamed so a caller can
39
+ translate prover output back (:func:`apply_reverse_tptp`). A name that was
40
+ already TPTP-legal is returned completely untouched — the very same
41
+ ``Node`` object, not a copy — so ``Node.to_tptp()``'s existing output for
42
+ every formula this module was already handling correctly is byte-identical
43
+ to before this sanitisation step existed.
44
+
45
+ **Soundness guard.** :meth:`~unicode_logic_kit.fol.nodes.Node.to_tptp` folds a
46
+ predicate/function/constant name for TPTP's lowercase-initial identifier rule
47
+ by lower-casing only its FIRST character (see
48
+ :func:`unicode_logic_kit.fol._fol_nodes.tptp_fold_first_letter`) — the exact
49
+ mirror of ``tptp_input.py``'s ``_cap()``, which capitalises only the first
50
+ character of a parsed predicate name on import. That fold is not injective by
51
+ itself: ``Foo`` and ``foo`` (or two constants, or two functions) still both
52
+ fold to ``foo``. The outermost ``to_tptp()`` call checks the ONE formula it
53
+ renders (it runs the very same check, :func:`unicode_logic_kit.fol._tptp_symbols
54
+ .check_symbols`), but it has no way to know whether some OTHER formula elsewhere
55
+ in the same problem folds to the same identifier, so the whole-problem check has
56
+ to happen here, where every premise and the conclusion are in view together. Two distinct kit-level names colliding on export would otherwise be
57
+ silently merged into ONE TPTP symbol — e.g. a premise ``Foo(a)`` and its
58
+ negation ``¬FOO(a)`` would both render as ``foo(a)``, making the exported
59
+ axiom set ``{foo(a), ~foo(a)}`` — internally CONTRADICTORY, so an external
60
+ prover proves any conjecture from it via ex falso quodlibet, a false
61
+ "Theorem" verdict for a query the premises never actually entail. Rather than
62
+ risk that, :func:`generate_tptp_problem` refuses with ``NotImplementedError``
63
+ naming both colliding kit-level names, mirroring
64
+ :func:`unicode_logic_kit.atp.tptp_ncl.to_tptp_ncl`'s own (separately
65
+ implemented, since NXF's modal connectives are outside ``to_tptp``'s
66
+ classical FOL fragment) collision guard for the NXF export path. This check
67
+ runs AFTER the ASCII sanitisation step above, over the sanitised formulas —
68
+ :func:`_sanitize_for_tptp` already avoids colliding with itself (a shared
69
+ reservation set covers both already-legal and newly-synthesised names in
70
+ each namespace), so in practice this guard only ever fires for the same
71
+ kind of pre-existing, already-legal-name case-fold collision it always did
72
+ (``Foo``/``foo``); it is not weakened or bypassed by the sanitisation step.
73
+
74
+ This check compares predicate names with predicate names and function/constant
75
+ names with function/constant names; it does not look across the two. Only two
76
+ DISTINCT names within the SAME kind colliding is refused here. Equality and
77
+ disequality (``=``, ``≠``) map to a fixed TPTP token (``=``, ``!=``), never through
78
+ the first-letter fold, so they cannot fold together with a NAME; they are no
79
+ identifier to begin with, so :func:`_sanitize_for_tptp` never offers them to the "is
80
+ this already legal?" test. The arithmetic comparisons and functions are a different
81
+ matter, see the next section. Variables are checked per formula (``x`` and ``X`` are
82
+ one TPTP variable, and the refusal says so).
83
+
84
+ **A numeral is a constant, and ``+ - * / < > ≤ ≥`` are ordinary symbols.** The kit
85
+ reads a :class:`~unicode_logic_kit.fol.nodes.Number` on every route that was not asked
86
+ for arithmetic as a constant identified by its VALUE (``1``, ``1.0`` and ``01`` are
87
+ one constant) about which nothing else is known, the four operators as uninterpreted
88
+ function symbols and the four comparisons as uninterpreted predicates. TPTP says
89
+ otherwise for the text the single renderers write (``Number.to_tptp``: ``1`` is an
90
+ ``$int``; ``$sum``, ``$less``, ... are arithmetic): measured on Vampire 5.0.1 and E
91
+ 3.5.1, the text ``p(1)`` is a type error for a predicate over individuals, and
92
+ ``1 != 2``, ``$less(1,2)`` and ``$sum(1,1) = 2`` are THEOREMS of the prover's
93
+ arithmetic that this reading does not have. So the problem writers
94
+ (:func:`generate_tptp_problem`, :func:`generate_tptp_problem_with_mapping`,
95
+ :func:`generate_tptp_problem_for_prover` and, for the typed route,
96
+ :func:`~unicode_logic_kit.atp.tptp_tff.generate_tff_problem`) write neither: a numeral
97
+ is written as the constant named by the numeral's value (``1`` and ``1.0`` are ``1``,
98
+ ``2.5`` is ``2.5``; ``unicode_logic_kit.fol._numeral_symbols.numeral_name``), an
99
+ operator as an ordinary function or predicate of its own name, and the ordinary renamer
100
+ (:func:`_sanitize_for_tptp`) gives each of them a word of a prover's grammar (``1`` is
101
+ ``n1``, ``+`` is ``u002b``, ``<`` is ``u003c``): collision-free against every other symbol
102
+ of the problem (one word per value, never the word of a user's symbol, a sort or a
103
+ predicate), recorded in the returned :class:`TptpNameMap` (``term`` and ``predicate`` for
104
+ the operators, ``term`` and ``numerals`` for the numerals), and read back through it
105
+ (:func:`apply_reverse_tptp` hands a proof's ``n1`` back as ``Number(1)`` and ``u002b`` as
106
+ ``+``). A numeral is never a double-quoted distinct object either: TPTP makes those
107
+ pairwise distinct, and two numerals of different value may denote one element. A
108
+ ``Constant`` (or ``SortedConstant`` or ``Function``) spelled like a numeral of the same
109
+ problem, ``Number(1)`` next to ``Constant('1')``, would be the same name and is refused by
110
+ name. The arithmetic reading is asked for by name and written elsewhere: the TFA writer
111
+ (:mod:`~unicode_logic_kit.atp._tff_problem`, the ``sort=`` option of the backends) keeps
112
+ ``$sum``, ``$less`` and the number literals, typed ``$int`` or ``$real``.
113
+
114
+ **A predicate and a function/constant that fold to the SAME identifier.** An
115
+ earlier version of this module argued that a predicate ``Agent`` and a role
116
+ function ``agent`` (both ``agent``) are harmless because a TPTP reader
117
+ resolves a bare identifier by its syntactic position. That is false for the
118
+ provers this module feeds: Vampire 5.0.1 answers ``Non-boolean term
119
+ agent(X0) of sort $i is used in a formula context`` and E 3.5.1 stops with a
120
+ parse error on ``agent(agent(X))``, so the problem is rejected before any SZS
121
+ status exists (and "no answer" read as a verdict looks like "undecided"). A
122
+ reader MAY resolve by position — this kit's own reader
123
+ (:mod:`~unicode_logic_kit.fol.tptp_input`) does — but a prover is not obliged
124
+ to, so the problem text must not depend on it. The writer therefore renames
125
+ the TERM-side symbol, the function or constant, whenever its rendered name
126
+ equals a predicate's, whatever the arities (:func:`_separate_term_names`): the
127
+ predicate keeps its natural name, the term becomes ``<name>_term`` (a numeric
128
+ suffix is added if that is taken), and the pair is recorded in
129
+ :class:`TptpNameMap`, so :func:`apply_reverse_tptp` and
130
+ :meth:`TptpNameMap.reverse_rendered` restore the original. The rename is exact
131
+ — a symbol is only a name — and a problem without such a clash is rendered
132
+ byte-for-byte as before. The same-kind guard above has already run on the
133
+ ASCII-sanitised formulas by then, so a refusal for two LEGAL names of one kind
134
+ (``car``/``Car``) can never be dodged by this rename. The guard predicates a
135
+ many-sorted node lowers to (``S(x)`` for a sort ``S``) count as predicate
136
+ names for BOTH checks: the sort guard ``Foo`` of ``∀x:Foo P(x)`` in one premise
137
+ and a predicate ``foo`` of another are one TPTP word, which no walk of the
138
+ source trees would see, so the whole-problem check is fed the non-emptiness
139
+ axioms too (their atoms ARE the guard predicates, one per sort), exactly as the
140
+ single-formula guard sees them by recording what it renders. A
141
+ :class:`~unicode_logic_kit.fol.nodes.Measure` writes the function ``measure`` (it
142
+ is that symbol: ``to_z3`` declares ``measure/2`` for it), so it is renamed like
143
+ ``Function('measure', ...)`` when a predicate is written ``measure`` too, and
144
+ :func:`apply_reverse_tptp` hands it back as that function.
145
+
146
+ Seven node classes write a predicate, function or constant name: ``Atom``,
147
+ ``Function``, ``Constant`` and ``SortedConstant`` (the four the sanitiser walks),
148
+ ``Measure`` (the function ``measure``), and ``SortedQuantifier`` / ``SortedCount``
149
+ (the guard predicate of their sort); ``Number`` writes a numeral and ``Variable``
150
+ a variable, and no other class writes a symbol. Every name writer is seen by the
151
+ same-kind check and by the cross-kind separation; ``tests/test_tptp_writer_names.py``
152
+ derives the list from the node classes themselves, so a class added later cannot
153
+ be missed.
154
+
155
+ **One predicate at two arities.** ``Zed(a)`` and ``Zed(a, b)`` are written as
156
+ ``zed(a)`` and ``zed(a,b)``, which a prover reads as two symbols (``zed/1`` and
157
+ ``zed/2``). So does the kit's z3 route across formulas (each formula is
158
+ translated in its own environment and z3 overloads a name by its arity), so the
159
+ two agree and the writer says nothing; the TF0 and TFA writers, which declare one
160
+ type per name, refuse it by name. (Inside ONE formula ``to_z3`` raises on such a
161
+ pair, where the writers write two symbols.)
162
+
163
+ **No conclusion.** ``conclusion=None`` writes no ``conjecture`` line, for a prover
164
+ that is asked whether the premises are satisfiable (Vampire: ``SZS status
165
+ Satisfiable`` or ``Unsatisfiable``). Every check, the cross-kind separation, the
166
+ non-emptiness axioms and the returned map work on the premises alone, in the
167
+ ``fof``, TF0 and TFA writers alike.
168
+
169
+ That asymmetry — a name that is not TPTP-legal, or that clashes across kinds,
170
+ is renamed and recorded, while two legal names of one kind that fold together
171
+ are refused by name — is deliberate for this release. The same-kind refusal
172
+ predates the name map and is kept so that no existing caller silently receives
173
+ a renamed symbol; it is not a claim that the two cases differ in principle.
174
+
175
+ **A variable that has no TPTP spelling.** A variable is written as the upper-case
176
+ of its name, and a TPTP variable is an upper-case letter followed by letters, digits
177
+ and underscores, so ``ä`` (written ``Ä``), ``x-1`` and ``1x`` cannot be written as
178
+ they are, and every prover rejects the text. A variable is BOUND, so the writer
179
+ renames it without recording anything
180
+ (:func:`unicode_logic_kit.fol._tptp_symbols.legalise_variables`): per formula,
181
+ injectively (``ä`` and ``Ä`` stay two variables) and capture-free (the new name,
182
+ ``x0``, ``x1``, ... minted through :mod:`unicode_logic_kit.fol._identifiers`, equals no
183
+ variable of the formula as written). A formula whose variables are all legal is
184
+ passed on as the very same object. The single ``Node.to_tptp`` has no map and
185
+ refuses such a variable by name.
186
+
187
+ **``$true`` and ``$false``.** TPTP defines these two propositions and this kit's
188
+ reader produces them as the nullary atoms ``Atom('$true')`` / ``Atom('$false')``, so
189
+ a problem read with :func:`~unicode_logic_kit.fol.tptp_input.parse_tptp` and written
190
+ back must keep them: they are written verbatim, never renamed, never declared (TF0,
191
+ TFA) and never counted by the collision checks, and ``to_z3`` reads them as true and
192
+ false so that z3 and the provers agree. Any other ``$``-word as a predicate,
193
+ function or constant name, and ``$true`` itself with arguments, is an ordinary user
194
+ name that is not a TPTP word, and the writers rewrite it like ``has-part``
195
+ (``$foo`` is ``u0024foo``).
196
+
197
+ **Many-sorted (MSFOL) soundness.** The kit has ONE universe; a sort ``S`` is the
198
+ extension of the unary predicate ``S`` (the sort and the predicate of that name
199
+ are one symbol) and is never empty; sorts may overlap; a sorted constant ``c:S``
200
+ is in ``S`` (and in every other sort it is written with); an unannotated constant,
201
+ an unsorted variable and the value of a function may be any element. A sorted
202
+ quantifier/constant/count lowers (via ``Node.to_tptp()``'s auto-reduction,
203
+ ``fol.nodes.to_fol``) to a plain unary predicate guard, which by itself says
204
+ neither that the guarded sort is non-empty nor that a sorted constant is in its
205
+ sort. This ``fof`` builder is the route that asks that question for every sorted
206
+ problem: the backends' automatic mode (``tff=None``) tries the native typed route
207
+ (:func:`~unicode_logic_kit.atp.tptp_tff.generate_tff_problem`) first and falls back
208
+ to THIS writer when the typed text would ask another question, or the typed writer
209
+ does not cover a node of the problem that this one does (see that module's
210
+ docstring, and :func:`generate_tptp_problem_for_prover`), and a caller forcing
211
+ ``tff=False``, or :mod:`atp.twee_entailment` (which has no typed route), reaches it
212
+ directly. So :func:`generate_tptp_problem_with_mapping` adds two kinds of background axiom
213
+ lines, both with the ``axiom`` role — an assumption the conjecture's refutation
214
+ search may use freely, exactly what an entailment's premise side means — never the
215
+ ``conjecture`` role, and never folded into ``Node.to_tptp()`` itself, which stays
216
+ polarity-blind:
217
+
218
+ * one ``fof(nonempty_sort_<i>, axiom, ...).`` line per distinct sort name in
219
+ ``premises``/``conclusion``
220
+ (``unicode_logic_kit.fol._msfl_nodes.nonempty_sort_axioms``). Each axiom is rendered
221
+ straight from ``Node.to_tptp()`` on the RAW kit-level sort name, bypassing
222
+ :func:`_sanitize_for_tptp`'s renaming map entirely: a sorted node's own lazy
223
+ ``to_fol`` reduction elsewhere in the SAME problem is equally unsanitised (see
224
+ this module's own ASCII-legality section — sort names are a narrower, pre-existing
225
+ gap this fix does not touch), so keeping the axiom unsanitised too is what keeps
226
+ both talking about the identical predicate. A sort name that is not a TPTP word is
227
+ therefore refused by name (the guard's legality check sees the predicate it is
228
+ written as, and says it is the guard of the sort S), not rewritten: the TF0
229
+ writer, which keeps sorts in a namespace of their own, rewrites such a sort. The
230
+ guard names are RESERVED in the sanitiser (:func:`_sanitize_for_tptp`), so a
231
+ predicate that has to be rewritten never lands on one: a sort ``Hasu002dpart``
232
+ next to a predicate ``has-part`` (written ``hasu002dpart`` before the guard was
233
+ reserved, which merged the two into one symbol) now gets ``Hasu002dpart2`` for
234
+ the predicate.
235
+ * one ``fof(sort_member_<i>, axiom, S(c)).`` line per distinct sorted constant
236
+ ``c:S`` (``unicode_logic_kit.fol._msfl_nodes.sort_membership_axioms``; a constant
237
+ written with two sorts has two lines; in the order the pairs first occur). Unlike
238
+ a sort name a CONSTANT is renamed on its way into the problem (``human:Human`` is
239
+ written ``human_term`` because the sort guard is the word ``human``, ``9lives`` is
240
+ ``n9lives``, ``sókrates`` is ``su00f3krates``), so these atoms are built from the
241
+ formulas AS SANITISED AND SEPARATED, whose constants are the tokens the premises
242
+ use; an atom built from the raw names would say ``human(human)`` about another
243
+ symbol and silently do nothing. The sort is the raw sort name, as in the
244
+ non-emptiness lines, and every constant is one of the formulas' own, so the
245
+ returned name map covers what the lines write.
246
+
247
+ Both are empty for a problem without a sorted node (and the second for one without
248
+ a sorted constant), so the generated text is byte-identical to before for those.
249
+ """
250
+
251
+ import re
252
+ from dataclasses import dataclass, field
253
+ from typing import Callable, Dict, FrozenSet, List, Optional, Sequence, Tuple, Union
254
+
255
+ from ..fol._fol_nodes import constant_name_to_ascii, tptp_fold_first_letter
256
+ from ..fol._msfl_nodes import nonempty_sort_axioms, sort_membership_axioms
257
+ from ..fol._numeral_symbols import numeral_value, numerals_as_constants
258
+ from ..fol._tptp_symbols import (
259
+ check_no_symbol_collisions, is_tptp_boolean_atom, legalise_variables,
260
+ )
261
+ from ..fol.nodes import (
262
+ Atom, Constant, Function, Measure, Node, Number, SortedConstant, Variable, free_variables,
263
+ )
264
+ from ._ascii_names import ascii_safe_base, reserve_rendered
265
+ from ._writer_support import (
266
+ check_against_generated, normalise_premise_names, tptp_name_token,
267
+ )
268
+ # generate_tff_problem lives in tptp_tff.py (the native TF0/typed-TPTP
269
+ # writer, a sibling module rather than an addition to this fof-only one —
270
+ # see that module's docstring); re-exported here purely so a caller already
271
+ # depending on "the shared TPTP problem generator module" for the fof route
272
+ # finds the typed sibling at the same place, per the natural pairing with
273
+ # generate_tptp_problem above.
274
+ from .tptp_tff import (
275
+ Tf0Refusal, generate_tff_problem, generate_tff_problem_with_mapping,
276
+ problem_needs_tff, _separated_term_token, _tptp_word_base,
277
+ )
278
+
279
+ __all__ = ["generate_tptp_problem", "generate_tptp_problem_with_mapping",
280
+ "generate_tptp_problem_for_prover", "TptpProblem",
281
+ "TptpNameMap", "apply_reverse_tptp", "generate_tff_problem",
282
+ "generate_tff_problem_with_mapping"]
283
+
284
+
285
+ # ---------------------------------------------------------------------------
286
+ # ASCII/legality sanitisation — see the module docstring's second section.
287
+ # ---------------------------------------------------------------------------
288
+
289
+ # A raw kit-level name that is ALREADY safe to hand to Node.to_tptp(): pure
290
+ # ASCII, letter-initial (so the fold turns it into a legal lower_word no
291
+ # matter its case), and containing only the characters lower_word allows
292
+ # after that. Names the widened parser can now produce never contain
293
+ # anything outside this (unicode letters/digits/underscore/combining marks
294
+ # only), so this is the exact complement of "needs a replacement".
295
+ _TPTP_SAFE_RE = re.compile(r"[A-Za-z][A-Za-z0-9_]*")
296
+
297
+
298
+ def _is_tptp_safe(name: str) -> bool:
299
+ return bool(name) and name.isascii() and bool(_TPTP_SAFE_RE.fullmatch(name))
300
+
301
+
302
+ @dataclass
303
+ class _Renamer:
304
+ """One symbol namespace's original->safe-token map, whole-problem-shared.
305
+
306
+ Two passes, run over the WHOLE problem before any text is rendered:
307
+
308
+ 1. :meth:`collect` — called once per occurrence of every name in this
309
+ namespace, in problem order. An already-legal name is reserved
310
+ immediately and unconditionally (R1: it is never touched, so its
311
+ reservation cannot depend on what else is in the problem); anything
312
+ else is queued, deduplicated by first occurrence.
313
+ 2. :meth:`finalize` — synthesises a token for every queued name, each
314
+ de-collided (:func:`reserve_rendered`) against ``used`` as it now
315
+ stands: every already-legal name in the WHOLE problem, not just the
316
+ ones that happened to appear earlier in iteration order.
317
+
318
+ Doing this in one combined pass (synthesise-as-you-go) would make
319
+ collision-avoidance depend on argument order: a synthesised name could
320
+ legitimately claim a token that a DIFFERENT, already-legal name
321
+ appearing LATER in the same problem also owns — since R1 forbids moving
322
+ the legal name off of it, that is a genuine, unavoidable ambiguity, but
323
+ one this two-pass split avoids ever manufacturing purely from processing
324
+ order (R2: "two different names never collide" holds regardless of
325
+ where in the problem each one appears). :meth:`get` is only valid after
326
+ :meth:`finalize` — every name this namespace will ever be asked about
327
+ must have gone through :meth:`collect` first.
328
+ """
329
+
330
+ prefix: str
331
+ render: Callable[[str], str]
332
+ case_fix: Callable[[str], str]
333
+ mapping: Dict[str, str] = field(default_factory=dict)
334
+ used: set = field(default_factory=set)
335
+ _pending: List[str] = field(default_factory=list)
336
+
337
+ def collect(self, name: str) -> None:
338
+ if name in self.mapping or name in self._pending:
339
+ return
340
+ if _is_tptp_safe(name):
341
+ self.used.add(self.render(name))
342
+ self.mapping[name] = name
343
+ else:
344
+ self._pending.append(name)
345
+
346
+ def finalize(self) -> None:
347
+ for name in self._pending:
348
+ base = self.case_fix(_tptp_word_base(name, self.prefix))
349
+ token = reserve_rendered(base, self.used, self.render)
350
+ self.mapping[name] = token
351
+ self._pending = []
352
+
353
+ def get(self, name: str) -> str:
354
+ return self.mapping[name]
355
+
356
+
357
+ def _predicate_base_case(base: str) -> str:
358
+ """Force uppercase-initial — the kit's own PREDICATE convention.
359
+
360
+ Necessary for round-tripping, not merely stylistic: TPTP's own fold
361
+ always lower-cases whatever we export, and ``tptp_input.py``'s ``_cap()``
362
+ always UPPER-cases the first letter of whatever text a prover echoes
363
+ back — regardless of what we originally exported. A synthesised
364
+ predicate token therefore has to already BE upper-case-initial, or the
365
+ reverse mapping (keyed by the token we chose) would never match what
366
+ comes back from ``_cap()``. Function/constant names need no such fix:
367
+ neither export nor import case-folds them at all (see
368
+ :func:`_term_base_case`), so any ASCII letter-initial form round-trips
369
+ verbatim.
370
+ """
371
+ return base[0].upper() + base[1:] if base else base
372
+
373
+
374
+ def _term_base_case(base: str) -> str:
375
+ """Force lowercase-initial — the kit's own NAME (function/constant)
376
+ convention, and, since neither export nor import case-folds a
377
+ function/constant name at all, the form that makes ``render(candidate)
378
+ == candidate`` (no fold vs. no-fold asymmetry to reverse)."""
379
+ return base[0].lower() + base[1:] if base else base
380
+
381
+
382
+ @dataclass
383
+ class TptpNameMap:
384
+ """The renamings :func:`_sanitize_for_tptp` chose for one problem.
385
+
386
+ ``predicate`` and ``term`` are original-kit-name -> raw-token dicts (the
387
+ exact string substituted into the sanitised AST, BEFORE ``Node.to_tptp``'s
388
+ own fold) for the predicate namespace and the shared function/constant
389
+ namespace respectively — mirroring the two-namespace split
390
+ :func:`_check_no_symbol_collisions` already uses. An original name that
391
+ was already TPTP-legal maps to itself (see :class:`_Renamer`), so
392
+ :meth:`reverse` inverts cleanly even for untouched names.
393
+
394
+ A function/constant whose rendered name equals a predicate's (the class
395
+ ``Agent`` and the role function ``agent``) is recorded here too, under
396
+ ``term``, mapped to its ``<name>_term`` replacement
397
+ (:func:`_separate_term_names`); no token of ``term`` renders like any token
398
+ of ``predicate``, so :meth:`reverse_rendered` never has to choose between
399
+ the two kinds for one piece of prover text.
400
+
401
+ A writer also records what it called its lines, which a symbol rename does not
402
+ cover: ``premises`` holds the premise names in order (the names the caller gave with
403
+ ``premise_names=``, or ``premise_1``, ``premise_2``, ... by default; empty for a map
404
+ that no writer made) and ``background`` the axiom lines the writer added on its own
405
+ as ``(name, meaning)`` pairs (the non-emptiness line of a sort and the membership
406
+ line of a sorted constant). A reader of a prover's proof goes through them
407
+ (:func:`~unicode_logic_kit.atp.tstp.relevant_premises_from_tstp`). They are not renames,
408
+ so they take no part in ``==``: two maps are equal when they rename alike.
409
+
410
+ A numeral is written as a constant (see the module docstring), so it is a ``term`` entry:
411
+ its key is the name :func:`~unicode_logic_kit.fol._numeral_symbols.numeral_name` gives its
412
+ value (``'1'``, ``'2.5'``) and its value the word it was written as. ``numerals`` holds
413
+ those keys, which is what tells :func:`apply_reverse_tptp` that the word stands for a
414
+ :class:`~unicode_logic_kit.fol.nodes.Number` and not for a constant spelled alike (a problem
415
+ never has both: that is refused). The arithmetic operators are ``term`` entries (``'+'``)
416
+ and ``predicate`` entries (``'<'``) like any other name when the writer wrote them as
417
+ ordinary symbols.
418
+ """
419
+
420
+ predicate: Dict[str, str] = field(default_factory=dict)
421
+ term: Dict[str, str] = field(default_factory=dict)
422
+ premises: Tuple[str, ...] = field(default=(), compare=False)
423
+ background: Tuple[Tuple[str, str], ...] = field(default=(), compare=False)
424
+ numerals: FrozenSet[str] = field(default=frozenset())
425
+
426
+ def reverse_numerals(self) -> Dict[str, Union[int, float]]:
427
+ """Return ``{word: value}`` for every numeral the writer wrote: the word a prover's
428
+ text has (the token the writer chose for it) and the value of the
429
+ :class:`~unicode_logic_kit.fol.nodes.Number` it stands for."""
430
+ return {token: numeral_value(name) for name, token in self.term.items()
431
+ if name in self.numerals}
432
+
433
+ def reverse(self) -> Tuple[Dict[str, str], Dict[str, str]]:
434
+ """Return ``(predicate_reverse, term_reverse)``: token -> original.
435
+
436
+ The token used as the reverse-dict KEY is exactly what a prover's
437
+ own output re-parsed via :func:`~unicode_logic_kit.fol.tptp_input
438
+ .parse_tptp_formula` produces for that symbol — see
439
+ :func:`_predicate_base_case`/:func:`_term_base_case`'s docstrings for
440
+ why that already equals the raw token stored in ``predicate``/
441
+ ``term`` (no extra fold/cap step needed here). Use this for the
442
+ STRUCTURED Rückweg (:func:`apply_reverse_tptp`, and anything that
443
+ goes through it such as :func:`~unicode_logic_kit.atp.tstp
444
+ .reverse_map_derivation`) — never for raw, un-parsed prover text; see
445
+ :meth:`reverse_rendered` for that.
446
+ """
447
+ return ({v: k for k, v in self.predicate.items()},
448
+ {v: k for k, v in self.term.items()})
449
+
450
+ def reverse_rendered(self) -> Tuple[Dict[str, str], Dict[str, str]]:
451
+ """Return ``(predicate_reverse, term_reverse)`` keyed by the
452
+ RENDERED token — exactly the text ``Node.to_tptp()`` actually wrote
453
+ into the generated problem, and therefore exactly what a prover
454
+ echoes back UNPARSED (raw stdout, an SZS detail string, and similar
455
+ free text — R3's "Erklärungstexte").
456
+
457
+ :meth:`reverse` is keyed by the raw, pre-fold token instead, which is
458
+ the right key for the STRUCTURED Rückweg (:func:`apply_reverse_tptp`
459
+ re-parses a prover's TSTP text via
460
+ :func:`~unicode_logic_kit.fol.tptp_input.parse_tptp_formula` first,
461
+ which re-applies the kit's uppercase-initial predicate convention on
462
+ import — see :func:`_predicate_base_case` — undoing the export-time
463
+ fold before this mapping is ever consulted) but the WRONG key for
464
+ free text that was never re-parsed: a synthesised or already-legal
465
+ predicate token such as ``Human`` renders as ``human`` (only the
466
+ first character is folded, :func:`tptp_fold_first_letter`), so raw
467
+ prover stdout contains ``human``, not ``Human`` — a reverse dict
468
+ keyed by ``Human`` never matches it, and the original name is never
469
+ restored (this is exactly the bug this method fixes). Term
470
+ (function/constant) tokens are unaffected in practice — they are
471
+ already chosen/kept lowercase-initial (:func:`_term_base_case`), so
472
+ rendering them again is a no-op — but this method renders them the
473
+ same way regardless, so it stays correct even for a term name built
474
+ directly (e.g. a bare ``Constant("Foo")``) outside the parser's own
475
+ lowercase-initial NAME convention rather than assuming every caller
476
+ went through it.
477
+
478
+ Injective by construction: :class:`_Renamer` already de-collides
479
+ every name in a namespace on its RENDERED form (:func:`reserve_rendered`,
480
+ and the immediate ``self.used.add(self.render(name))`` for an
481
+ already-legal name in :meth:`_Renamer.collect`) before assigning it a
482
+ token, so two distinct original names can never render to the same
483
+ text within one namespace — this dict can never silently drop or
484
+ merge an entry.
485
+ """
486
+ pred_rendered = {tptp_fold_first_letter(v): k for k, v in self.predicate.items()}
487
+ term_rendered = {tptp_fold_first_letter(constant_name_to_ascii(v)): k
488
+ for k, v in self.term.items()}
489
+ return pred_rendered, term_rendered
490
+
491
+
492
+ def _is_fixed_atom(atom: Atom, uninterpreted_arithmetic: bool) -> bool:
493
+ """Whether ``atom`` is written with a token of the TPTP language itself and so is never
494
+ renamed: equality and disequality, ``$true`` / ``$false`` and, unless the problem reads
495
+ the comparisons as ordinary predicates, ``<``, ``>``, ``≤`` and ``≥``."""
496
+ return (atom.predicate in Atom.INFIX_PREDS_TPTP or is_tptp_boolean_atom(atom)
497
+ or (not uninterpreted_arithmetic and atom.predicate in Atom.PREFIX_PREDS_TPTP))
498
+
499
+
500
+ def _is_fixed_function(function: Function, uninterpreted_arithmetic: bool) -> bool:
501
+ """Whether ``function`` is written with a dollar-word of TPTP (``$sum``, ...) and so is
502
+ never renamed: an arithmetic operator, unless the problem reads it as an ordinary
503
+ function."""
504
+ return not uninterpreted_arithmetic and function.name in Function.TPTP_ARITH_OPS
505
+
506
+
507
+ def _sanitize_node_for_tptp(node: Node, predicates: _Renamer, terms: _Renamer,
508
+ uninterpreted_arithmetic: bool = False) -> Node:
509
+ """Rebuild ``node`` with every non-TPTP-legal symbol name replaced.
510
+
511
+ Structural recursion via ``Node.map_children`` (see
512
+ :mod:`fol.sanitize`'s ``_rewrite`` for the same pattern); an
513
+ already-legal name comes back as the exact same string, so a node whose
514
+ own name and every descendant's name were already legal is rebuilt with
515
+ identical field values throughout — ``Node.to_tptp()`` on the result is
516
+ therefore byte-identical to ``Node.to_tptp()`` on the original (R1).
517
+
518
+ ``uninterpreted_arithmetic`` makes ``+ - * /`` and ``< > ≤ ≥`` names like any other
519
+ (renamed to a word of a prover's grammar); without it they are left alone, to be written
520
+ as TPTP's own ``$sum``, ``$less``, ... (the arithmetic reading).
521
+ """
522
+ if isinstance(node, Atom):
523
+ if _is_fixed_atom(node, uninterpreted_arithmetic):
524
+ pred = node.predicate
525
+ else:
526
+ pred = predicates.get(node.predicate)
527
+ return Atom(pred, tuple(_sanitize_node_for_tptp(a, predicates, terms, uninterpreted_arithmetic)
528
+ for a in node.args))
529
+ if isinstance(node, Function):
530
+ if _is_fixed_function(node, uninterpreted_arithmetic):
531
+ name = node.name
532
+ else:
533
+ name = terms.get(node.name)
534
+ return Function(name, tuple(_sanitize_node_for_tptp(a, predicates, terms, uninterpreted_arithmetic)
535
+ for a in node.args))
536
+ if isinstance(node, Constant):
537
+ return Constant(terms.get(node.name))
538
+ if isinstance(node, SortedConstant):
539
+ # Renders (via to_fol) as the plain Constant of the same name, so it
540
+ # is the same symbol and must get the same token as that Constant.
541
+ return SortedConstant(terms.get(node.name), node.sort)
542
+ return node.map_children(
543
+ lambda c: _sanitize_node_for_tptp(c, predicates, terms, uninterpreted_arithmetic))
544
+
545
+
546
+ def _collect_names_for_tptp(node: Node, predicates: _Renamer, terms: _Renamer,
547
+ uninterpreted_arithmetic: bool = False) -> None:
548
+ """First pass (see :class:`_Renamer`): register every predicate/
549
+ function/constant name ``node`` (and its descendants) uses, without
550
+ rewriting anything yet."""
551
+ for n in node.walk():
552
+ if isinstance(n, Atom):
553
+ if not _is_fixed_atom(n, uninterpreted_arithmetic):
554
+ predicates.collect(n.predicate)
555
+ elif isinstance(n, Function):
556
+ if not _is_fixed_function(n, uninterpreted_arithmetic):
557
+ terms.collect(n.name)
558
+ elif isinstance(n, (Constant, SortedConstant)):
559
+ terms.collect(n.name)
560
+
561
+
562
+ def _sanitize_for_tptp(formulas: List[Node],
563
+ sort_guards: Tuple[str, ...] = (),
564
+ *, uninterpreted_arithmetic: bool = False,
565
+ numerals: FrozenSet[str] = frozenset()) -> Tuple[List[Node], TptpNameMap]:
566
+ """Sanitise every formula's predicate/function/constant names for TPTP.
567
+
568
+ Returns ``(sanitised_formulas, mapping)`` — see the module docstring's
569
+ ASCII-sanitisation section. Both namespaces (predicate; function+constant)
570
+ are shared across ALL of ``formulas``, so the same original name maps to
571
+ the same token everywhere (R2), and a synthesised token can never
572
+ collide with any name anywhere in the problem, regardless of where each
573
+ one appears (:class:`_Renamer`'s two-pass collect/finalize split).
574
+
575
+ ``sort_guards`` are the kit names of the sorts of the problem, which the fof
576
+ writer writes as predicates (the guard of a sort). They appear in no formula
577
+ this function walks, yet their words are in the problem, so they are RESERVED
578
+ in the predicate namespace: a token synthesised for a predicate that is not a
579
+ TPTP word never equals one (``has-part`` next to the sort ``Hasu002dpart``
580
+ becomes ``Hasu002dpart2``, not the sort's own word). Nothing is recorded in
581
+ the map for a guard: it is not renamed.
582
+
583
+ ``uninterpreted_arithmetic`` reads ``+ - * /`` and ``< > ≤ ≥`` as ordinary function and
584
+ predicate names, renamed and recorded like any other; the default leaves them to be
585
+ written as TPTP's own arithmetic words. ``numerals`` are the names of the constants that
586
+ stand for numerals (:func:`~unicode_logic_kit.fol._numeral_symbols.numerals_as_constants`),
587
+ which the returned map records as such.
588
+ """
589
+ predicates = _Renamer(prefix="p", render=tptp_fold_first_letter,
590
+ case_fix=_predicate_base_case)
591
+ terms = _Renamer(prefix="n", case_fix=_term_base_case,
592
+ render=lambda n: tptp_fold_first_letter(constant_name_to_ascii(n)))
593
+ for f in formulas:
594
+ _collect_names_for_tptp(f, predicates, terms, uninterpreted_arithmetic)
595
+ for guard in sort_guards:
596
+ predicates.used.add(predicates.render(guard))
597
+ predicates.finalize()
598
+ terms.finalize()
599
+ sanitised = [_sanitize_node_for_tptp(f, predicates, terms, uninterpreted_arithmetic)
600
+ for f in formulas]
601
+ mapping = TptpNameMap(predicate=predicates.mapping, term=terms.mapping, numerals=numerals)
602
+ return sanitised, mapping
603
+
604
+
605
+ def _rename_terms(node: Node, table: Dict[str, str]) -> Node:
606
+ """Rebuild ``node`` with every function/constant name found in ``table``
607
+ replaced by its value; predicates, variables and every other name stay.
608
+ A node that carries no such name is rebuilt equal to the original.
609
+
610
+ A :class:`~unicode_logic_kit.fol.nodes.Measure` writes the binary function
611
+ ``measure`` (it is that very symbol: ``to_z3`` declares ``measure/2`` for it
612
+ too), so it is renamed like ``Function('measure', ...)``, and when ``measure``
613
+ is in ``table`` it is rebuilt as that function."""
614
+ if isinstance(node, Atom):
615
+ return Atom(node.predicate, tuple(_rename_terms(a, table) for a in node.args))
616
+ if isinstance(node, Measure) and "measure" in table:
617
+ return Function(table["measure"], (_rename_terms(node.entity, table),
618
+ _rename_terms(node.dimension, table)))
619
+ if isinstance(node, Function):
620
+ name = node.name if node.name in Function.TPTP_ARITH_OPS else table.get(node.name, node.name)
621
+ return Function(name, tuple(_rename_terms(a, table) for a in node.args))
622
+ if isinstance(node, Constant):
623
+ return Constant(table.get(node.name, node.name))
624
+ if isinstance(node, SortedConstant):
625
+ return SortedConstant(table.get(node.name, node.name), node.sort)
626
+ return node.map_children(lambda c: _rename_terms(c, table))
627
+
628
+
629
+ def _separate_term_names(formulas: List[Node], mapping: TptpNameMap,
630
+ sort_names: Tuple[str, ...] = ()
631
+ ) -> Tuple[List[Node], TptpNameMap]:
632
+ """Make the problem injective ACROSS kinds: no function/constant may render
633
+ as the name of a predicate (see the module docstring's "A predicate and a
634
+ function/constant that fold to the SAME identifier").
635
+
636
+ ``formulas``/``mapping`` are :func:`_sanitize_for_tptp`'s output, already
637
+ past :func:`_check_no_symbol_collisions`. Every function/constant token
638
+ whose rendered name (the text ``Node.to_tptp`` writes) equals the rendered
639
+ name of a predicate — or of a sort guard predicate named in
640
+ ``sort_names`` — is replaced, in every formula, by a fresh token
641
+ (:func:`~unicode_logic_kit.atp.tptp_tff._separated_term_token`:
642
+ ``<name>_term`` plus a numeric suffix if taken) that equals no rendered
643
+ predicate and no rendered term of the problem. The tokens are visited in
644
+ sorted order, so the result depends only on WHICH symbols the problem
645
+ contains, never on the order of its formulas or on any hash ordering.
646
+
647
+ Returns ``(formulas, mapping)``; ``mapping.term`` maps each affected
648
+ ORIGINAL kit name to its replacement, which is what lets
649
+ :func:`apply_reverse_tptp` restore it. With nothing to separate the inputs
650
+ are returned unchanged (the same objects), so the rendered text is
651
+ byte-identical to what it was before this pass existed.
652
+ """
653
+ def render(token: str) -> str:
654
+ return tptp_fold_first_letter(constant_name_to_ascii(token))
655
+
656
+ predicate_names = {tptp_fold_first_letter(s) for s in sort_names}
657
+ tokens = set()
658
+ for formula in formulas:
659
+ for node in formula.walk():
660
+ if isinstance(node, Atom):
661
+ if (node.predicate not in Atom.INFIX_PREDS_TPTP and node.predicate not in Atom.PREFIX_PREDS_TPTP
662
+ and not is_tptp_boolean_atom(node)):
663
+ predicate_names.add(tptp_fold_first_letter(node.predicate))
664
+ elif isinstance(node, Function):
665
+ if node.name not in Function.TPTP_ARITH_OPS:
666
+ tokens.add(node.name)
667
+ elif isinstance(node, (Constant, SortedConstant)):
668
+ tokens.add(node.name)
669
+ elif isinstance(node, Measure):
670
+ tokens.add("measure") # the function a Measure node is written as
671
+ clashing = sorted(t for t in tokens if render(t) in predicate_names)
672
+ if not clashing:
673
+ return formulas, mapping
674
+
675
+ taken = predicate_names | {render(t) for t in tokens}
676
+ replacement = {t: _separated_term_token(t, taken, render) for t in clashing}
677
+ original_of = {token: original for original, token in mapping.term.items()}
678
+ term = dict(mapping.term)
679
+ for token, new in replacement.items():
680
+ term[original_of.get(token, token)] = new
681
+ separated = [_rename_terms(f, replacement) for f in formulas]
682
+ return separated, TptpNameMap(predicate=dict(mapping.predicate), term=term,
683
+ numerals=mapping.numerals)
684
+
685
+
686
+ def apply_reverse_tptp(node: Node, mapping: TptpNameMap) -> Node:
687
+ """Translate a ``Node`` parsed from a TPTP-family prover's OWN output
688
+ (e.g. one TSTP proof step) back to original kit-level names.
689
+
690
+ Walks ``node`` exactly the way :func:`_sanitize_node_for_tptp` walked the
691
+ export direction, looking up each predicate/function/constant name in
692
+ ``mapping.reverse()``'s tables. A name the prover introduced itself (a
693
+ Skolem constant, a CNF-clausification symbol — ``sK1``, ``esk1_0``, and
694
+ similar) was never one of ours to begin with, so it has no entry in
695
+ either table and is left exactly as the prover printed it, not guessed
696
+ at or dropped. The word a numeral was written as comes back as the
697
+ :class:`~unicode_logic_kit.fol.nodes.Number` of its value (``n1`` is ``Number(1)``), and the
698
+ word of an operator written as an ordinary symbol as the operator (``u002b`` is ``+``).
699
+ """
700
+ pred_rev, term_rev = mapping.reverse()
701
+ return _apply_reverse_tptp(node, pred_rev, term_rev, mapping.reverse_numerals())
702
+
703
+
704
+ def _apply_reverse_tptp(node: Node, pred_rev: Dict[str, str], term_rev: Dict[str, str],
705
+ numeral_rev: Optional[Dict[str, Union[int, float]]] = None) -> Node:
706
+ numeral_rev = numeral_rev or {}
707
+ if isinstance(node, Atom):
708
+ if node.predicate in Atom.INFIX_PREDS_TPTP or node.predicate in Atom.PREFIX_PREDS_TPTP:
709
+ pred = node.predicate
710
+ else:
711
+ pred = pred_rev.get(node.predicate, node.predicate)
712
+ return Atom(pred, tuple(_apply_reverse_tptp(a, pred_rev, term_rev, numeral_rev)
713
+ for a in node.args))
714
+ if isinstance(node, Function):
715
+ # A parsed dollar-function statement (Vampire/E echoing $sum(...)
716
+ # etc. back) already comes out of parse_tptp_formula with the
717
+ # KIT-level operator name (tptp_input.py's dollar_func_app maps
718
+ # "$sum" -> "+" before this function ever sees it) — the same
719
+ # names Function.TPTP_ARITH_OPS is keyed by, not its dollar-word
720
+ # values, so this mirrors the forward-direction check exactly.
721
+ if node.name in Function.TPTP_ARITH_OPS:
722
+ name = node.name
723
+ else:
724
+ name = term_rev.get(node.name, node.name)
725
+ return Function(name, tuple(_apply_reverse_tptp(a, pred_rev, term_rev, numeral_rev)
726
+ for a in node.args))
727
+ if isinstance(node, Constant):
728
+ if node.name in numeral_rev:
729
+ return Number(numeral_rev[node.name])
730
+ return Constant(term_rev.get(node.name, node.name))
731
+ if isinstance(node, SortedConstant):
732
+ return SortedConstant(term_rev.get(node.name, node.name), node.sort)
733
+ return node.map_children(lambda c: _apply_reverse_tptp(c, pred_rev, term_rev, numeral_rev))
734
+
735
+
736
+ # ---------------------------------------------------------------------------
737
+ # Cross-formula case-fold collision guard (pre-existing; now runs on the
738
+ # ASCII-sanitised formulas — see the module docstring).
739
+ # ---------------------------------------------------------------------------
740
+
741
+ def _check_no_symbol_collisions(formulas: List[Node], *,
742
+ where: str = "generate_tptp_problem",
743
+ subject: str = "problem",
744
+ sorts: Tuple[str, ...] = ()) -> None:
745
+ """Raise ``NotImplementedError`` if any two distinct predicate names, or
746
+ any two distinct function/constant names, across ``formulas`` would fold
747
+ to the same TPTP identifier under :meth:`Node.to_tptp` — see the module
748
+ docstring. The check itself is
749
+ :func:`unicode_logic_kit.fol._tptp_symbols.check_no_symbol_collisions`, the
750
+ ONE implementation that the outermost ``Node.to_tptp()`` call also runs
751
+ over the single formula it renders; here it runs over every premise and
752
+ the conclusion TOGETHER, which no single formula's check can — plus, in
753
+ :func:`generate_tptp_problem_with_mapping`, the non-emptiness axioms, whose
754
+ atoms are the guard predicates the sorted nodes lower to.
755
+
756
+ ``where`` and ``subject`` name the writer that is calling and what it was
757
+ given, so a refusal says which entry point refused (the TFA writer and
758
+ :func:`~unicode_logic_kit.atp.tstp.to_tstp` run this check too); ``sorts`` are
759
+ the kit names of the sorts of the problem, so that the refusal of an illegal
760
+ guard predicate says it is a sort.
761
+ """
762
+ check_no_symbol_collisions(formulas, where=where, subject=subject, sorts=sorts)
763
+
764
+
765
+ def generate_tptp_problem_with_mapping(premises: List[Node],
766
+ conclusion: Optional[Node] = None,
767
+ *, premise_names: Optional[Sequence[str]] = None
768
+ ) -> Tuple[str, TptpNameMap]:
769
+ """Like :func:`generate_tptp_problem`, but also returns the
770
+ :class:`TptpNameMap` recording every rename it applied: the ASCII-legality
771
+ ones and the function/constant renamed because its TPTP name equals a
772
+ predicate's (see the module docstring), the premise names and the background
773
+ axioms it added.
774
+
775
+ ``conclusion=None`` writes a problem WITHOUT a conjecture, for a prover that
776
+ is asked whether the premises are satisfiable (Vampire answers ``SZS status
777
+ Satisfiable`` or ``Unsatisfiable``). Every check and rename below works on the
778
+ premises alone, and the map is the map of the premises.
779
+
780
+ ``premise_names`` names the premises' ``axiom`` lines: one string per premise,
781
+ written as a TPTP name (a lower word or an integer as it is, anything else
782
+ single-quoted with ``\\`` and ``'`` escaped), pairwise distinct as written and
783
+ distinct from every name the writer gives its own lines (``goal``,
784
+ ``nonempty_sort_<i>``, ``sort_member_<i>``). ``None`` keeps ``premise_<i>``. The
785
+ names are recorded, in order and also when they are the default ones, in the
786
+ returned map's ``premises``, and
787
+ :func:`~unicode_logic_kit.atp.tstp.relevant_premises_from_tstp` reads a prover's
788
+ proof back through them.
789
+
790
+ A free variable in a premise or the conclusion is refused by name: a prover
791
+ reads an unbound variable in a ``fof`` formula as a syntax error, and the answer
792
+ must not depend on whether the problem has a sort.
793
+
794
+ Raises:
795
+ TypeError: ``premise_names`` is a single string, or holds a non-string.
796
+ ValueError: ``premise_names`` has other than one name per premise, holds a
797
+ name no TPTP name can spell (the empty name, a control character), two
798
+ names that are the same as written, or a name the writer gives one of its
799
+ own lines.
800
+ NotImplementedError: as :func:`generate_tptp_problem`, and a free variable.
801
+
802
+ This is the way to build a problem for a prover. Never assemble one by
803
+ joining ``Node.to_tptp()`` strings: a single formula cannot know that two
804
+ of its symbols fold to one TPTP word in ANOTHER formula, which is what the
805
+ checks here are for.
806
+
807
+ Callers that need to translate a prover's OWN output (a proof, a
808
+ countermodel, an unsat core, ...) back to kit-level names — anything
809
+ reading a TSTP derivation via :mod:`atp.tstp`, for instance — must use
810
+ THIS function (not the plain :func:`generate_tptp_problem`) so they have
811
+ the mapping :func:`apply_reverse_tptp` needs. A caller that only wants
812
+ the problem text (nothing reads the answer's symbol names back) can keep
813
+ using :func:`generate_tptp_problem`.
814
+ """
815
+ return _write_fof_problem(premises, conclusion, premise_names,
816
+ where="generate_tptp_problem_with_mapping")
817
+
818
+
819
+ def _refuse_free_variables(premises: List[Node], conclusion: Optional[Node],
820
+ *, where: str) -> None:
821
+ """Refuse, by name, a premise or the conclusion with a free variable.
822
+
823
+ A TPTP ``fof`` formula has no implicit closure for a prover to apply: Vampire stops
824
+ with ``unquantified variable`` and E with ``Formula has free variables``, so the
825
+ problem would come back as a prover's parse error, whether or not it uses a sort.
826
+ The typed writers refuse a free variable too (:class:`~unicode_logic_kit.atp.tptp_tff
827
+ .Tf0Refusal`, and ``ValueError`` in the TFA writer)."""
828
+ for label, formula in ([(f"premise {i}", p) for i, p in enumerate(premises, start=1)]
829
+ + ([] if conclusion is None else [("the conclusion", conclusion)])):
830
+ try:
831
+ free = sorted({v.name for v in free_variables(formula) if isinstance(v, Variable)})
832
+ except TypeError:
833
+ continue # a node ``free_variables`` does not know: to_tptp refuses it
834
+ if free:
835
+ shown = ", ".join(repr(n) for n in free)
836
+ raise NotImplementedError(
837
+ f"{where}: free variable{'s' if len(free) > 1 else ''} {shown} in {label} — "
838
+ "every variable of a TPTP formula must be bound, because a prover reads an "
839
+ "unbound variable in a fof formula as a syntax error (Vampire: "
840
+ "'unquantified variable', E: 'Formula has free variables') and this kit "
841
+ "does not pick a closure for it. Bind the variable with a quantifier "
842
+ "before writing the problem.")
843
+
844
+
845
+ def _nullary_functions_as_constants(formula: Node) -> Node:
846
+ """``formula`` with every function of no arguments written as the constant of its name:
847
+ TPTP has no empty argument list (``f()`` is no term, and Vampire stops with a parse
848
+ error), and the kit defines a function of no arguments as the constant of its name
849
+ (the TF0 and Prover9 writers write it that way too). A formula without one comes back
850
+ as the very same object."""
851
+ stack: List[Node] = [formula]
852
+ while stack:
853
+ node = stack.pop()
854
+ if isinstance(node, Function) and not node.args:
855
+ break
856
+ stack.extend(node._child_nodes())
857
+ else:
858
+ return formula
859
+
860
+ def rewrite(node: Node) -> Node:
861
+ if isinstance(node, Function) and not node.args:
862
+ return Constant(node.name)
863
+ return node.map_children(rewrite)
864
+
865
+ return rewrite(formula)
866
+
867
+
868
+ def _write_fof_problem(premises: Sequence[Node], conclusion: Optional[Node],
869
+ premise_names: Optional[Sequence[str]],
870
+ *, where: str) -> Tuple[str, TptpNameMap]:
871
+ """The ``fof`` writer both :func:`generate_tptp_problem` and
872
+ :func:`generate_tptp_problem_with_mapping` are; ``where`` is the name of the one
873
+ that was called, which a refusal opens with."""
874
+ premises = list(premises)
875
+ names = normalise_premise_names(premise_names, len(premises), where=where)
876
+ formulas = premises + ([] if conclusion is None else [conclusion])
877
+ # A numeral is a constant (see the module docstring): written under the name of its
878
+ # value, so that the renamer below gives it a word and the map reads it back.
879
+ formulas, numerals = numerals_as_constants(formulas, where=where)
880
+ formulas = [_nullary_functions_as_constants(f) for f in formulas]
881
+ # Many-sorted non-emptiness axioms (see the module docstring) — computed
882
+ # first because their sort guard predicates take part in the same-kind check
883
+ # and in the cross-kind separation, exactly like a predicate written in the
884
+ # source: ``∀x:Foo P(x)`` is written with the guard predicate ``Foo`` and the
885
+ # axiom ``∃x Foo(x)``, which no walk of the SOURCE trees would otherwise see.
886
+ # The sanitiser reserves them too, so that a predicate it has to rewrite never
887
+ # lands on the word of a sort.
888
+ sort_axioms = nonempty_sort_axioms(*formulas)
889
+ sort_guards = tuple(a.predicate for ax in sort_axioms for a in ax.walk()
890
+ if isinstance(a, Atom))
891
+ sanitised, mapping = _sanitize_for_tptp(formulas, sort_guards, uninterpreted_arithmetic=True,
892
+ numerals=numerals)
893
+ # A variable that has no TPTP spelling is renamed (per formula, no record).
894
+ sanitised = [legalise_variables(f) for f in sanitised]
895
+ _check_no_symbol_collisions(sanitised + list(sort_axioms), sorts=sort_guards, where=where)
896
+ sanitised, mapping = _separate_term_names(sanitised, mapping, sort_guards)
897
+ # A free variable is refused only after the symbol checks above, which a problem that
898
+ # has both is refused for first (as it always was).
899
+ _refuse_free_variables(premises, conclusion, where=where)
900
+ sanitised_premises = sanitised if conclusion is None else sanitised[:-1]
901
+ # The membership atoms come from the SANITISED formulas (see below); the same atoms
902
+ # over the names as the caller wrote them say what each line means.
903
+ membership = sort_membership_axioms(*sanitised)
904
+ membership_meaning = [f"{a.args[0].name} is in the sort {a.predicate}"
905
+ for a in sort_membership_axioms(*formulas)]
906
+ generated = ([] if conclusion is None else [("goal", "the conjecture")])
907
+ generated += [(f"nonempty_sort_{i}", f"the non-emptiness axiom of the sort {sort!r}")
908
+ for i, sort in enumerate(sort_guards, start=1)]
909
+ generated += [(f"sort_member_{i}", f"the membership axiom ({meaning})")
910
+ for i, meaning in enumerate(membership_meaning, start=1)]
911
+ check_against_generated(names, generated, where=where)
912
+ lines: List[str] = []
913
+ for name, premise in zip(names, sanitised_premises):
914
+ lines.append(f"fof({tptp_name_token(name)}, axiom, {premise.to_tptp()}).")
915
+ # Many-sorted non-emptiness axioms — see the module docstring. Built
916
+ # from the ORIGINAL (pre-sanitisation) premises/conclusion so each one
917
+ # renders the exact raw sort-predicate name a sorted node's own lazy
918
+ # to_tptp()/to_fol reduction emits elsewhere in this same problem.
919
+ for i, axiom in enumerate(sort_axioms, start=1):
920
+ lines.append(f"fof(nonempty_sort_{i}, axiom, {axiom.to_tptp()}).")
921
+ # Sort membership of the sorted constants — the other half of the guard
922
+ # reading (see the module docstring). Unlike the sort NAME, a constant is
923
+ # renamed on its way into the problem (``human:Human`` is written
924
+ # ``human_term``, ``9lives`` is ``n9lives``, ``sókrates`` is transliterated), so
925
+ # the atoms are built from the formulas AS SANITISED AND SEPARATED, whose
926
+ # constants are the very tokens the premises above use: an atom built from the
927
+ # raw names would be about another symbol and silently do nothing. Every
928
+ # constant of these atoms is one of the formulas' own, so the name map already
929
+ # covers it.
930
+ for i, atom in enumerate(membership, start=1):
931
+ lines.append(f"fof(sort_member_{i}, axiom, {atom.to_tptp()}).")
932
+ if conclusion is not None:
933
+ lines.append(f"fof(goal, conjecture, {sanitised[-1].to_tptp()}).")
934
+ mapping.premises = names
935
+ mapping.background = tuple(
936
+ [(f"nonempty_sort_{i}", f"the sort {sort} is not empty")
937
+ for i, sort in enumerate(sort_guards, start=1)]
938
+ + [(f"sort_member_{i}", meaning) for i, meaning in enumerate(membership_meaning, start=1)])
939
+ return "\n".join(lines) + "\n", mapping
940
+
941
+
942
+ def generate_tptp_problem(premises: List[Node], conclusion: Optional[Node] = None,
943
+ *, premise_names: Optional[Sequence[str]] = None) -> str:
944
+ """Build a TPTP ``fof`` problem string from premises and a conclusion.
945
+
946
+ Each premise becomes ``fof(premise_<i>, axiom, <tptp>).`` (1-based) and
947
+ the conclusion becomes ``fof(goal, conjecture, <tptp>).`` — or, with
948
+ ``conclusion=None``, no conjecture line is written at all and the problem asks
949
+ whether the premises are satisfiable. The bodies
950
+ come from ``Node.to_tptp`` (variables upper-cased TPTP-style, predicate/
951
+ function/constant names folded on their first character only — see the
952
+ module docstring), after every name has been made TPTP-ASCII-legal (see
953
+ the module docstring's sanitisation section) — a name that was already
954
+ legal renders byte-identically to before that step existed. Shared
955
+ verbatim by :mod:`atp.vampire_entailment`, :mod:`atp.eprover_backend`,
956
+ and :mod:`atp.twee_entailment`.
957
+
958
+ Before rendering, checks that the export stays injective across every
959
+ premise and the conclusion TOGETHER — see the module docstring's
960
+ soundness-guard section and :func:`_check_no_symbol_collisions` — and
961
+ separates a function/constant from a predicate that would render as the
962
+ same word (:func:`_separate_term_names`; the term becomes
963
+ ``<name>_term``, which only :func:`generate_tptp_problem_with_mapping`
964
+ reports back).
965
+
966
+ A caller that also needs to translate a prover's response back to
967
+ kit-level symbol names should call
968
+ :func:`generate_tptp_problem_with_mapping` instead, which returns the
969
+ same text plus the :class:`TptpNameMap` :func:`apply_reverse_tptp` needs.
970
+
971
+ A numeral is written as a constant and ``+ - * / < > ≤ ≥`` as ordinary symbols, each
972
+ under a word of its own (see the module docstring): ``⊢ 1 ≠ 2`` is the problem
973
+ ``n1 != n2``, which is not a theorem, and ``⊢ 1 + 1 = 2`` is ``u002b(n1,n1) = n2``.
974
+ A function of no arguments is the constant of its name and is written as one
975
+ (``p(fzero)``, never ``p(fzero())``: TPTP has no empty argument list).
976
+
977
+ ``premise_names`` names the premises' lines instead of ``premise_<i>`` — see
978
+ :func:`generate_tptp_problem_with_mapping`, which also records them. A free
979
+ variable in a premise or the conclusion is refused.
980
+
981
+ Raises:
982
+ TypeError: ``premise_names`` is a single string, or holds a non-string.
983
+ ValueError: ``premise_names`` has other than one name per premise, a name no
984
+ TPTP name can spell, two names that are the same as written, or a name
985
+ the writer gives one of its own lines (``goal``, ``nonempty_sort_<i>``,
986
+ ``sort_member_<i>``).
987
+ NotImplementedError: either (a) two distinct predicate names, or two
988
+ distinct function/constant names, would render as the same TPTP
989
+ identifier (the collision guard above; a sort counts as the
990
+ predicate it is written as), or two LEGAL variables of one formula
991
+ would render as one TPTP variable (``x`` and ``X``), or a sort name
992
+ is not a TPTP word, or a constant, sorted constant or function is
993
+ spelled like a numeral of the problem (``Number(1)`` next to
994
+ ``Constant('1')``: one name would be two symbols), or (b) a premise or the
995
+ conclusion contains a node outside the classical FOL fragment
996
+ ``Node.to_tptp`` covers (modal / second-order / Łukasiewicz /
997
+ lambda) — surfaced by ``to_tptp`` itself, unchanged from before
998
+ this module existed.
999
+ """
1000
+ text, _mapping = _write_fof_problem(premises, conclusion, premise_names,
1001
+ where="generate_tptp_problem")
1002
+ return text
1003
+
1004
+
1005
+ @dataclass(frozen=True)
1006
+ class TptpProblem:
1007
+ """A TPTP problem text with the record a backend needs to run and explain it.
1008
+
1009
+ Fields:
1010
+
1011
+ * ``text`` -- the problem, ``fof`` or ``tff``.
1012
+ * ``name_map`` -- the :class:`TptpNameMap` of the renames the writer applied.
1013
+ * ``dialect`` -- ``"fof"`` or ``"tff"``: which writer produced ``text``.
1014
+ * ``tff_refusal`` -- the typed writer's message when the automatic mode tried
1015
+ it, was refused, and wrote ``fof`` instead; ``None`` otherwise.
1016
+ """
1017
+
1018
+ text: str
1019
+ name_map: TptpNameMap
1020
+ dialect: str
1021
+ tff_refusal: Optional[str] = None
1022
+
1023
+ @property
1024
+ def fallback_note(self) -> Optional[str]:
1025
+ """The sentence a verdict's detail carries when the automatic mode fell back
1026
+ from the typed writer to ``fof`` (and why); ``None`` when it did not."""
1027
+ if self.tff_refusal is None:
1028
+ return None
1029
+ return ("the typed (TF0) writer refused this problem, so it was written as an "
1030
+ "untyped fof problem, which reads a sort as the unary predicate of its "
1031
+ "name and asserts that no sort is empty and that every sorted constant "
1032
+ f"is in its sort. The typed writer said: {self.tff_refusal}")
1033
+
1034
+
1035
+ def generate_tptp_problem_for_prover(premises: List[Node],
1036
+ conclusion: Optional[Node] = None,
1037
+ *, tff: Optional[bool] = None,
1038
+ fof_writer: Optional[Callable[..., Tuple[str, TptpNameMap]]] = None,
1039
+ premise_names: Optional[Sequence[str]] = None
1040
+ ) -> TptpProblem:
1041
+ """The problem a TPTP prover is given for ``premises ⊨ conclusion``, in the
1042
+ dialect ``tff`` selects.
1043
+
1044
+ * ``tff=False`` -- the ``fof`` writer
1045
+ (:func:`generate_tptp_problem_with_mapping`).
1046
+ * ``tff=True`` -- the typed TF0 writer
1047
+ (:func:`~unicode_logic_kit.atp.tptp_tff.generate_tff_problem_with_mapping`).
1048
+ What it refuses stays refused: :class:`~unicode_logic_kit.atp.tptp_tff.Tf0Refusal`
1049
+ (a ``ValueError`` and a ``NotImplementedError``) with its message, so a
1050
+ backend reports it as ``unknown`` / ``unsupported``.
1051
+ * ``tff=None`` -- the automatic mode: the typed writer when a sorted node
1052
+ occurs (:func:`~unicode_logic_kit.atp.tptp_tff.problem_needs_tff`), the ``fof``
1053
+ writer otherwise. EVERY refusal of the typed writer (a ``NotImplementedError``,
1054
+ which :class:`~unicode_logic_kit.atp.tptp_tff.Tf0Refusal` is a kind of) is
1055
+ answered with the ``fof`` problem: a problem whose typed text would not ask
1056
+ the kit's question (an unannotated constant or a function value that the type
1057
+ inference puts into a sort, an equation over an unsorted variable next to a
1058
+ sort, ...), and a node outside the typed writer's fragment that the ``fof``
1059
+ writer writes (a counting quantifier, ``Contrast``, ``Measure``). The result
1060
+ records that in ``tff_refusal``, and its ``fallback_note`` words it for a
1061
+ verdict's detail. A refusal of the ``fof`` writer itself
1062
+ (``NotImplementedError``) propagates, with the typed writer's refusal as its
1063
+ ``__cause__``.
1064
+
1065
+ A node outside what BOTH writers cover raises ``NotImplementedError`` in every
1066
+ mode, as before.
1067
+
1068
+ ``fof_writer`` is the function that writes the ``fof`` dialect, with the signature
1069
+ of :func:`generate_tptp_problem_with_mapping`, which is the default. A backend
1070
+ passes the name it has bound in its own module, so that the problem text a prover
1071
+ is given stays substitutable at the one place each backend reads it from. It is
1072
+ called with ``premise_names=`` only when the caller passed some.
1073
+
1074
+ ``premise_names`` names the premises' lines in whichever dialect is written (see
1075
+ :func:`generate_tptp_problem_with_mapping`); the returned problem's ``name_map``
1076
+ records them. A refusal that is about the names (a ``ValueError`` or ``TypeError``
1077
+ that is no :class:`~unicode_logic_kit.atp.tptp_tff.Tf0Refusal`) is raised in every
1078
+ mode, and is not what the automatic mode falls back from.
1079
+ """
1080
+ premises = list(premises)
1081
+ refusal: Optional[str] = None
1082
+ typed_refusal: Optional[NotImplementedError] = None
1083
+ if tff is None:
1084
+ use_tff = problem_needs_tff(premises, conclusion)
1085
+ else:
1086
+ use_tff = tff
1087
+ if use_tff:
1088
+ try:
1089
+ text, mapping = generate_tff_problem_with_mapping(
1090
+ premises, conclusion, premise_names=premise_names)
1091
+ except NotImplementedError as exc:
1092
+ # ``Tf0Refusal`` is one, and so is the typed writer's refusal of a node
1093
+ # outside its fragment (``Count``, ``Contrast``, ``Measure``) that the
1094
+ # ``fof`` writer does write.
1095
+ if tff is not None:
1096
+ raise
1097
+ typed_refusal = exc
1098
+ refusal = str(exc)
1099
+ else:
1100
+ return TptpProblem(text, mapping, "tff")
1101
+ write_fof = generate_tptp_problem_with_mapping if fof_writer is None else fof_writer
1102
+ try:
1103
+ if premise_names is None:
1104
+ text, mapping = write_fof(premises, conclusion)
1105
+ else:
1106
+ text, mapping = write_fof(premises, conclusion, premise_names=premise_names)
1107
+ except NotImplementedError as exc:
1108
+ if typed_refusal is not None:
1109
+ raise exc from typed_refusal
1110
+ raise
1111
+ return TptpProblem(text, mapping, "fof", refusal)