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,1546 @@
1
+ """Native TF0 (monomorphic typed first-order TPTP, ``tff``) export.
2
+
3
+ The classical ``fof`` route (:mod:`atp._tptp_problem`) already handles
4
+ many-sorted formulas via :meth:`~unicode_logic_kit.fol.nodes.SortedQuantifier
5
+ .to_tptp`'s auto-reduction: a sort ``S`` becomes an ordinary guard predicate,
6
+ ``∀x:S φ`` becomes ``![X]: (s(X) => <φ>)``. That is sound but throws the
7
+ typing away — an external prover sees an unsorted formula with one extra
8
+ predicate per sort, not a genuinely typed problem. This module instead emits
9
+ TPTP's own typed dialect: one ``tff(name, type, Sym: Type).`` declaration per
10
+ sort/predicate/function/constant, and formula bodies with GENUINE typed
11
+ quantifiers (``![X: sort]: ...``) rather than guard atoms.
12
+
13
+ Scope: TF0 only
14
+ ----------------
15
+ Deliberately narrower than full TFF: no THF (higher-order), no TF1
16
+ polymorphism (type variables, ``!>``), and none of TPTP's built-in
17
+ arithmetic sorts (``$int``/``$rat``/``$real``) — none of those constructs are
18
+ producible by this kit's own AST in the first place (there is no "type
19
+ variable" or "higher-order" node class), so the writer's fragment gate below
20
+ simply enumerates the classical many-sorted FOL node set it understands and
21
+ refuses everything else BY NAME, mirroring
22
+ :mod:`unicode_logic_kit.fol.casl_export`'s ``_check_fragment`` gate for the
23
+ structurally identical problem (CASL's own typed spec syntax).
24
+
25
+ **Numerals and operators are ordinary symbols here.** A problem that was not asked
26
+ for arithmetic reads a :class:`~unicode_logic_kit.fol.nodes.Number` as a CONSTANT
27
+ identified by its value (``1`` and ``1.0`` are one constant) and ``+ - * /`` and ``<
28
+ > ≤ ≥`` as uninterpreted function and predicate symbols (the same definition the
29
+ ``fof`` writer writes, see :mod:`unicode_logic_kit.atp._tptp_problem`). So this writer
30
+ declares a numeral as a constant of ``$i`` (``tff(const_decl_1, type, n1: $i ).``),
31
+ an operator as an uninterpreted function ``$i * $i > $i`` and a comparison as an
32
+ uninterpreted predicate ``$i * $i > $o``, each under a word of the TF0 grammar that the
33
+ ordinary renamer chose and the returned name map records; the numerals are the map's
34
+ ``numerals``, which is how a proof's ``n1`` reads back as ``Number(1)``. The numeral has
35
+ no sort annotation to write, so a numeral that the inference would put into a user sort
36
+ (``∀x:S P(x), P(1)``: the position of ``P``'s argument holds ``S``) is refused as an
37
+ unannotated constant is (:class:`Tf0Refusal`), and the backends' automatic mode writes
38
+ ``fof`` for it. A ``Constant`` spelled like a numeral of the problem (``Number(1)`` next
39
+ to ``Constant('1')``) is refused by name. TPTP's own arithmetic (``$int``, ``$sum``,
40
+ ``$less``, number literals) is the TFA writer's
41
+ (:func:`~unicode_logic_kit.atp._tff_problem.generate_tff_arith_problem`, the ``sort=``
42
+ option of the backends), and is out of scope here.
43
+
44
+ The two builtin TPTP types this module DOES use, ``$i`` (the default
45
+ individual type — never declared, exactly the TPTP standard's own
46
+ convention) and ``$o`` (booleans — only ever appears as a predicate's own
47
+ result type), are never routed through the sort-name sanitiser below; they
48
+ are TPTP reserved dollar-words, lexically disjoint from every
49
+ ``lower_word``-shaped user sort name this module CAN synthesise.
50
+
51
+ Sort/type inference: a union-find over symbol-position "slots"
52
+ -------------------------------------------------------------------
53
+ The same technique :mod:`unicode_logic_kit.fol.casl_export` uses for CASL's
54
+ ``ops``/``preds`` blocks, adapted for TFF: :class:`~unicode_logic_kit.fol.nodes
55
+ .SortedQuantifier` only annotates its OWN bound variable, not every symbol
56
+ position that variable is later passed to, so before any ``tff(...,type,...)``
57
+ declaration can be written, this module infers a single concrete sort for
58
+ every predicate/function ARGUMENT position and every function/constant
59
+ RESULT, by propagating the sort annotations that ARE present
60
+ (:class:`SortedQuantifier`, :class:`~unicode_logic_kit.fol.nodes.SortedConstant`)
61
+ through every place a term is passed as an argument. A plain, unsorted
62
+ :class:`~unicode_logic_kit.fol.nodes.Quantifier`'s variable anchors its slot to
63
+ ``$i`` directly (TPTP's own "no type given" default), which plugs into
64
+ EXACTLY the same union-find machinery as a genuine sort name — a position
65
+ connected to both ``$i`` (from one occurrence) and a real sort ``S`` (from
66
+ another) is a genuine conflict and is refused loudly, precisely because
67
+ mixing an implicitly-``$i`` and an explicitly-sorted use of the same symbol
68
+ position really is unsound. See :mod:`unicode_logic_kit.fol.casl_export`'s
69
+ module docstring for the full union-find account (:class:`_UnionFind`
70
+ below is line-for-line the same algorithm, ``$i`` playing CASL's
71
+ ``default_sort`` role).
72
+
73
+ ``SortedCount`` (∃≥n / ∃≤n / ∃=n x:S φ) is lowered BEFORE inference/rendering
74
+ via :func:`_expand_sorted_count` — the same distinct-witnesses encoding
75
+ :class:`~unicode_logic_kit.fol.nodes.Count._expand` uses, reimplemented here
76
+ with SORT-TYPED witness quantifiers (``SortedQuantifier`` over the SAME sort
77
+ ``S``) instead of Count's plain, unsorted ones, so the exported problem never
78
+ needs a synthetic guard predicate for the witnesses either.
79
+ :class:`~unicode_logic_kit.fol.nodes.SortedCardinality` has no first-order
80
+ export at all (set cardinality is second-order) and is refused by the
81
+ fragment gate, exactly like :class:`~unicode_logic_kit.fol.nodes.Cardinality`
82
+ in the classical route.
83
+
84
+ Free variables are refused, not implicitly closed
85
+ ---------------------------------------------------
86
+ TPTP semantics implicitly universally-close a free variable in an
87
+ axiom/conjecture at type ``$i``. This module refuses a free variable instead
88
+ (mirroring :func:`unicode_logic_kit.fol.casl_export.formula_to_casl`'s own
89
+ refusal, for the same reason): a kit-level formula that reaches this module
90
+ with a genuinely free variable has no way to tell whether that variable was
91
+ MEANT to range over a user sort — silently defaulting it to ``$i`` could
92
+ render a DIFFERENT, unintended typed problem. Every variable rendered here
93
+ comes from an explicit :class:`~unicode_logic_kit.fol.nodes.Quantifier` /
94
+ ``SortedQuantifier`` binder in the kit's own AST.
95
+
96
+ Name legality (ASCII, first-letter-fold, whole-problem-injective)
97
+ -------------------------------------------------------------------
98
+ Mirrors :mod:`atp._tptp_problem`'s sanitisation for predicates/functions/
99
+ constants (ASCII transliteration, every other character outside
100
+ ``[A-Za-z0-9_]`` written as a ``uXXXX`` code-point escape, then fold only the
101
+ first character for TPTP's lower_word rule, with a synthesised token
102
+ (:func:`_tptp_word_base`) when the original name is not already TPTP-legal)
103
+ and extends the SAME treatment to SORT names, which
104
+ share the kit's uppercase-initial :samp:`PREDICATE` lexical convention (see
105
+ ``fol/_identifiers.py``'s ``sort_pattern()``) and therefore need exactly the
106
+ same fold. Predicates, functions+constants (one shared namespace) and sorts
107
+ (a third, independent namespace — TPTP types live in yet another syntactic
108
+ position) are each de-collided separately; two DISTINCT kit-level names in
109
+ the same namespace that would fold to the same TPTP identifier are refused
110
+ with ``NotImplementedError`` naming both, exactly like ``_tptp_problem``'s
111
+ own collision guard. This module deliberately does not depend on
112
+ ``atp._tptp_problem`` at import time (that module imports this one) to avoid
113
+ a circular import between the two — the small renaming primitives are
114
+ duplicated here at the size CASL export's own ``_check_reserved`` duplicates
115
+ ``eval.validate``'s builtin sets (see that module's docstring for the same
116
+ "importing would be backwards" reasoning).
117
+
118
+ A predicate and a function/constant that fold to the SAME identifier
119
+ --------------------------------------------------------------------
120
+ Unlike untyped ``fof``, TF0 has ONE flat table of declared symbols, so a
121
+ predicate ``Agent`` and a role function ``agent`` (both ``agent``) would get
122
+ two ``tff(…, type, agent: …)`` declarations for one name — for the function
123
+ case Vampire and E both refuse the problem (``agent(X0) … is not an instance
124
+ of sort $i`` / ``type error``) before they answer anything, so no verdict
125
+ comes back at all. The writer therefore renames the
126
+ TERM-side symbol (the function or constant; the predicate keeps its natural
127
+ name) to ``<name>_term`` — de-collided with a numeric suffix against every
128
+ identifier the problem already uses — whenever its rendered name equals a
129
+ predicate's, whatever the arities. The rename is exact (a symbol is only a
130
+ name) and is recorded: :func:`generate_tff_problem_with_mapping` returns the
131
+ :class:`~unicode_logic_kit.atp._tptp_problem.TptpNameMap`
132
+ (:func:`~unicode_logic_kit.atp._tptp_problem.apply_reverse_tptp` restores the
133
+ original names).
134
+
135
+ A function/constant that folds to the name of a SORT is separated the same way,
136
+ for a measured reason: Vampire 5.0.1 resolves the term's name to the type
137
+ (``The sort $tType of the intended term argument human … is not an instance of
138
+ sort human``) and refuses the problem, while E accepts it.
139
+
140
+ A SORT and a PREDICATE that fold to the same name are REFUSED, by name. They are
141
+ read correctly by Vampire and E (``human: $tType`` next to ``human: human > $o``),
142
+ but they would not mean what the kit's other routes mean. The kit defines a sort
143
+ as its guard predicate: ``∀x:S φ`` is ``∀x (S(x) → φ)``, with ``∃x S(x)`` for the
144
+ sort's non-emptiness, which is what ``to_z3``, the ``fof`` writer and the Prover9
145
+ writer do, so there ``∃y:Car Car(y)`` is valid (``Car`` is one symbol, and it is
146
+ non-empty). TF0 declares the type ``car`` and an UNRELATED predicate ``car`` over
147
+ it, and ``∃y:Car Car(y)`` is then not a theorem (let ``Car`` hold of nothing): a
148
+ prover answering over TF0 would answer a different question than z3 does, and the
149
+ default backend chain and ``backends=['vampire']`` would disagree on one input.
150
+ TF0 cannot express the guard reading, so the pair is refused, naming both, in the
151
+ same family as the same-kind refusal below; rename one of the two, or write the
152
+ problem with the ``fof`` writer, which reads the sort as that predicate. (The TFA
153
+ writer has no sorts: it refuses a sorted node by name.)
154
+
155
+ A problem needs no conclusion: ``conclusion=None`` writes no ``conjecture`` line,
156
+ for a prover asked whether the premises are satisfiable. Every check and rename
157
+ works on the premises alone.
158
+
159
+ A variable that has no TPTP spelling (``ä`` is written ``Ä``, ``x-1``, ``1x``) is
160
+ renamed to a fresh legal one, per formula, without a record: a variable is bound
161
+ (:func:`unicode_logic_kit.fol._tptp_symbols.legalise_variables`). The nullary atoms
162
+ ``$true`` and ``$false`` are TPTP's own propositions (this kit's TPTP reader
163
+ produces them): they are written verbatim and are neither declared nor renamed.
164
+
165
+ The typed text against the kit's own reading of a sort
166
+ ------------------------------------------------------------
167
+ What a sort IS in this kit: there is ONE universe; a sort ``S`` is a non-empty
168
+ subset of it (the extension of the unary predicate ``S``) and sorts may overlap; a
169
+ constant written ``c:S`` is an element of ``S``, and ``c:S`` here and a plain ``c``
170
+ there are one constant; an unannotated constant, an unsorted variable and the value
171
+ of a function may be ANY element of the universe (there is no way to declare a
172
+ function's result sort); a predicate is a relation over the whole universe. That is
173
+ what ``to_z3``, the ``fof`` writer (with the non-emptiness and the sort membership
174
+ axioms it adds), the Prover9 writer and the finite model finder answer.
175
+
176
+ TF0's types are DISJOINT sets, and the union-find above decides the type of every
177
+ position, so a typed text can ask another question. Two shapes of it are REFUSED
178
+ (:class:`Tf0Refusal`, by name, with what to write instead; :func:`check_typed_reading`
179
+ is the one implementation, which the NXF writer and the Hets backend share):
180
+
181
+ * **An unannotated constant or a function value that the inference puts into a
182
+ sort.** A constant that is written ``c:S`` NOWHERE in the problem, or the value
183
+ of a function, whose position is in one class with a sort ``S`` (an argument
184
+ position that a ``∀x:S`` binds, an equation with a sorted term, ...) would be
185
+ declared ``c: s`` / ``f: ... > s``, which asserts what no formula says. (A
186
+ function ARGUMENT position may be a sort: nothing is asserted about the value of
187
+ the function there. A constant annotated ``c:S`` anywhere is in ``S`` at every
188
+ occurrence.) Measured, Vampire 5.0.1 and E 3.5.1: ``∀x:Human Mortal(x) ⊢
189
+ Mortal(socrates)`` and ``∀x:Foo R(x), Q(g(a)) ⊢ R(g(a))`` were theorems of the
190
+ typed text and are not entailed (take a universe of two elements, the sort and the
191
+ predicate on the first, the constant or ``g(a)`` the second).
192
+ * **An equation over a variable bound by an unsorted quantifier, in a problem that
193
+ has a sort.** An unsorted quantifier ranges over the whole universe, the elements
194
+ of every sort included, so with an equation it bounds the size of the universe;
195
+ in the typed text it ranges over ``$i``, a separate type. ``∀x ∀y x = y ⊢ ∀x:A ∀z:A
196
+ x = z`` is valid (the universe has one element, ``A`` is a subset of it) and is
197
+ not a theorem of the typed text; ``∀x ∀y x = y, ∃x:A ∃z:A ¬(x = z)`` is
198
+ unsatisfiable and the typed text is satisfiable. (An equation between sorted
199
+ variables, or between constants and function values, is harmless. A ``SortedCount``
200
+ is expanded first: its witnesses are sorted variables.)
201
+
202
+ Why these two conditions are enough. Say the typed text has neither. (1) A model of
203
+ the kit's reading in which the premises hold and the conclusion fails gives a model
204
+ of the typed text in which they do too: ``$i`` is the universe, each type a tagged
205
+ copy of its sort, and every term at a position of type ``s`` is a variable bound by
206
+ ``∀x:S`` or a constant annotated ``c:S`` (the others were refused), so it denotes
207
+ an element of ``S``, and the two evaluations agree: a theorem of the typed text is
208
+ valid in the kit. (2) A model of the typed text in which the premises hold and the
209
+ conclusion fails gives a model of the kit's reading: multiply ``$i`` by a set at
210
+ least as large as every type, with constants and function values at the first
211
+ coordinate and the predicates ignoring the second; that changes no formula without
212
+ an equation over an unsorted variable, and every type then embeds into the result,
213
+ which is the universe; a constant or function value that the inference had put into
214
+ a sort is simply the embedded element, which the kit allows for an unannotated
215
+ symbol. So a countermodel of the typed text is one of the kit's reading. Differentially
216
+ (Vampire on the typed text and on the ``fof`` text, ``tests/test_tf0_typed_reading.py``)
217
+ what the writer accepts is answered the same.
218
+
219
+ The two conditions are SUFFICIENT, not necessary: ``∀x:S P(x), c = d:S ⊢ P(c)`` is
220
+ refused (``c`` is written with no sort) although the typed text happens to answer it
221
+ correctly, and such a problem goes to the ``fof`` route, which asks the kit's
222
+ question for every problem. With
223
+ ``tff=None`` the backends fall back to it and say so in the verdict's detail;
224
+ ``tff=True`` reports the refusal (``unknown`` / ``unsupported``) and ``tff=False``
225
+ writes ``fof``. :func:`infer_tff_signature` is signature inference, not a decision
226
+ route, and keeps inferring.
227
+
228
+ The asymmetry with the same-namespace rule above is deliberate for this
229
+ release. Two LEGAL names of ONE kind that fold together (``Foo``/``foo``) are
230
+ still refused by name rather than renamed: that refusal predates the name map
231
+ and is kept so that no existing caller silently receives a renamed symbol it
232
+ did not ask for. A name that is not TPTP-legal at all, or that clashes across
233
+ kinds, has always needed a rewrite to be expressible, so it is renamed and
234
+ recorded.
235
+ """
236
+
237
+ import re
238
+ from dataclasses import dataclass, field
239
+ from typing import (
240
+ TYPE_CHECKING, Callable, Dict, FrozenSet, Iterable, List, Optional, Sequence, Set, Tuple,
241
+ Union,
242
+ )
243
+
244
+ from ..fol.nodes import (
245
+ Node, Variable, Constant, SortedConstant, Function,
246
+ Atom, Not, And, Or, Xor, Implies, Iff, Quantifier, SortedQuantifier,
247
+ SortedCount, free_variables, substitute,
248
+ )
249
+ from ..fol._fol_nodes import constant_name_to_ascii, tptp_fold_first_letter
250
+ from ..fol._numeral_symbols import numerals_as_constants
251
+ from ..fol._tptp_symbols import check_variable_names, is_tptp_boolean_atom, legalise_variables
252
+ from ..fol.signature import Signature, PredicateDecl, FunctionDecl, ConstantDecl
253
+ from ._ascii_names import ascii_safe_base, reserve_rendered
254
+ from ._writer_support import (
255
+ check_against_generated, normalise_premise_names, refusals_speak_as, tptp_name_token,
256
+ )
257
+
258
+ if TYPE_CHECKING:
259
+ from ._tptp_problem import TptpNameMap
260
+
261
+ __all__ = [
262
+ "generate_tff_problem", "generate_tff_problem_with_mapping",
263
+ "formula_to_tff", "infer_tff_signature", "check_typed_reading",
264
+ "unsorted_equality_refusal", "Tf0Refusal",
265
+ "problem_needs_tff", "TFF_INDIVIDUAL_SORT", "TFF_BOOLEAN_SORT",
266
+ ]
267
+
268
+ _SORTED_NODE_NAMES = frozenset({"SortedQuantifier", "SortedConstant", "SortedCount"})
269
+
270
+
271
+ class Tf0Refusal(ValueError, NotImplementedError):
272
+ """The typed (TF0) writer will not write this problem.
273
+
274
+ Raised when the typed text would not ask the question the kit's other routes
275
+ ask (an unannotated constant or a function value that the inference would put
276
+ into a sort, an equation over an unsorted variable next to a sort, one position
277
+ holding two sorts, a sort that is also a predicate), or when it cannot be written
278
+ at all in one table of typed symbols (a free variable, a predicate or function at
279
+ two arities, a name that is both a constant and a function). The message names the
280
+ term, the sort and the reason, and says what to write instead. The fof writer
281
+ (:func:`~unicode_logic_kit.atp._tptp_problem.generate_tptp_problem_with_mapping`)
282
+ asks the kit's question for every one of these problems; the backends'
283
+ automatic mode (``tff=None``) falls back to it when this is raised.
284
+
285
+ It derives from both :class:`ValueError` and :class:`NotImplementedError`, so
286
+ that code which caught either of the two before this class existed (the sort
287
+ conflict was a ``ValueError``, the sort and predicate of one name a
288
+ ``NotImplementedError``) keeps working.
289
+
290
+ Fields:
291
+
292
+ * ``reason`` -- a short machine-readable word: ``"sort_conflict"``,
293
+ ``"sort_predicate_name"``, ``"unsorted_term_in_sort"``,
294
+ ``"unsorted_equality"``, ``"free_variable"``, ``"arity_conflict"``,
295
+ ``"constant_function_clash"`` or ``"premise_name"`` (a premise named like one of
296
+ the lines the writer writes itself).
297
+ """
298
+
299
+ def __init__(self, message: str, reason: str = "") -> None:
300
+ super().__init__(message)
301
+ self.reason = reason
302
+
303
+
304
+ def problem_needs_tff(premises: Sequence[Node], conclusion: Optional[Node] = None) -> bool:
305
+ """Whether any of ``premises`` + ``conclusion`` contains a
306
+ :class:`~unicode_logic_kit.fol.nodes.SortedQuantifier` /
307
+ :class:`~unicode_logic_kit.fol.nodes.SortedConstant` /
308
+ :class:`~unicode_logic_kit.fol.nodes.SortedCount` node.
309
+
310
+ The auto-select signal the TPTP-family backends
311
+ (:mod:`atp.vampire_entailment`, :mod:`atp.eprover_backend`) use to try
312
+ this module's native typed export before the classical guard-predicate
313
+ ``fof`` route (:mod:`atp._tptp_problem`) — see each backend's ``tff=``
314
+ option. It says that the problem is worth TRYING as TF0, not that TF0 will
315
+ take it: the typed writer refuses (:class:`Tf0Refusal`) the problems whose
316
+ typed text would not ask the kit's question, and the automatic mode then
317
+ writes the ``fof`` problem. A batch with no sorted node at all is already
318
+ fully served by the classical route (a plain ``Quantifier`` needs no typing
319
+ to export soundly), so the default stays ``fof`` there.
320
+ """
321
+ for f in list(premises) + ([] if conclusion is None else [conclusion]):
322
+ for n in f.walk():
323
+ if type(n).__name__ in _SORTED_NODE_NAMES:
324
+ return True
325
+ return False
326
+
327
+ #: TPTP's built-in "individual" type — the implicit type of an untyped
328
+ #: quantified variable. Never declared with its own ``tff(...,type,...)``
329
+ #: statement (it is a TPTP reserved word), and never routed through the
330
+ #: sort-name sanitiser (it is not a kit-level sort name at all).
331
+ TFF_INDIVIDUAL_SORT = "$i"
332
+
333
+ #: TPTP's built-in boolean type — only ever a PREDICATE's own result type.
334
+ TFF_BOOLEAN_SORT = "$o"
335
+
336
+ _ARITHMETIC_PREDS = frozenset({"<", ">", "≤", "≥"})
337
+ _ARITHMETIC_FUNCS = frozenset({"+", "-", "*", "/"})
338
+
339
+ _ARITH_OUT_OF_SCOPE = (
340
+ "generate_tff_problem: {kind} {name!r} is an arithmetic symbol, and this entry point "
341
+ "does not read it as an ordinary one: reading it as arithmetic needs TPTP's arithmetic "
342
+ "sorts ($int/$rat/$real), which are out of scope for a TF0-only exporter (see module "
343
+ "docstring 'Scope: TF0 only'). The problem writers generate_tff_problem and "
344
+ "generate_tptp_problem_with_mapping write it as an ordinary uninterpreted symbol, which "
345
+ "is what this kit means by it unless arithmetic is asked for by name "
346
+ "(generate_tff_arith_problem, sort='int')."
347
+ )
348
+
349
+
350
+ # =============================================================================
351
+ # Fragment gate
352
+ # =============================================================================
353
+
354
+ # The classical MSFOL node set this exporter understands PLUS SortedCount
355
+ # (lowered away by _expand_all_sorted_counts before inference/rendering ever
356
+ # see it) — exactly unicode_logic_kit.fol.casl_export's _ALLOWED_CASL_NODES
357
+ # with SortedCount added; see the module docstring's "Scope" section for why
358
+ # every other node class (Cardinality/SortedCardinality, modal/temporal,
359
+ # second-order, lambda, Łukasiewicz, Number, ...) is refused by name rather
360
+ # than silently narrowed.
361
+ _ALLOWED_TFF_NODES = frozenset({
362
+ "Variable", "Constant", "SortedConstant", "Function",
363
+ "Atom", "Not", "And", "Or", "Xor", "Implies", "Iff",
364
+ "Quantifier", "SortedQuantifier", "SortedCount",
365
+ })
366
+
367
+
368
+ def _check_fragment(formula: Node) -> None:
369
+ """Walk ``formula`` and refuse (loudly, by name) the first node outside
370
+ the TF0-expressible fragment this exporter covers.
371
+
372
+ A bespoke recursion rather than ``Node.walk()``: ``SortedCount.n`` is a
373
+ ``Number`` child (the symbolic counting bound, e.g. the ``2`` in
374
+ ``∃≥2 x:S φ``) but is not itself a FORMULA or TERM position — it is
375
+ never rendered as a TFF expression, only read as a Python int by
376
+ :func:`_expand_sorted_count` — so it must NOT trip the "a numeric
377
+ literal needs an arithmetic sort" refusal below the way an actual
378
+ ``Number`` TERM occurrence (e.g. inside an Atom's arguments) should.
379
+ """
380
+ def visit(node: Node) -> None:
381
+ cls = type(node).__name__
382
+ if getattr(node, "sort", None) == TFF_INDIVIDUAL_SORT:
383
+ raise NotImplementedError(
384
+ f"generate_tff_problem: a sort named {TFF_INDIVIDUAL_SORT!r} cannot be "
385
+ f"written in TF0: {TFF_INDIVIDUAL_SORT} is TPTP's built-in type of "
386
+ f"individuals, the type this writer gives every UNSORTED term, so a "
387
+ f"user sort of that name would be merged with everything that has no "
388
+ f"sort (∀x:$i R(x) would say ∀x R(x)). Rename the sort.")
389
+ if cls not in _ALLOWED_TFF_NODES:
390
+ if cls == "Number":
391
+ raise NotImplementedError(
392
+ "generate_tff_problem: this entry point does not read a numeric "
393
+ "literal as an ordinary constant. Read as arithmetic it would need "
394
+ "TPTP's arithmetic sorts ($int/$rat/$real), which are out of scope "
395
+ "for this TF0-only exporter (see module docstring 'Scope: TF0 "
396
+ "only'). The problem writers generate_tff_problem and "
397
+ "generate_tptp_problem_with_mapping write a numeral as a constant "
398
+ "of $i, which is what this kit means by it unless arithmetic is "
399
+ "asked for by name (generate_tff_arith_problem, sort='int')."
400
+ )
401
+ raise NotImplementedError(
402
+ f"generate_tff_problem: {cls!r} is outside the classical "
403
+ "many-sorted FOL fragment this native TF0 exporter covers "
404
+ "(only Atom, Not, And, Or, Xor, Implies, Iff, Quantifier, "
405
+ "SortedQuantifier, SortedCount, and the term classes "
406
+ "Variable, Constant, SortedConstant, Function are "
407
+ "TF0-expressible here). Modal/temporal/epistemic operators, "
408
+ "second-order quantification, SortedCardinality (set "
409
+ "cardinality has no first-order counterpart), and lambda "
410
+ "terms are all out of scope for this TF0-only exporter."
411
+ )
412
+ if cls == "SortedCount":
413
+ visit(node.variable)
414
+ visit(node.formula)
415
+ return
416
+ for child in node._child_nodes():
417
+ visit(child)
418
+
419
+ visit(formula)
420
+
421
+
422
+ # =============================================================================
423
+ # SortedCount lowering: typed distinct-witnesses encoding
424
+ # =============================================================================
425
+
426
+ _COUNT_EXPAND_MAX = 500
427
+ _COUNT_TOO_LARGE = (
428
+ "generate_tff_problem: SortedCount n={n} is too large to expand to "
429
+ "typed first-order TFF (the distinct-witnesses encoding materialises "
430
+ f"O(n^2) constraints under n nested quantifiers; the limit is n<={_COUNT_EXPAND_MAX})."
431
+ )
432
+
433
+
434
+ def _balanced_and(parts: List[Node]) -> Node:
435
+ """Fold a non-empty list of formulas into a balanced (shallow) And tree —
436
+ mirrors ``fol._fol_nodes._balanced_and``, duplicated here (a private
437
+ helper of a module this one does not otherwise depend on) so an
438
+ O(n^2)-conjunct count expansion stays only O(log n) deep."""
439
+ while len(parts) > 1:
440
+ merged: List[Node] = [And(parts[i], parts[i + 1]) for i in range(0, len(parts) - 1, 2)]
441
+ if len(parts) % 2:
442
+ merged.append(parts[-1])
443
+ parts = merged
444
+ return parts[0]
445
+
446
+
447
+ def _expand_sorted_count(node: SortedCount) -> Node:
448
+ """Lower one ``SortedCount`` to plain SortedQuantifier/And/Or/Not/Atom,
449
+ via the standard distinct-witnesses counting encoding, SORT-TYPED: every
450
+ witness variable is bound by a ``SortedQuantifier`` over ``node.sort``,
451
+ not a plain (guard-predicate-needing) ``Quantifier``.
452
+
453
+ ``∃≥m x:S φ`` becomes ``∃x_0:S … ∃x_{m-1}:S (⋀ φ[x_i] ∧ ⋀_{i<j} x_i ≠ x_j)``;
454
+ ``∃≤n`` is ``¬(∃≥n+1)``; ``∃=n`` is ``∃≥n ∧ ¬(∃≥n+1)`` — the exact reading
455
+ :meth:`~unicode_logic_kit.fol.nodes.Count._expand` uses, reimplemented here
456
+ (rather than delegating to ``SortedCount._relativize``/``Count._expand``)
457
+ because those guard the matrix with a predicate atom instead of typing
458
+ the witness quantifier, which is exactly the guard-atom encoding this
459
+ NATIVE TFF exporter exists to avoid.
460
+ """
461
+ if node.n.value > _COUNT_EXPAND_MAX:
462
+ raise NotImplementedError(_COUNT_TOO_LARGE.format(n=node.n.value))
463
+ var, sort, phi = node.variable, node.sort, node.formula
464
+ avoid = {v.name for v in free_variables(phi)} | {var.name}
465
+
466
+ def fresh(k: int) -> List[Variable]:
467
+ out: List[Variable] = []
468
+ i = 0
469
+ while len(out) < k:
470
+ cand = f"{var.name}_{i}"
471
+ if cand not in avoid:
472
+ out.append(Variable(cand))
473
+ avoid.add(cand)
474
+ i += 1
475
+ return out
476
+
477
+ def at_least(m: int) -> Node:
478
+ if m <= 0:
479
+ w = fresh(1)[0]
480
+ g = substitute(phi, var, w)
481
+ return SortedQuantifier("∃", w, sort, Or(g, Not(g)))
482
+ ws = fresh(m)
483
+ conjuncts = [substitute(phi, var, w) for w in ws]
484
+ conjuncts += [Atom("≠", (ws[i], ws[j]))
485
+ for i in range(m) for j in range(i + 1, m)]
486
+ body = _balanced_and(conjuncts)
487
+ for w in reversed(ws):
488
+ body = SortedQuantifier("∃", w, sort, body)
489
+ return body
490
+
491
+ if node.op == "ge":
492
+ return at_least(node.n.value)
493
+ if node.op == "le":
494
+ return Not(at_least(node.n.value + 1))
495
+ return And(at_least(node.n.value), Not(at_least(node.n.value + 1)))
496
+
497
+
498
+ def _expand_all_sorted_counts(node: Node) -> Node:
499
+ """Bottom-up rewrite: every ``SortedCount`` (including one nested inside
500
+ another's matrix) is replaced by :func:`_expand_sorted_count`'s typed
501
+ encoding. After this pass the tree is guaranteed to use only the
502
+ fragment :mod:`unicode_logic_kit.fol.casl_export` also accepts (plus
503
+ ``SortedQuantifier``/``SortedConstant``), since ``_check_fragment`` has
504
+ already run on the (pre-expansion) input."""
505
+ node = node.map_children(_expand_all_sorted_counts)
506
+ if isinstance(node, SortedCount):
507
+ return _expand_sorted_count(node)
508
+ return node
509
+
510
+
511
+ # =============================================================================
512
+ # Sort inference: union-find over slots and concrete sort names
513
+ # =============================================================================
514
+ #
515
+ # Line-for-line the same algorithm as unicode_logic_kit.fol.casl_export's
516
+ # _UnionFind — see that module's docstring for the full account. $i plays
517
+ # CASL's default_sort role: it is unioned in directly wherever a plain
518
+ # (unsorted) Quantifier's variable is used, so a position that is EVER used
519
+ # both implicitly-$i and explicitly-sorted is a genuine, correctly-refused
520
+ # conflict rather than two different silently-coexisting readings.
521
+
522
+ _Slot = Union[Tuple[str, str, int], Tuple[str, str], str]
523
+
524
+
525
+ class _UnionFind:
526
+ """Weighted union-find over sort-inference slots and concrete sort names.
527
+
528
+ Invariant: whenever a class contains a concrete sort name (a plain str
529
+ member, including ``$i``), that string is ALWAYS the class's root.
530
+ """
531
+
532
+ def __init__(self) -> None:
533
+ self._parent: Dict[_Slot, _Slot] = {}
534
+ self._rank: Dict[_Slot, int] = {}
535
+
536
+ def find(self, x: _Slot) -> _Slot:
537
+ self._parent.setdefault(x, x)
538
+ path = []
539
+ while self._parent[x] != x:
540
+ path.append(x)
541
+ x = self._parent[x]
542
+ for n in path:
543
+ self._parent[n] = x
544
+ return x
545
+
546
+ def union(self, a: _Slot, b: _Slot, context: str) -> None:
547
+ ra, rb = self.find(a), self.find(b)
548
+ if ra == rb:
549
+ return
550
+ a_concrete, b_concrete = isinstance(ra, str), isinstance(rb, str)
551
+ if a_concrete and b_concrete:
552
+ raise Tf0Refusal(
553
+ f"generate_tff_problem: sort conflict while resolving "
554
+ f"{context} — inferred both {ra!r} and {rb!r}. TF0's types are "
555
+ f"disjoint sets, so one position cannot hold two of them; in this "
556
+ f"kit a sort is a subset of ONE universe, sorts may overlap, and an "
557
+ f"unsorted variable or term ranges over the whole universe, sorted "
558
+ f"elements included. The other routes ask that question and this "
559
+ f"text cannot: write the problem with the fof writer "
560
+ f"(generate_tptp_problem_with_mapping; tff=False in the backends, "
561
+ f"which tff=None falls back to).",
562
+ reason="sort_conflict",
563
+ )
564
+ if a_concrete:
565
+ self._parent[rb] = ra
566
+ elif b_concrete:
567
+ self._parent[ra] = rb
568
+ else:
569
+ rank_a, rank_b = self._rank.get(ra, 0), self._rank.get(rb, 0)
570
+ if rank_a < rank_b:
571
+ ra, rb = rb, ra
572
+ self._parent[rb] = ra
573
+ if rank_a == rank_b:
574
+ self._rank[ra] = rank_a + 1
575
+
576
+ def sort_of(self, slot: _Slot) -> str:
577
+ """Return the concrete sort resolved for ``slot``, defaulting to
578
+ :data:`TFF_INDIVIDUAL_SORT` if that slot's class has no concrete
579
+ member at all (a symbol position never connected to any sort
580
+ annotation — TPTP's own "no type given" reading)."""
581
+ root = self.find(slot)
582
+ return root if isinstance(root, str) else TFF_INDIVIDUAL_SORT
583
+
584
+
585
+ @dataclass
586
+ class _Signature:
587
+ """Mutable accumulator built by one :func:`_analyze` pass — mirrors
588
+ ``casl_export._Signature`` minus ``default_sort`` (fixed to ``$i``).
589
+
590
+ ``sorted_constants`` are the constants written ``c:S`` somewhere in the
591
+ problem (``c`` is ONE symbol wherever it occurs, so a plain occurrence of
592
+ it is covered too); ``unsorted_equality`` is the first equality or
593
+ disequality atom that has a bare variable bound by an unsorted quantifier
594
+ as one of its sides. :func:`_check_reading` reads both.
595
+
596
+ ``uninterpreted_arithmetic`` says that ``+ - * /`` and ``< > ≤ ≥`` are ordinary
597
+ symbols of the problem (the problem writers) and not a refused construct (signature
598
+ inference and the typed-reading check)."""
599
+
600
+ uf: _UnionFind = field(default_factory=_UnionFind)
601
+ pred_arity: Dict[str, int] = field(default_factory=dict)
602
+ func_arity: Dict[str, int] = field(default_factory=dict)
603
+ const_names: Set[str] = field(default_factory=set)
604
+ func_names: Set[str] = field(default_factory=set)
605
+ literal_sorts: Set[str] = field(default_factory=set)
606
+ sorted_constants: Set[str] = field(default_factory=set)
607
+ unsorted_equality: Optional[Node] = None
608
+ uninterpreted_arithmetic: bool = False
609
+
610
+
611
+ _CONST_VS_FUNCTION = (
612
+ "generate_tff_problem: {name!r} is used both as a constant and as a "
613
+ "function — this exporter declares at most one 'tff(...,type,...)' "
614
+ "entry per name, so the two uses cannot be reconciled into one symbol. "
615
+ "Rename one of the two, or write the problem with the fof writer "
616
+ "(tff=False in the backends), which writes them as two symbols."
617
+ )
618
+
619
+
620
+ def _infer_term(node: Node, env: Dict[str, str], sig: _Signature) -> _Slot:
621
+ """Process a term node: register its symbol, recurse into sub-terms,
622
+ return the union-find slot representing its sort. Mirrors
623
+ ``casl_export._infer_term``."""
624
+ cls = type(node).__name__
625
+
626
+ if cls == "Variable":
627
+ if node.name not in env:
628
+ raise Tf0Refusal(
629
+ f"generate_tff_problem: free variable '{node.name}' — every "
630
+ "variable must be bound by an enclosing Quantifier or "
631
+ "SortedQuantifier; TPTP's implicit top-level closure would "
632
+ "silently default an unbound variable to $i, which this "
633
+ "exporter refuses to guess (see module docstring). Bind the "
634
+ "variable with a quantifier; the fof writer refuses a free "
635
+ "variable too, so every route answers the same.",
636
+ reason="free_variable",
637
+ )
638
+ return env[node.name]
639
+
640
+ if cls in ("Constant", "SortedConstant"):
641
+ name = node.name
642
+ if name in sig.func_names:
643
+ raise Tf0Refusal(_CONST_VS_FUNCTION.format(name=name),
644
+ reason="constant_function_clash")
645
+ sig.const_names.add(name)
646
+ slot: _Slot = ("const", name)
647
+ if cls == "SortedConstant":
648
+ sig.literal_sorts.add(node.sort)
649
+ sig.sorted_constants.add(name)
650
+ sig.uf.union(slot, node.sort, f"constant '{name}' sort annotation")
651
+ return slot
652
+
653
+ if cls == "Function":
654
+ name = node.name
655
+ if name in _ARITHMETIC_FUNCS and not sig.uninterpreted_arithmetic:
656
+ raise NotImplementedError(_ARITH_OUT_OF_SCOPE.format(kind="function", name=name))
657
+ if name in sig.const_names:
658
+ raise Tf0Refusal(_CONST_VS_FUNCTION.format(name=name),
659
+ reason="constant_function_clash")
660
+ arity = len(node.args)
661
+ prev = sig.func_arity.get(name)
662
+ if prev is not None and prev != arity:
663
+ raise Tf0Refusal(
664
+ f"generate_tff_problem: function '{name}' used with "
665
+ f"conflicting arities {prev} and {arity}. This exporter "
666
+ f"declares one type per name; write the problem with the fof "
667
+ f"writer (tff=False in the backends), which writes the two "
668
+ f"arities as two symbols, or rename one of them.",
669
+ reason="arity_conflict",
670
+ )
671
+ sig.func_arity[name] = arity
672
+ sig.func_names.add(name)
673
+ for i, a in enumerate(node.args):
674
+ arg_slot = _infer_term(a, env, sig)
675
+ sig.uf.union(("func", name, i), arg_slot,
676
+ f"function '{name}' argument {i + 1}")
677
+ return ("func_result", name)
678
+
679
+ raise AssertionError(
680
+ f"generate_tff_problem: unreachable — term node {cls!r} passed the "
681
+ "fragment check but is not handled by _infer_term."
682
+ )
683
+
684
+
685
+ def _infer_formula(node: Node, env: Dict[str, str], sig: _Signature) -> None:
686
+ """Walk a formula node, threading the bound-variable environment,
687
+ registering every predicate/connective/quantifier constraint. Mirrors
688
+ ``casl_export._infer_formula``."""
689
+ cls = type(node).__name__
690
+
691
+ if cls == "Atom":
692
+ if is_tptp_boolean_atom(node): # $true / $false: TPTP's own, declared by no one
693
+ return
694
+ if node.predicate in _ARITHMETIC_PREDS and not sig.uninterpreted_arithmetic:
695
+ raise NotImplementedError(
696
+ _ARITH_OUT_OF_SCOPE.format(kind="predicate", name=node.predicate))
697
+ if node.predicate in ("=", "≠"):
698
+ if len(node.args) != 2:
699
+ raise ValueError(
700
+ "generate_tff_problem: an equality/disequality atom "
701
+ f"must have exactly 2 arguments, got {len(node.args)}."
702
+ )
703
+ s0 = _infer_term(node.args[0], env, sig)
704
+ s1 = _infer_term(node.args[1], env, sig)
705
+ sig.uf.union(s0, s1, "an equality/disequality atom")
706
+ if sig.unsorted_equality is None and any(
707
+ isinstance(a, Variable) and env[a.name] == TFF_INDIVIDUAL_SORT
708
+ for a in node.args):
709
+ sig.unsorted_equality = node
710
+ return
711
+ arity = len(node.args)
712
+ prev = sig.pred_arity.get(node.predicate)
713
+ if prev is not None and prev != arity:
714
+ raise Tf0Refusal(
715
+ f"generate_tff_problem: predicate '{node.predicate}' used "
716
+ f"with conflicting arities {prev} and {arity}. This exporter "
717
+ f"declares one type per name; write the problem with the fof "
718
+ f"writer (tff=False in the backends), which writes the two "
719
+ f"arities as two symbols, or rename one of them.",
720
+ reason="arity_conflict",
721
+ )
722
+ sig.pred_arity[node.predicate] = arity
723
+ for i, a in enumerate(node.args):
724
+ arg_slot = _infer_term(a, env, sig)
725
+ sig.uf.union(("pred", node.predicate, i), arg_slot,
726
+ f"predicate '{node.predicate}' argument {i + 1}")
727
+ return
728
+
729
+ if cls == "Not":
730
+ _infer_formula(node.formula, env, sig)
731
+ return
732
+
733
+ if cls in ("And", "Or", "Xor", "Implies", "Iff"):
734
+ _infer_formula(node.left, env, sig)
735
+ _infer_formula(node.right, env, sig)
736
+ return
737
+
738
+ if cls == "Quantifier":
739
+ new_env = dict(env)
740
+ new_env[node.variable.name] = TFF_INDIVIDUAL_SORT
741
+ _infer_formula(node.formula, new_env, sig)
742
+ return
743
+
744
+ if cls == "SortedQuantifier":
745
+ sig.literal_sorts.add(node.sort)
746
+ new_env = dict(env)
747
+ new_env[node.variable.name] = node.sort
748
+ _infer_formula(node.formula, new_env, sig)
749
+ return
750
+
751
+ raise AssertionError(
752
+ f"generate_tff_problem: unreachable — formula node {cls!r} passed "
753
+ "the fragment check but is not handled by _infer_formula."
754
+ )
755
+
756
+
757
+ def _analyze(formulas: Sequence[Node], *, uninterpreted_arithmetic: bool = False) -> _Signature:
758
+ """Run sort inference over a batch of already-fragment-checked,
759
+ already-SortedCount-expanded formulas that share one TFF signature.
760
+
761
+ With ``uninterpreted_arithmetic`` the operators and comparisons are symbols like
762
+ any other (the problem writers); without it they are refused by name."""
763
+ sig = _Signature(uninterpreted_arithmetic=uninterpreted_arithmetic)
764
+ for f in formulas:
765
+ _infer_formula(f, {}, sig)
766
+ return sig
767
+
768
+
769
+ # =============================================================================
770
+ # The typed reading against the definition
771
+ # =============================================================================
772
+ #
773
+ # The inference above is a decision about TYPES, not about the kit's model
774
+ # theory. TF0's types are disjoint sets and the inference puts a symbol position
775
+ # into a sort whenever it shares a class with one, while the kit has ONE universe,
776
+ # sorts that are subsets of it, and terms that are in a sort only when a formula
777
+ # says so. Two situations make the typed text ask another question, and
778
+ # :func:`_check_reading` refuses both (the argument is in the module docstring):
779
+ #
780
+ # * a constant that is written ``c:S`` NOWHERE, or a function value, whose position
781
+ # is typed to a user sort: the typed text declares ``c: s`` / ``f: ... > s``, which
782
+ # asserts what no formula says (and a theorem of the typed text may then be no
783
+ # theorem);
784
+ # * an equation with a bare variable bound by an UNSORTED quantifier, in a problem
785
+ # that has a sort: the unsorted variable ranges over ``$i``, a type of its own,
786
+ # instead of over the universe the sort is a subset of (and a countermodel of the
787
+ # typed text may then be no countermodel).
788
+
789
+ def _first_atom_text(formulas: Sequence[Node], matches: Callable[[Node], bool]) -> str:
790
+ """The first atom, in problem order, that has a sub-term for which ``matches``
791
+ holds, as text -- where a message points the reader at; ``""`` if there is none."""
792
+ for formula in formulas:
793
+ for node in formula.walk():
794
+ if isinstance(node, Atom) and any(
795
+ matches(term) for arg in node.args for term in arg.walk()):
796
+ return node.to_unicode_str()
797
+ return ""
798
+
799
+
800
+ _FOF_ADVICE = ("write the problem with the fof writer (generate_tptp_problem_with_mapping; "
801
+ "tff=False in the backends, which tff=None falls back to), which does not "
802
+ "type it")
803
+
804
+
805
+ def unsorted_equality_refusal(writer: str, equation: str, sorts: Iterable[str],
806
+ unsorted_type: str = TFF_INDIVIDUAL_SORT) -> Tf0Refusal:
807
+ """The refusal for an equation over a variable bound by an unsorted quantifier
808
+ in a problem that has a sort, as an exception for the caller to raise: the typed
809
+ writers (TF0, NXF, and CASL through :func:`check_typed_reading`) share its text.
810
+
811
+ ``equation`` is the atom as text, ``sorts`` the sorts of the problem and
812
+ ``unsorted_type`` the type an unsorted variable has in the text being refused.
813
+ The condition itself is syntactic: a user sort occurs, and an equality or
814
+ disequality atom has a bare variable bound by an unsorted quantifier as one of
815
+ its sides (after a ``SortedCount`` is expanded: its witnesses are sorted).
816
+ """
817
+ names = sorted(sorts)
818
+ first = names[0]
819
+ return Tf0Refusal(
820
+ f"{writer}: the equation {equation} has a variable bound by an UNSORTED "
821
+ f"quantifier, and the problem also uses the sort "
822
+ f"{', '.join(repr(s) for s in names)}. In this kit an unsorted quantifier ranges "
823
+ f"over the whole universe, sorted elements included, so an equation over it "
824
+ f"bounds how many elements there are, and a sort is a subset of that universe; "
825
+ f"in the typed text the unsorted variable ranges over {unsorted_type}, a type "
826
+ f"of its own that is disjoint from {first}, so the same equation says nothing "
827
+ f"about the elements of {first}. For example ∀x ∀y x = y ⊢ ∀x:{first} "
828
+ f"∀z:{first} x = z is valid in this kit and is not a theorem of the typed text. "
829
+ f"To ask the kit's question, {_FOF_ADVICE}; or quantify the variable with a sort "
830
+ f"(∀x:{first} ...) if it is meant to range over one.",
831
+ reason="unsorted_equality")
832
+
833
+
834
+ def _check_reading(sig: _Signature, formulas: Sequence[Node], *, writer: str,
835
+ unsorted_type: str = TFF_INDIVIDUAL_SORT,
836
+ numerals: FrozenSet[str] = frozenset()) -> None:
837
+ """Refuse (:class:`Tf0Refusal`, by name) a problem whose typed text would not
838
+ ask the kit's question. ``sig`` is :func:`_analyze`'s result over ``formulas``
839
+ (already expanded); ``writer`` names the caller in the message and
840
+ ``unsorted_type`` the type an unsorted variable gets in the text (``$i`` in
841
+ TF0). ``numerals`` are the names of the constants that stand for numerals: a
842
+ numeral that is typed into a sort is refused like an unannotated constant, in
843
+ words that fit a numeral (it cannot be written with a sort).
844
+
845
+ Checked in a fixed order (function values, then constants, each by name, then
846
+ the equation), so that the refusal does not depend on the order of the
847
+ formulas.
848
+ """
849
+ for name in sorted(sig.func_names):
850
+ sort = sig.uf.sort_of(("func_result", name))
851
+ if sort == TFF_INDIVIDUAL_SORT:
852
+ continue
853
+
854
+ def is_that_function(t: Node, name: str = name) -> bool:
855
+ return isinstance(t, Function) and t.name == name
856
+
857
+ use = _first_atom_text(formulas, is_that_function)
858
+ raise Tf0Refusal(
859
+ f"{writer}: the value of the function {name!r} would be typed as the sort "
860
+ f"{sort!r}, because it is used at a position that also holds terms of that "
861
+ f"sort ({use}). In this kit a function value may be any element of the "
862
+ f"universe: a function has no declared result sort, and nothing in the "
863
+ f"problem says that {name}(...) is a {sort}. The typed text would assume it, "
864
+ f"so a prover would answer a stronger question than the other routes ask. "
865
+ f"To ask the kit's question, {_FOF_ADVICE}; if the function is meant to "
866
+ f"return {sort}s, say so there as a premise with the sort written as a "
867
+ f"predicate ({sort}({name}(...))).",
868
+ reason="unsorted_term_in_sort")
869
+ for name in sorted(sig.const_names - sig.sorted_constants):
870
+ sort = sig.uf.sort_of(("const", name))
871
+ if sort == TFF_INDIVIDUAL_SORT:
872
+ continue
873
+
874
+ def is_that_constant(t: Node, name: str = name) -> bool:
875
+ return isinstance(t, Constant) and t.name == name
876
+
877
+ use = _first_atom_text(formulas, is_that_constant)
878
+ if name in numerals:
879
+ raise Tf0Refusal(
880
+ f"{writer}: the numeral {name} would be typed as the sort {sort!r}, because "
881
+ f"it is used at a position that also holds terms of that sort ({use}). In "
882
+ f"this kit a numeral is a constant that may be any element of the universe, "
883
+ f"and nothing says that it is a {sort}; a numeral cannot be written with a "
884
+ f"sort ({name}:{sort}), and the typed declaration would assume it, so a "
885
+ f"prover would answer a stronger question than the other routes ask. To ask "
886
+ f"the kit's question, {_FOF_ADVICE}.",
887
+ reason="unsorted_term_in_sort")
888
+ raise Tf0Refusal(
889
+ f"{writer}: the constant {name!r} is written with a sort nowhere in this "
890
+ f"problem (there is no {name}:{sort}), but the typed text would declare it "
891
+ f"in the sort {sort!r}, because it is used at a position that also holds "
892
+ f"terms of that sort ({use}). In this kit an unannotated constant may be "
893
+ f"any element of the universe, and nothing here says that it is a {sort}; "
894
+ f"the typed declaration would assume it, so a prover would answer a "
895
+ f"stronger question than the other routes ask. Write the constant with its "
896
+ f"sort ({name}:{sort}) if it is meant to be one; otherwise {_FOF_ADVICE}.",
897
+ reason="unsorted_term_in_sort")
898
+ if sig.unsorted_equality is not None and sig.literal_sorts:
899
+ raise unsorted_equality_refusal(
900
+ writer, sig.unsorted_equality.to_unicode_str(), sig.literal_sorts,
901
+ unsorted_type)
902
+
903
+
904
+ def check_typed_reading(formulas: Sequence[Node], *, writer: str = "check_typed_reading",
905
+ unsorted_type: str = TFF_INDIVIDUAL_SORT) -> None:
906
+ """Refuse a problem for which a TYPED reading would differ from the kit's.
907
+
908
+ The kit has one universe, a sort is a non-empty subset of it, a constant
909
+ written ``c:S`` is in ``S`` and an unannotated constant, an unsorted variable
910
+ and a function value are any element. A typed language (TF0, CASL) has disjoint
911
+ types and the writers infer the type of each symbol position, which gives a
912
+ problem a different meaning when (a) an unannotated constant or a function value
913
+ is put into a sort by the inference, (b) an equation over a variable bound by an
914
+ unsorted quantifier meets a sort, (c) one position would hold two sorts, or
915
+ (d) a sort and a predicate share a name. This function runs the same inference
916
+ as :func:`generate_tff_problem` over ``formulas`` (premises and conclusion
917
+ together) and raises :class:`Tf0Refusal` for each of them. It is the check the
918
+ TF0 writer, the NXF writer and :class:`~unicode_logic_kit.atp.hets_backend.HetsBackend`
919
+ share; the last uses it because CASL's inference is the same algorithm with
920
+ ``Thing`` as the type of an unsorted variable.
921
+
922
+ ``writer`` is the name that opens the message, ``unsorted_type`` the type an
923
+ unsorted variable has in the text being guarded.
924
+
925
+ Raises:
926
+ Tf0Refusal: for each case above, and for a free variable, a symbol at two
927
+ arities and a name that is both a constant and a function (which the
928
+ typed writers cannot write either).
929
+ NotImplementedError: a node outside the fragment the typed writers cover.
930
+ """
931
+ formulas = list(formulas)
932
+ for f in formulas:
933
+ _check_fragment(f)
934
+ expanded = [_expand_all_sorted_counts(f) for f in formulas]
935
+ sig = _analyze(expanded)
936
+ clash = sorted(sig.literal_sorts & set(sig.pred_arity))
937
+ if clash:
938
+ raise Tf0Refusal(
939
+ f"{writer}: {clash[0]!r} is both a sort and a predicate in this problem. "
940
+ "This kit reads a sort as the guard predicate of its name (∀x:S φ is "
941
+ "∀x (S(x) → φ), with ∃x S(x) for its non-emptiness), and a typed text "
942
+ "declares the type and the predicate separately, so a prover would answer "
943
+ "a different question than the other routes do. Rename one of the two, "
944
+ f"or {_FOF_ADVICE}.",
945
+ reason="sort_predicate_name")
946
+ _check_reading(sig, expanded, writer=writer, unsorted_type=unsorted_type)
947
+
948
+
949
+ # =============================================================================
950
+ # Name legality: ASCII, first-letter-fold, whole-problem-injective
951
+ # =============================================================================
952
+
953
+ _TPTP_SAFE_RE = re.compile(r"[A-Za-z][A-Za-z0-9_]*")
954
+
955
+
956
+ def _is_tptp_safe(name: str) -> bool:
957
+ return bool(name) and name.isascii() and bool(_TPTP_SAFE_RE.fullmatch(name))
958
+
959
+
960
+ _NON_WORD_CHARACTER = re.compile(r"[^A-Za-z0-9_]")
961
+
962
+
963
+ def _tptp_word_base(name: str, prefix: str) -> str:
964
+ """The base of the replacement for a name that is not a TPTP word: only
965
+ ``[A-Za-z0-9_]``, letter-initial.
966
+
967
+ :func:`~unicode_logic_kit.atp._ascii_names.ascii_safe_base` transliterates a
968
+ non-ASCII character (``θ`` is ``theta``, anything else ``u03a9``) and puts
969
+ ``prefix`` in front of a digit-leading or empty name, but it leaves an ASCII
970
+ character that is no TPTP word character (``-``, ``.``, ``:``, ``$``) where it
971
+ is, and a name written like that is rejected by Vampire and by E
972
+ (``has-part``). Every such character is written as the code-point escape
973
+ ``uXXXX`` the transliteration already uses, so ``has-part`` is
974
+ ``hasu002dpart``; a name that starts with an underscore gets ``prefix`` too.
975
+ Injectivity is not this function's job but the renamer's: a token it
976
+ synthesises is de-collided against every name of the problem
977
+ (:func:`~unicode_logic_kit.atp._ascii_names.reserve_rendered`), so
978
+ ``has-part`` next to ``has_part`` stay two symbols, and so do ``has-part``
979
+ next to a name that is itself spelled ``hasu002dpart``.
980
+ """
981
+ base = _NON_WORD_CHARACTER.sub(lambda m: "u%04x" % ord(m.group()),
982
+ ascii_safe_base(name, prefix))
983
+ return base if base[:1].isalpha() else prefix + base
984
+
985
+
986
+ def _upper_initial(base: str) -> str:
987
+ """Force uppercase-initial — the kit's own PREDICATE/SORT convention."""
988
+ return base[0].upper() + base[1:] if base else base
989
+
990
+
991
+ def _lower_initial(base: str) -> str:
992
+ """Force lowercase-initial — the kit's own NAME (function/constant) convention."""
993
+ return base[0].lower() + base[1:] if base else base
994
+
995
+
996
+ def _render_token(raw: str) -> str:
997
+ """ASCII-transliterate then TPTP-fold a sanitised (possibly still
998
+ non-ASCII) raw name — the single render function every namespace's
999
+ renamer below de-collides against."""
1000
+ return tptp_fold_first_letter(constant_name_to_ascii(raw))
1001
+
1002
+
1003
+ #: Suffix of the replacement name a function/constant receives when its
1004
+ #: rendered TPTP name equals a predicate's (see the module docstring's "A
1005
+ #: predicate and a function/constant that fold to the SAME identifier").
1006
+ #: One shared definition: :mod:`atp._tptp_problem`'s ``fof`` writer imports
1007
+ #: :func:`_separated_term_token` from here so both dialects mint the same
1008
+ #: names.
1009
+ _TERM_SEPARATOR_SUFFIX = "_term"
1010
+
1011
+
1012
+ def _separated_term_token(token: str, taken: Set[str],
1013
+ render: Callable[[str], str] = _render_token) -> str:
1014
+ """The replacement for the term-symbol ``token`` whose rendered name
1015
+ equals a predicate's: its lower-initial form plus
1016
+ :data:`_TERM_SEPARATOR_SUFFIX`, de-collided (numeric suffix) against
1017
+ ``taken`` — the RENDERED form of every identifier the problem already
1018
+ uses, in any namespace — which this call extends with the rendered
1019
+ result (:func:`~unicode_logic_kit.atp._ascii_names.reserve_rendered`).
1020
+
1021
+ ``token`` is already ASCII and letter-initial (a legal name passes
1022
+ through the sanitiser untouched, an illegal one was given such a token),
1023
+ so the result matches ``[a-z][A-Za-z0-9_]*``: it renders as itself, which
1024
+ is what lets a reverse map keyed on the token also match rendered text.
1025
+ """
1026
+ return reserve_rendered(token[:1].lower() + token[1:] + _TERM_SEPARATOR_SUFFIX,
1027
+ taken, render)
1028
+
1029
+
1030
+ @dataclass
1031
+ class _Renamer:
1032
+ """One symbol namespace's original -> TPTP-legal-token map.
1033
+
1034
+ Two passes (mirrors ``atp._tptp_problem._Renamer``): :meth:`collect`
1035
+ reserves every already-legal name immediately (R1: never renamed);
1036
+ :meth:`finalize` synthesises a token for everything else, de-collided
1037
+ against every reservation so far. :meth:`check_no_collisions` then
1038
+ catches the one case the two-pass split cannot rule out by construction:
1039
+ two DIFFERENT already-legal names (both bypass synthesis entirely) that
1040
+ fold to the SAME rendered identifier (``Foo``/``foo``).
1041
+ """
1042
+
1043
+ prefix: str
1044
+ case_fix: Callable[[str], str]
1045
+ mapping: Dict[str, str] = field(default_factory=dict)
1046
+ used: set = field(default_factory=set)
1047
+ _pending: List[str] = field(default_factory=list)
1048
+
1049
+ def collect(self, name: str) -> None:
1050
+ if name in self.mapping or name in self._pending:
1051
+ return
1052
+ if _is_tptp_safe(name):
1053
+ self.used.add(_render_token(name))
1054
+ self.mapping[name] = name
1055
+ else:
1056
+ self._pending.append(name)
1057
+
1058
+ def finalize(self) -> None:
1059
+ for name in self._pending:
1060
+ base = self.case_fix(_tptp_word_base(name, self.prefix))
1061
+ token = reserve_rendered(base, self.used, _render_token)
1062
+ self.mapping[name] = token
1063
+ self._pending = []
1064
+
1065
+ def get(self, name: str) -> str:
1066
+ return _render_token(self.mapping[name])
1067
+
1068
+ def separate_from(self, predicates: "_Renamer", sorts: "_Renamer") -> None:
1069
+ """Rename every name of THIS (function/constant) namespace whose
1070
+ rendered token equals a PREDICATE's or a SORT's rendered token, so that
1071
+ no term symbol shares a TF0 identifier with either (one flat symbol
1072
+ table). A term named like a sort is not read: Vampire resolves the
1073
+ term's name to the type (``The sort $tType of the intended term
1074
+ argument human … is not an instance of sort human``), while E happens
1075
+ to accept it. (A predicate and a sort that share a name never get this
1076
+ far: :func:`_check_no_sort_predicate_clash` refuses the pair.)
1077
+
1078
+ Runs after :meth:`check_no_collisions`, so the refusal of two LEGAL
1079
+ names of one kind (``Foo``/``foo``) is decided first and unchanged.
1080
+ Names are visited in sorted order of their kit-level spelling and the
1081
+ replacement is de-collided against every rendered identifier in the
1082
+ problem (predicates, sorts, this namespace), so the result is the
1083
+ same for the same symbols however the formulas were ordered. Only
1084
+ ``mapping`` changes; a namespace with no clash is left byte-for-byte
1085
+ as it was.
1086
+ """
1087
+ predicate_tokens = {_render_token(t) for t in predicates.mapping.values()}
1088
+ sort_tokens = {_render_token(t) for t in sorts.mapping.values()}
1089
+ taken = predicate_tokens | sort_tokens
1090
+ taken |= {_render_token(t) for t in self.mapping.values()}
1091
+ for name in sorted(self.mapping):
1092
+ if _render_token(self.mapping[name]) in predicate_tokens | sort_tokens:
1093
+ self.mapping[name] = _separated_term_token(self.mapping[name], taken)
1094
+
1095
+ def check_no_collisions(self, kind: str) -> None:
1096
+ seen: Dict[str, str] = {}
1097
+ for name in sorted(self.mapping):
1098
+ rendered = _render_token(self.mapping[name])
1099
+ if rendered in seen and seen[rendered] != name:
1100
+ raise NotImplementedError(
1101
+ f"generate_tff_problem: distinct {kind} names "
1102
+ f"{seen[rendered]!r} and {name!r} would both render as "
1103
+ f"the TFF identifier {rendered!r} — refusing to "
1104
+ "silently merge two distinct symbols; rename one of "
1105
+ "them before exporting this problem."
1106
+ )
1107
+ seen[rendered] = name
1108
+
1109
+
1110
+ @dataclass
1111
+ class _Names:
1112
+ predicate: _Renamer
1113
+ term: _Renamer
1114
+ sort: _Renamer
1115
+
1116
+
1117
+ def _check_no_sort_predicate_clash(predicates: _Renamer, sorts: _Renamer) -> None:
1118
+ """Refuse a sort and a predicate that render as one TF0 identifier.
1119
+
1120
+ The kit reads a sort as the guard predicate of its name (see the module
1121
+ docstring), which TF0, with a type and a predicate in two tables of its own,
1122
+ cannot say. The first pair in the sorted order of the kit names is reported,
1123
+ so the message does not depend on the order of the formulas."""
1124
+ sort_of_word: Dict[str, str] = {}
1125
+ for sort in sorted(sorts.mapping):
1126
+ sort_of_word.setdefault(_render_token(sorts.mapping[sort]), sort)
1127
+ for predicate in sorted(predicates.mapping):
1128
+ word = _render_token(predicates.mapping[predicate])
1129
+ if word in sort_of_word:
1130
+ sort = sort_of_word[word]
1131
+ raise Tf0Refusal(
1132
+ f"generate_tff_problem: the sort {sort!r} and the predicate "
1133
+ f"{predicate!r} would both render as the TF0 identifier {word!r}. "
1134
+ "This kit reads a sort as the guard predicate of its name "
1135
+ "(∀x:S φ is ∀x (S(x) → φ), with ∃x S(x) for its non-emptiness: "
1136
+ "what to_z3, the fof writer and the Prover9 writer do), and TF0 "
1137
+ f"cannot say that: it would declare {word!r} once as a type and "
1138
+ "once as an unrelated predicate, so a prover would answer a "
1139
+ "different question than the other routes do — refusing; rename "
1140
+ "one of the two, or write the problem with the fof writer "
1141
+ "(generate_tptp_problem_with_mapping), which reads the sort as "
1142
+ "that predicate.",
1143
+ reason="sort_predicate_name")
1144
+
1145
+
1146
+ def _build_names(sig: _Signature) -> _Names:
1147
+ predicates = _Renamer(prefix="p", case_fix=_upper_initial)
1148
+ terms = _Renamer(prefix="n", case_fix=_lower_initial)
1149
+ sorts = _Renamer(prefix="s", case_fix=_upper_initial)
1150
+ for name in sig.pred_arity:
1151
+ predicates.collect(name)
1152
+ for name in sorted(sig.func_names | sig.const_names):
1153
+ terms.collect(name)
1154
+ for name in sig.literal_sorts:
1155
+ sorts.collect(name)
1156
+ predicates.finalize()
1157
+ terms.finalize()
1158
+ sorts.finalize()
1159
+ predicates.check_no_collisions("predicate")
1160
+ terms.check_no_collisions("function/constant")
1161
+ sorts.check_no_collisions("sort")
1162
+ _check_no_sort_predicate_clash(predicates, sorts)
1163
+ terms.separate_from(predicates, sorts)
1164
+ return _Names(predicates, terms, sorts)
1165
+
1166
+
1167
+ def _sort_text(sort: str, names: _Names) -> str:
1168
+ if sort == TFF_INDIVIDUAL_SORT:
1169
+ return sort
1170
+ return names.sort.get(sort)
1171
+
1172
+
1173
+ # =============================================================================
1174
+ # Formula rendering — mirrors Node.to_tptp's own (always fully-parenthesised)
1175
+ # shape exactly, so nesting never needs a bespoke parenthesisation policy.
1176
+ # =============================================================================
1177
+
1178
+ def _render_term(node: Node, names: _Names) -> str:
1179
+ if isinstance(node, Variable):
1180
+ return node.name.upper()
1181
+ if isinstance(node, (Constant, SortedConstant)):
1182
+ return names.term.get(node.name)
1183
+ if isinstance(node, Function):
1184
+ name = names.term.get(node.name)
1185
+ if not node.args:
1186
+ return name
1187
+ args = ",".join(_render_term(a, names) for a in node.args)
1188
+ return f"{name}({args})"
1189
+ raise AssertionError(f"generate_tff_problem: unreachable term node {type(node).__name__!r}.")
1190
+
1191
+
1192
+ def _render_atom(node: Atom, names: _Names) -> str:
1193
+ if is_tptp_boolean_atom(node):
1194
+ return node.to_tptp() # the truth constant's own word, whichever spelling it has
1195
+ if node.predicate == "=":
1196
+ return f"{_render_term(node.args[0], names)} = {_render_term(node.args[1], names)}"
1197
+ if node.predicate == "≠":
1198
+ return f"{_render_term(node.args[0], names)} != {_render_term(node.args[1], names)}"
1199
+ pred = names.predicate.get(node.predicate)
1200
+ if not node.args:
1201
+ return pred
1202
+ args = ",".join(_render_term(a, names) for a in node.args)
1203
+ return f"{pred}({args})"
1204
+
1205
+
1206
+ def _quant_symbol(qtype: str) -> str:
1207
+ if qtype in ("∀", "forall"):
1208
+ return "!"
1209
+ if qtype in ("∃", "exists"):
1210
+ return "?"
1211
+ raise ValueError(f"generate_tff_problem: unknown quantifier type {qtype!r}.")
1212
+
1213
+
1214
+ def _render(node: Node, names: _Names) -> str:
1215
+ if isinstance(node, Atom):
1216
+ return _render_atom(node, names)
1217
+ if isinstance(node, Not):
1218
+ return f"~({_render(node.formula, names)})"
1219
+ if isinstance(node, And):
1220
+ return f"({_render(node.left, names)} & {_render(node.right, names)})"
1221
+ if isinstance(node, Or):
1222
+ return f"({_render(node.left, names)} | {_render(node.right, names)})"
1223
+ if isinstance(node, Xor):
1224
+ return f"({_render(node.left, names)} <~> {_render(node.right, names)})"
1225
+ if isinstance(node, Implies):
1226
+ return f"({_render(node.left, names)} => {_render(node.right, names)})"
1227
+ if isinstance(node, Iff):
1228
+ return f"({_render(node.left, names)} <=> {_render(node.right, names)})"
1229
+ if isinstance(node, Quantifier):
1230
+ var = node.variable.name.upper()
1231
+ q = _quant_symbol(node.type)
1232
+ return f"({q}[{var}: {TFF_INDIVIDUAL_SORT}]: {_render(node.formula, names)})"
1233
+ if isinstance(node, SortedQuantifier):
1234
+ var = node.variable.name.upper()
1235
+ q = _quant_symbol(node.type)
1236
+ sort_text = _sort_text(node.sort, names)
1237
+ return f"({q}[{var}: {sort_text}]: {_render(node.formula, names)})"
1238
+ raise AssertionError(f"generate_tff_problem: unreachable formula node {type(node).__name__!r}.")
1239
+
1240
+
1241
+ # =============================================================================
1242
+ # Type declaration rendering
1243
+ # =============================================================================
1244
+
1245
+ def _render_map_type(arg_sorts: List[str], result_sort: str) -> str:
1246
+ if not arg_sorts:
1247
+ return result_sort
1248
+ if len(arg_sorts) == 1:
1249
+ return f"{arg_sorts[0]} > {result_sort}"
1250
+ return "(" + " * ".join(arg_sorts) + ") > " + result_sort
1251
+
1252
+
1253
+ def _render_type_decls(sig: _Signature, names: _Names) -> List[str]:
1254
+ lines: List[str] = []
1255
+ counter = [0]
1256
+
1257
+ def next_name(prefix: str) -> str:
1258
+ counter[0] += 1
1259
+ return f"{prefix}_{counter[0]}"
1260
+
1261
+ for sort in sorted(sig.literal_sorts):
1262
+ lines.append(f"tff({next_name('sort_decl')}, type, {_sort_text(sort, names)}: $tType ).")
1263
+ for name in sorted(sig.func_names):
1264
+ arity = sig.func_arity[name]
1265
+ arg_sorts = [_sort_text(sig.uf.sort_of(("func", name, i)), names) for i in range(arity)]
1266
+ result_sort = _sort_text(sig.uf.sort_of(("func_result", name)), names)
1267
+ type_str = _render_map_type(arg_sorts, result_sort)
1268
+ lines.append(f"tff({next_name('func_decl')}, type, {names.term.get(name)}: {type_str} ).")
1269
+ for name in sorted(sig.const_names):
1270
+ const_sort = _sort_text(sig.uf.sort_of(("const", name)), names)
1271
+ lines.append(f"tff({next_name('const_decl')}, type, {names.term.get(name)}: {const_sort} ).")
1272
+ for name in sorted(sig.pred_arity):
1273
+ arity = sig.pred_arity[name]
1274
+ arg_sorts = [_sort_text(sig.uf.sort_of(("pred", name, i)), names) for i in range(arity)]
1275
+ type_str = _render_map_type(arg_sorts, TFF_BOOLEAN_SORT)
1276
+ lines.append(f"tff({next_name('pred_decl')}, type, {names.predicate.get(name)}: {type_str} ).")
1277
+ return lines
1278
+
1279
+
1280
+ # =============================================================================
1281
+ # Public API
1282
+ # =============================================================================
1283
+
1284
+ def formula_to_tff(formula: Node) -> str:
1285
+ """Render a single formula as bare TFF formula text (no ``tff(...)``
1286
+ statement wrapper) — the TF0 sibling of
1287
+ :func:`unicode_logic_kit.fol.casl_export.formula_to_casl`.
1288
+
1289
+ Runs the full validation pipeline (fragment check, SortedCount lowering,
1290
+ free-variable check, arity/sort-conflict detection, name legalisation —
1291
+ see the module docstring) on ``formula`` ALONE; a symbol's sort/name
1292
+ token is therefore whatever this one formula's own occurrences pin down,
1293
+ not shared with any other formula the way :func:`generate_tff_problem`
1294
+ shares one signature across a whole premise/conclusion batch.
1295
+
1296
+ A function/constant whose TF0 name equals a predicate's is renamed
1297
+ ``<name>_term`` here too, but this function returns bare text and so
1298
+ cannot report it; use :func:`generate_tff_problem_with_mapping` when the
1299
+ rename has to be undone.
1300
+
1301
+ Raises:
1302
+ NotImplementedError: ``formula`` contains a node outside the TF0
1303
+ fragment (see module docstring 'Scope'), a name-folding
1304
+ collision (two distinct symbols in one namespace), a sort and a
1305
+ predicate that render as one identifier, or two LEGAL variables that
1306
+ render as one TPTP variable (``x`` and ``X``), or the typed text would
1307
+ ask another question than the kit's (see :class:`Tf0Refusal`: an
1308
+ unannotated constant, a numeral or a function value typed into a sort, an
1309
+ equation over an unsorted variable next to a sort), or a constant, sorted
1310
+ constant or function is spelled like a numeral of the formula
1311
+ (``Number(1)`` next to ``Constant('1')``). A numeral and ``+ - * /
1312
+ < > ≤ ≥`` are NOT refused: they are written as a constant of ``$i`` and
1313
+ as uninterpreted symbols (see the module docstring).
1314
+ ValueError: a free variable, or an unsatisfiable sort constraint. Every
1315
+ refusal that is not a fragment or name-folding one is a
1316
+ :class:`Tf0Refusal`, which is a ``ValueError`` and a
1317
+ ``NotImplementedError`` at once.
1318
+ """
1319
+ with refusals_speak_as("formula_to_tff", "generate_tff_problem"):
1320
+ [formula], numerals = numerals_as_constants([formula], where="generate_tff_problem")
1321
+ _check_fragment(formula)
1322
+ formula = legalise_variables(formula)
1323
+ expanded = _expand_all_sorted_counts(formula)
1324
+ check_variable_names(expanded, where="generate_tff_problem", subject="formula")
1325
+ sig = _analyze([expanded], uninterpreted_arithmetic=True)
1326
+ names = _build_names(sig)
1327
+ _check_reading(sig, [expanded], writer="generate_tff_problem", numerals=numerals)
1328
+ return _render(expanded, names)
1329
+
1330
+
1331
+ def generate_tff_problem(premises: List[Node], conclusion: Optional[Node] = None,
1332
+ *, premise_names: Optional[Sequence[str]] = None) -> str:
1333
+ """Build a native, genuinely-typed TPTP ``tff`` problem string.
1334
+
1335
+ The TF0 sibling of :func:`atp._tptp_problem.generate_tptp_problem`: same
1336
+ ``premise_<i>``/``goal`` naming and axiom/conjecture role split, but
1337
+ emits real ``tff(name, type, Sym: Type).`` declarations (one per sort,
1338
+ predicate, function, and constant used anywhere in ``premises`` +
1339
+ ``conclusion``) followed by formula bodies with genuine typed
1340
+ quantifiers, instead of the classical route's guard-predicate encoding.
1341
+
1342
+ Args:
1343
+ premises: many-sorted (or plain classical) FOL premises, using
1344
+ :class:`~unicode_logic_kit.fol.nodes.SortedQuantifier` /
1345
+ :class:`~unicode_logic_kit.fol.nodes.SortedConstant` /
1346
+ :class:`~unicode_logic_kit.fol.nodes.SortedCount` freely alongside
1347
+ plain :class:`~unicode_logic_kit.fol.nodes.Quantifier` (which
1348
+ renders as an explicitly-``$i``-typed variable).
1349
+ conclusion: the conjecture, or ``None`` for a problem without one
1350
+ (no ``conjecture`` line is written; the prover is asked whether
1351
+ the premises are satisfiable).
1352
+ premise_names: one name per premise for the ``axiom`` lines instead of
1353
+ ``premise_<i>`` — see :func:`generate_tff_problem_with_mapping`, which also
1354
+ records them.
1355
+
1356
+ Returns:
1357
+ The complete ``tff`` problem text, newline-terminated: type
1358
+ declarations first (sorts, then functions, then constants, then
1359
+ predicates — declaration-before-use, matching
1360
+ ``casl_export.to_casl_spec``'s ordering), then one
1361
+ ``tff(premise_<i>, axiom, ...).`` per premise (1-based), then
1362
+ ``tff(goal, conjecture, ...).`` unless ``conclusion`` is ``None``.
1363
+
1364
+ Raises:
1365
+ NotImplementedError: a node outside the TF0 fragment (see module
1366
+ docstring 'Scope'), a name-folding collision, a sort and a predicate
1367
+ that render as one identifier, two variables of one formula that
1368
+ render as one TPTP variable, or a constant spelled like a numeral
1369
+ of the problem — see :func:`formula_to_tff`.
1370
+ ValueError: a free variable, an arity conflict, a constant-vs-
1371
+ function name clash, or an unsatisfiable sort constraint — see
1372
+ :func:`formula_to_tff`. These and the typed-reading refusals
1373
+ (an unannotated constant or a function value that the inference
1374
+ types into a sort, an equation over an unsorted variable next to a
1375
+ sort; see the module docstring and :class:`Tf0Refusal`) are all
1376
+ :class:`Tf0Refusal`, a ``ValueError`` and a ``NotImplementedError``
1377
+ at once; the backends' automatic mode (``tff=None``) writes ``fof``
1378
+ instead when it is raised.
1379
+
1380
+ A function or constant whose TF0 name equals a predicate's (the class
1381
+ ``Agent`` and the role function ``agent``) is renamed ``<name>_term`` so
1382
+ the problem never declares one identifier at two types; use
1383
+ :func:`generate_tff_problem_with_mapping` to also receive the record of
1384
+ that rename.
1385
+ """
1386
+ text, _mapping = _write_tff_problem(premises, conclusion, premise_names)
1387
+ return text
1388
+
1389
+
1390
+ def generate_tff_problem_with_mapping(premises: List[Node],
1391
+ conclusion: Optional[Node] = None,
1392
+ *, premise_names: Optional[Sequence[str]] = None
1393
+ ) -> Tuple[str, "TptpNameMap"]:
1394
+ """Like :func:`generate_tff_problem`, but also returns the
1395
+ :class:`~unicode_logic_kit.atp._tptp_problem.TptpNameMap` recording every
1396
+ predicate/function/constant name the writer changed — a name that is not
1397
+ TPTP-legal (non-ASCII, digit-leading) and a function/constant renamed
1398
+ because its TF0 name equals a predicate's — and the premise names.
1399
+
1400
+ Returns ``(text, name_map)``. ``name_map`` is the same
1401
+ :class:`~unicode_logic_kit.atp._tptp_problem.TptpNameMap` the ``fof`` writer
1402
+ (:func:`~unicode_logic_kit.atp._tptp_problem.generate_tptp_problem_with_mapping`)
1403
+ returns, so :func:`~unicode_logic_kit.atp._tptp_problem.apply_reverse_tptp`
1404
+ translates a formula read back from this problem (or from a prover's
1405
+ answer to it) to the original kit-level names. Sort names are not part of
1406
+ the map: a sort that is not TPTP-legal is still rewritten (as before, with
1407
+ no record of it), and two distinct sorts that fold together are refused.
1408
+ ``name_map.premises`` holds the premise names in order, also the default ones.
1409
+
1410
+ ``premise_names`` names the premises' ``axiom`` lines instead of
1411
+ ``premise_<i>``, exactly as in
1412
+ :func:`~unicode_logic_kit.atp._tptp_problem.generate_tptp_problem_with_mapping`; the
1413
+ names the writer gives its own lines are ``goal``, ``sort_decl_<n>``,
1414
+ ``func_decl_<n>``, ``const_decl_<n>`` and ``pred_decl_<n>`` (a premise named like
1415
+ one of them is a :class:`Tf0Refusal`, which is a ``ValueError``).
1416
+
1417
+ Raises what :func:`generate_tff_problem` raises, and, for ``premise_names``,
1418
+ ``TypeError`` (a single string, a non-string entry) and ``ValueError`` (other than
1419
+ one name per premise, a name no TPTP name can spell, two names that are the same
1420
+ as written).
1421
+ """
1422
+ with refusals_speak_as("generate_tff_problem_with_mapping", "generate_tff_problem"):
1423
+ return _write_tff_problem(premises, conclusion, premise_names)
1424
+
1425
+
1426
+ def _write_tff_problem(premises: Sequence[Node], conclusion: Optional[Node],
1427
+ premise_names: Optional[Sequence[str]]
1428
+ ) -> Tuple[str, "TptpNameMap"]:
1429
+ """The TF0 writer; its refusals open with ``generate_tff_problem`` (the entry points
1430
+ above word them as their own, :func:`~unicode_logic_kit.atp._writer_support
1431
+ .refusals_speak_as`)."""
1432
+ from ._tptp_problem import TptpNameMap # deferred: that module imports this one
1433
+
1434
+ premises = list(premises)
1435
+ premise_labels = normalise_premise_names(premise_names, len(premises),
1436
+ where="generate_tff_problem")
1437
+ formulas = premises + ([] if conclusion is None else [conclusion])
1438
+ # A numeral is a constant of $i (see the module docstring): written under the name of
1439
+ # its value, so that the renamer gives it a word and the map reads it back.
1440
+ formulas, numerals = numerals_as_constants(formulas, where="generate_tff_problem")
1441
+ for f in formulas:
1442
+ _check_fragment(f)
1443
+ formulas = [legalise_variables(f) for f in formulas]
1444
+ expanded = [_expand_all_sorted_counts(f) for f in formulas]
1445
+ for f in expanded:
1446
+ check_variable_names(f, where="generate_tff_problem", subject="problem")
1447
+ sig = _analyze(expanded, uninterpreted_arithmetic=True)
1448
+ names = _build_names(sig)
1449
+ _check_reading(sig, expanded, writer="generate_tff_problem", numerals=numerals)
1450
+
1451
+ lines = _render_type_decls(sig, names)
1452
+ generated = [("goal", "the conjecture")] if conclusion is not None else []
1453
+ for line in lines:
1454
+ # tff(sort_decl_1, type, ...): the name of each declaration the writer gave
1455
+ generated.append((line[len("tff("):line.index(",")], "a type declaration"))
1456
+ try:
1457
+ check_against_generated(premise_labels, generated, where="generate_tff_problem")
1458
+ except ValueError as clash:
1459
+ raise Tf0Refusal(str(clash), reason="premise_name") from None
1460
+ for label, premise in zip(premise_labels, expanded):
1461
+ lines.append(f"tff({tptp_name_token(label)}, axiom, {_render(premise, names)} ).")
1462
+ if conclusion is not None:
1463
+ lines.append(f"tff(goal, conjecture, {_render(expanded[-1], names)} ).")
1464
+ return "\n".join(lines) + "\n", TptpNameMap(
1465
+ predicate=dict(names.predicate.mapping), term=dict(names.term.mapping),
1466
+ premises=premise_labels, numerals=numerals)
1467
+
1468
+
1469
+ def infer_tff_signature(formulas: Sequence[Node]) -> Signature:
1470
+ """Infer a full :class:`~unicode_logic_kit.fol.signature.Signature` (with
1471
+ per-argument-position and result sorts pinned down, never ``None``) from
1472
+ a batch of formulas — the same union-find inference
1473
+ :func:`generate_tff_problem` uses internally, exposed as a genuine
1474
+ :class:`Signature` object for callers that want it directly (e.g. to
1475
+ call :meth:`Signature.validate` on a further formula against the same
1476
+ vocabulary) rather than TFF text.
1477
+
1478
+ Unlike :meth:`Signature.from_formulas` (which leaves every argument
1479
+ position unsorted — see that method's docstring), every predicate/
1480
+ function argument and every function/constant result here is pinned to
1481
+ a concrete sort, defaulting to :data:`TFF_INDIVIDUAL_SORT` (``"$i"``)
1482
+ wherever the formulas never connect that position to a declared sort —
1483
+ exactly TPTP's own "no type given" reading, and exactly the sort text
1484
+ :func:`generate_tff_problem` would emit for it.
1485
+
1486
+ Kit-level names are used verbatim (unlike the TFF-text writer, this
1487
+ function does no ASCII/TPTP-legality renaming) — the returned
1488
+ :class:`Signature` is meant for use back inside the kit, not for
1489
+ rendering TPTP text.
1490
+
1491
+ This is signature INFERENCE, not a decision route: it reports the sort the
1492
+ writers' union-find assigns to every position, including the result of a
1493
+ function and an unannotated constant at a sorted position, which the kit's
1494
+ own reading of a sort does NOT say (a function has no declared result sort).
1495
+ :func:`generate_tff_problem` therefore refuses a problem in which the
1496
+ inference would do that (see :func:`check_typed_reading`), while this
1497
+ function keeps inferring.
1498
+
1499
+ Raises:
1500
+ NotImplementedError: a node outside the fragment, and a numeral or an
1501
+ arithmetic operator or comparison, which this function does not read as
1502
+ an ordinary symbol (:func:`generate_tff_problem` does, and writes it
1503
+ under a word of its own: no renaming happens here) — never a
1504
+ name-folding collision.
1505
+ ValueError: as :func:`formula_to_tff` (free variable, arity/sort
1506
+ conflicts).
1507
+ """
1508
+ formulas = list(formulas)
1509
+ for f in formulas:
1510
+ _check_fragment(f)
1511
+ expanded = [_expand_all_sorted_counts(f) for f in formulas]
1512
+ sig = _analyze(expanded)
1513
+
1514
+ def sort_or_none(slot) -> Optional[str]:
1515
+ resolved = sig.uf.sort_of(slot)
1516
+ return None if resolved == TFF_INDIVIDUAL_SORT else resolved
1517
+
1518
+ predicates: Dict[str, PredicateDecl] = {}
1519
+ for name, arity in sig.pred_arity.items():
1520
+ arg_sorts = tuple(sort_or_none(("pred", name, i)) for i in range(arity))
1521
+ predicates[name] = PredicateDecl(name, arity, arg_sorts)
1522
+
1523
+ functions: Dict[str, FunctionDecl] = {}
1524
+ for name, arity in sig.func_arity.items():
1525
+ arg_sorts = tuple(sort_or_none(("func", name, i)) for i in range(arity))
1526
+ result_sort = sort_or_none(("func_result", name))
1527
+ functions[name] = FunctionDecl(name, arity, arg_sorts, result_sort)
1528
+
1529
+ constants: Dict[str, ConstantDecl] = {}
1530
+ for name in sig.const_names:
1531
+ constants[name] = ConstantDecl(name, sort_or_none(("const", name)))
1532
+
1533
+ sorts: Set[str] = set(sig.literal_sorts)
1534
+ # Every declaration above is built with a tuple of argument sorts, never None.
1535
+ for pred_decl in predicates.values():
1536
+ sorts.update(s for s in pred_decl.arg_sorts or () if s is not None)
1537
+ for func_decl in functions.values():
1538
+ sorts.update(s for s in func_decl.arg_sorts or () if s is not None)
1539
+ if func_decl.result_sort is not None:
1540
+ sorts.add(func_decl.result_sort)
1541
+ for const_decl in constants.values():
1542
+ if const_decl.sort is not None:
1543
+ sorts.add(const_decl.sort)
1544
+
1545
+ return Signature(predicates=predicates, functions=functions,
1546
+ constants=constants, sorts=frozenset(sorts))