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,1503 @@
1
+ """TPTP input: read TPTP ``fof`` / ``cnf`` formulas and whole problem files into the AST.
2
+
3
+ This is the inverse of :meth:`Node.to_tptp`. TPTP has a genuinely different surface
4
+ syntax from the toolkit's Unicode notation (the ``fof(name, role, formula).`` wrapper,
5
+ ``! [X] : φ`` / ``? [X] : φ`` quantifiers, uppercase variables, lowercase
6
+ constants/functions/predicates, ``& | ~ => <=> <~>`` connectives, ``$less``/``$sum``
7
+ dollar-words), so it gets its own Lark grammar rather than the glyph-translation used
8
+ for LaTeX import.
9
+
10
+ Public API:
11
+
12
+ - :func:`parse_tptp_formula` — parse a single bare FOF/CNF formula into a :class:`Node`.
13
+ - :func:`parse_tptp` — parse a whole TPTP problem (one or more ``fof``/``cnf``
14
+ statements) into a list of :class:`TptpFormula` ``(name, role, formula)`` records.
15
+ - :func:`load_tptp` — read a ``.p`` / ``.tptp`` file and :func:`parse_tptp` it.
16
+
17
+ Naming conventions (the exact inverse of :meth:`Node.to_tptp`): a TPTP **variable**
18
+ (uppercase) becomes a lowercase :class:`Variable`; a **predicate** (lowercase in TPTP)
19
+ is capitalised to match the toolkit's uppercase-predicate convention, so ``loves`` ↦
20
+ ``Loves`` and ``p`` ↦ ``P``; **constants** and **functions** stay lowercase; ``=`` /
21
+ ``!=`` map to the ``=`` / ``≠`` atoms; the comparison dollar-words ``$less`` /
22
+ ``$greater`` / ``$lesseq`` / ``$greatereq`` map to the ``<`` / ``>`` / ``≤`` / ``≥``
23
+ atoms; and the arithmetic dollar-words ``$sum`` / ``$difference`` / ``$product`` /
24
+ ``$quotient`` map to the ``+`` / ``-`` / ``*`` / ``/`` functions. ``$true`` / ``$false``
25
+ are imported as the nullary atoms ``$true`` / ``$false`` (the toolkit has no
26
+ boolean-constant node), which every route that decides or evaluates a formula reads
27
+ as TPTP's defined propositions — true and false — and the TPTP writers write back
28
+ verbatim.
29
+
30
+ A TPTP variable is case-sensitive and the kit variable is its lower-cased form, so
31
+ two variables that differ only by letter case (``Xa``, ``XA``) would become one
32
+ kit variable. Where that changes the formula — one is captured by the other's
33
+ quantifier, or both occur free in one formula — the reader REFUSES it by name,
34
+ naming both variables (``![Xa, XA]: p(Xa, XA)`` is not read as ``∀xa ∀xa
35
+ P(xa, xa)``); where each is bound by a quantifier of its own and they never meet
36
+ (``(![Xa]: p(Xa)) & (![XA]: q(XA))``) the reading is the same formula and is
37
+ unchanged. No renaming is invented: rename one of the two.
38
+
39
+ Single-quoted atoms (``'http___example_org_Thing'``) — the form OWL→FOL translators
40
+ emit for IRIs — are accepted as functor / predicate / constant names: the quotes are
41
+ stripped and the ``\\'`` / ``\\\\`` escapes unescaped. The resulting names may contain
42
+ characters that are not legal MSFLParser tokens, so call
43
+ :func:`unicode_logic_kit.fol.sanitize_names` before re-parsing the rendered Unicode.
44
+
45
+ Scope: the first-order ``fof`` fragment, ``cnf`` clauses, and TF0 (monomorphic
46
+ typed first-order) ``tff`` — see :func:`parse_tff_problem` below for the typed
47
+ half. ``include('path')`` / ``include('path', [name, ...])`` directives are
48
+ resolved by every file/load entry point (see "Includes" below); the optional
49
+ 4th (``source``)/5th (``useful_info``) annotation fields of a statement parse
50
+ and are discarded. THF (higher-order TPTP), TF1 polymorphism (type
51
+ variables, ``!>``), and TPTP's built-in arithmetic sorts (``$int``/``$rat``/
52
+ ``$real``) remain out of scope — each is refused LOUDLY, naming the
53
+ construct, rather than silently narrowed. THF and TF1 are refused BEFORE the
54
+ first-order grammar is tried, by the statement's kind and by the binder, so the
55
+ refusal does not depend on the rest of the text parsing: a problem with a ``thf(...)``
56
+ statement (a type declaration, an application ``p @ A``, whatever its body) is a
57
+ :class:`TptpParsingError` naming THF, and a text with the type binder ``!>`` or a
58
+ quantifier variable of type ``$tType`` (``![A: $tType]``) is an error naming TF1
59
+ polymorphism that is a :class:`TptpParsingError` and also a
60
+ :class:`NotImplementedError`. Comments and single-quoted atoms are taken out first,
61
+ so a ``thf(`` or a ``!>`` inside one is no statement and no binder. The arithmetic sorts
62
+ are refused by :class:`NotImplementedError` where a declaration or a quantifier names
63
+ one (see :func:`parse_tff_problem`'s docstring for exactly which constructs raise
64
+ where).
65
+
66
+ Includes
67
+ --------
68
+ :func:`parse_tptp`, :func:`parse_tptp_problem` and :func:`parse_tff_problem`
69
+ take an optional ``base_dir`` (the directory an ``include(...)`` inside
70
+ ``text`` resolves relative to first) and ``search_paths`` (further roots
71
+ tried next, in order — the TPTP-library convention: ``include(
72
+ 'Axioms/SET001+0.ax')`` is root-relative, not relative to whatever file
73
+ referred to it). ``os.environ["TPTP"]``, if set, is ALSO tried as a root,
74
+ after ``search_paths`` — the same convention external TPTP tooling uses to
75
+ locate the library. ``base_dir=None`` (the default) means "no including
76
+ file's directory is known": an ``include`` then raises
77
+ :class:`TptpParsingError` naming the directive rather than guessing, which
78
+ keeps every existing bare-text caller — :func:`parse_tptp_formula` included,
79
+ which never resolves includes at all, since a single bare formula has no
80
+ "including file" — byte-identical by default.
81
+
82
+ :func:`load_tptp`, :func:`load_tptp_problem` and :func:`load_tff_problem`
83
+ set ``base_dir`` to the loaded file's own directory automatically (so a
84
+ sibling include resolves with no extra argument) and seed cycle detection
85
+ with the loaded file's own path, so a chain that loops back to the very
86
+ file you started from is caught, not only a cycle confined to files reached
87
+ purely via nested includes. A selection list (``include('path', [name1,
88
+ name2])``) imports only those formulas, matched by their own TPTP statement
89
+ name, and refuses a name absent from the included file; a missing include
90
+ file names the path and every root tried; a circular include chain names
91
+ the whole chain. A selection list on a file that also declares TFF
92
+ vocabulary (a ``tff(name, type, ...).`` statement) is refused — see
93
+ :func:`parse_tff_problem`'s docstring.
94
+
95
+ TF0 (``tff``) reading, in brief
96
+ --------------------------------
97
+ :func:`parse_tptp` / :func:`parse_tptp_formula` / :func:`load_tptp` now also
98
+ accept ``tff(name, axiom|conjecture|..., <formula>).`` statements (typed
99
+ quantifiers ``! [X: sort] : φ`` / ``? [X: sort] : φ`` build
100
+ :class:`~unicode_logic_kit.fol.nodes.SortedQuantifier`; an untyped ``! [X] : φ``
101
+ still builds a plain :class:`~unicode_logic_kit.fol.nodes.Quantifier`, exactly
102
+ as before — this is purely additive). A ``tff(name, type, ...).`` TYPE
103
+ DECLARATION, however, has no ``TptpFormula`` shape to return (it declares
104
+ vocabulary, not a formula), so :func:`parse_tptp` refuses a problem
105
+ containing one, naming :func:`parse_tff_problem` as the function that reads
106
+ it: that function returns the declared
107
+ :class:`~unicode_logic_kit.fol.signature.Signature` ALONGSIDE the formulas,
108
+ and additionally promotes a plain :class:`~unicode_logic_kit.fol.nodes.Constant`
109
+ occurrence to a :class:`~unicode_logic_kit.fol.nodes.SortedConstant` wherever
110
+ that name was declared with a concrete (non-``$i``) sort — recovering the
111
+ same AST shape :func:`~unicode_logic_kit.atp.tptp_tff.generate_tff_problem`
112
+ started from, since a TFF formula BODY carries no inline sort annotation for
113
+ a constant occurrence (only a bound variable does; a constant's sort lives
114
+ solely in its separate ``type`` declaration).
115
+
116
+ Two parsers, LALR first and Earley as a fallback
117
+ ------------------------------------------------
118
+ The grammar below is parsed by FOUR lark parsers: an LALR(1) pair (``file``
119
+ and ``formula`` start symbols) that is tried first, and the original Earley
120
+ pair that takes over whenever LALR raises ``UnexpectedInput``. The reason is
121
+ measured, not stylistic: on a real 1,367,212-byte, 4291-formula TPTP
122
+ translation of an ontology, ``load_tptp_problem`` took 68.3 s through Earley
123
+ and 1.7 s through LALR — and the two produce BYTE-IDENTICAL item lists for
124
+ all 4291 records. The cost is per formula (4.15 ms against 0.2 ms), not
125
+ superlinear in file size, and a ``cProfile`` run attributes 98 % of it to
126
+ lark's dynamic-lexer Earley chart, so this is the only lever that matters.
127
+
128
+ The fallback is what makes it risk-free, and only the PARSE step falls back:
129
+ if LALR refuses a text, Earley re-parses it and Earley raises, so no input
130
+ that parsed before stops parsing and every syntax-error message stays
131
+ byte-identical. The TRANSFORM step is never retried — the TF0-scope refusals
132
+ (``NotImplementedError`` for TF1/``$int``/``$rat``/``$real``, the
133
+ ``ConflictingArityError`` path) come out of the shared transformer and are
134
+ identical either way, so a retry would only double the work.
135
+
136
+ The asymmetry that remains: LALR uses lark's contextual lexer and Earley the
137
+ dynamic one, so in principle LALR could ACCEPT a text Earley rejects, which
138
+ the fallback does not protect against. Every terminal here is disjoint by
139
+ first character (``VAR`` ``[A-Z]``, ``LOWER`` ``[a-z]``, ``SQ`` ``'``,
140
+ ``DOLLARWORD`` ``$``, ``NUMBER`` a digit or ``-``) and every operator is a
141
+ literal string resolved by longest match, so a divergence would need a
142
+ GRAMMAR change to introduce. ``tests/test_tptp_input.py`` therefore keeps a
143
+ permanent parametrised agreement battery over both parsers, plus an explicit
144
+ assertion that the grammar still builds as LALR(1) — so a future grammar edit
145
+ that breaks the equivalence goes red instead of silently falling back to
146
+ Earley for every parse.
147
+ """
148
+
149
+ import os
150
+ import re
151
+ from dataclasses import dataclass, field
152
+ from typing import Any, Dict, List, Optional, Tuple
153
+
154
+ from lark import Lark, Transformer, Tree
155
+ from lark.exceptions import UnexpectedInput, VisitError
156
+
157
+ from ._fol_nodes import _numeral_from_text
158
+ from .nodes import (
159
+ Node, Variable, Constant, Number, Function,
160
+ Atom, Not, And, Or, Xor, Implies, Iff, Quantifier,
161
+ SortedQuantifier, SortedConstant,
162
+ )
163
+ from .naming import ParsingError
164
+ from .signature import Signature, PredicateDecl, FunctionDecl, ConstantDecl
165
+
166
+
167
+ class TptpParsingError(ParsingError):
168
+ """A TPTP import failure, carrying a plain message.
169
+
170
+ Subclasses :class:`ParsingError` (so ``except ParsingError`` still catches it)
171
+ but takes a string instead of a Lark exception — the same pattern
172
+ :class:`ConflictingArityError` uses.
173
+ """
174
+
175
+ def __init__(self, message: str):
176
+ self.args = (message,)
177
+
178
+ def __str__(self):
179
+ return self.args[0]
180
+
181
+
182
+ class _Tf1PolymorphismError(TptpParsingError, NotImplementedError):
183
+ """TF1 polymorphism (a type variable), refused by name.
184
+
185
+ A :class:`TptpParsingError`, like every other refusal of a text this reader does not
186
+ read, and also a :class:`NotImplementedError`, which is what the refusal of a TF1
187
+ type declaration always was: a caller that catches either keeps catching it.
188
+ """
189
+
190
+
191
+ _THF_REFUSAL = (
192
+ "SYNTAX_ERROR: THF (higher-order TPTP) is out of scope for "
193
+ "this reader; only fof, cnf, and tff (TF0, monomorphic) are "
194
+ "supported."
195
+ )
196
+
197
+ _TF1_REFUSAL = (
198
+ "TF0 reader: TF1 polymorphic types and type variables ('!> [...] : ...', "
199
+ "'![A: $tType] : ...') are out of scope for this monomorphic TF0-only reader."
200
+ )
201
+
202
+
203
+ # Dollar-word predicate / function dictionaries — the inverse of the
204
+ # PREFIX_PREDS_TPTP / TPTP_ARITH_OPS tables in _fol_nodes.py.
205
+ _DOLLAR_PRED = {"$less": "<", "$greater": ">", "$lesseq": "≤", "$greatereq": "≥"}
206
+ _DOLLAR_FUNC = {"$sum": "+", "$difference": "-", "$product": "*", "$quotient": "/"}
207
+
208
+
209
+ @dataclass(frozen=True)
210
+ class TptpFormula:
211
+ """One annotated TPTP statement: its ``name``, ``role``, and parsed ``formula``."""
212
+
213
+ name: str
214
+ role: str
215
+ formula: Node
216
+
217
+
218
+ @dataclass(frozen=True)
219
+ class _IncludeDirective:
220
+ """One parsed ``include('path').`` / ``include('path', [n1, n2]).`` directive.
221
+
222
+ Internal only: ``file()`` (see :class:`_TptpTransformer`) returns these
223
+ interleaved with :class:`TptpFormula` / TF0 declaration records in
224
+ source order, and :func:`_resolve_includes` splices each one, in place,
225
+ with the (recursively resolved) statements of the file it names, before
226
+ any public function ever sees one. ``selection`` is ``None`` when no
227
+ ``[...]`` name list was given (import everything from the included
228
+ file), else the tuple of names to import selectively.
229
+ """
230
+
231
+ file_name: str
232
+ selection: Optional[Tuple[str, ...]]
233
+
234
+
235
+ _GRAMMAR = r"""
236
+ file: (stmt | include_stmt)+
237
+ stmt: LOWER "(" fof_name "," LOWER "," formula ["," annotation_term ["," annotation_term]] ")" "."
238
+ | LOWER "(" fof_name "," LOWER "," tff_type_decl ["," annotation_term ["," annotation_term]] ")" "." -> stmt_type
239
+ fof_name: LOWER | NUMBER
240
+
241
+ // --- include directives: "include('path')." / "include('path', [n1,n2])." ---
242
+ // Resolved by _resolve_includes AFTER transforming (splicing in the named
243
+ // file's own statements), never during parsing -- see that function and the
244
+ // module docstring's "Includes" section.
245
+ include_stmt: "include" "(" (LOWER | SQ) ["," "[" name_list "]"] ")" "."
246
+ name_list: fof_name ("," fof_name)*
247
+
248
+ // --- the optional 4th (source) / 5th (useful_info) annotation fields ---
249
+ // Accepted syntactically and discarded -- see _TptpTransformer.stmt /
250
+ // .stmt_type. Not TPTP's full "general_term" grammar (no THF-shaped terms,
251
+ // no arithmetic expressions), just enough to admit the shapes real TPTP
252
+ // files use for these two fields: a functor application (`file(...)`,
253
+ // `inference(...)`), a bracketed list of either, or a bare atom/variable/
254
+ // number/quoted string.
255
+ annotation_term: LOWER "(" annotation_term_list ")"
256
+ | "[" [annotation_term_list] "]"
257
+ | LOWER
258
+ | SQ
259
+ | VAR
260
+ | NUMBER
261
+ annotation_term_list: annotation_term ("," annotation_term)*
262
+
263
+ ?formula: equiv
264
+ ?equiv: imp
265
+ | imp "<=>" imp -> iff
266
+ | imp "<~>" imp -> xor
267
+ ?imp: disj
268
+ | disj "=>" imp -> implies
269
+ | disj "<=" imp -> rev_implies
270
+ ?disj: conj
271
+ | disj "|" conj -> or_
272
+ | disj "~|" conj -> nor
273
+ ?conj: unary
274
+ | conj "&" unary -> and_
275
+ | conj "~&" unary -> nand
276
+ ?unary: "~" unary -> neg
277
+ | "!" "[" varlist "]" ":" unary -> forall
278
+ | "?" "[" varlist "]" ":" unary -> exists
279
+ | "(" formula ")"
280
+ | atom
281
+
282
+ // A TFF quantifier's variable may carry an explicit ":" sort. A plain,
283
+ // untyped "! [X] : ..." still round-trips (typed_var's optional sort is
284
+ // simply absent) -- shared unmodified by fof/cnf too, see module docstring.
285
+ varlist: typed_var ("," typed_var)*
286
+ typed_var: VAR (":" tff_atomic_type)?
287
+
288
+ ?atom: term "=" term -> equality
289
+ | term "!=" term -> disequality
290
+ | DOLLARWORD "(" termlist ")" -> dollar_atom
291
+ | LOWER "(" termlist ")" -> pred_app
292
+ | SQ "(" termlist ")" -> pred_app
293
+ | DOLLARWORD -> bool_const
294
+ | LOWER -> prop_atom
295
+ | SQ -> prop_atom
296
+
297
+ ?term: VAR -> var
298
+ | DOLLARWORD "(" termlist ")" -> dollar_func_app
299
+ | LOWER "(" termlist ")" -> func_app
300
+ | SQ "(" termlist ")" -> func_app
301
+ | LOWER -> constant
302
+ | SQ -> constant
303
+ | NUMBER -> number
304
+
305
+ termlist: term ("," term)*
306
+
307
+ // --- TF0 type declarations: "tff(name, type, <tff_type_decl>)." ---
308
+ // (TF1 polymorphism and THF are NOT extensions of this: tff_poly_decl below
309
+ // always raises, and "thf(" is refused at the statement-keyword check —
310
+ // see the module docstring and _TptpTransformer.stmt_type/tff_poly_decl.)
311
+ tff_type_decl: (LOWER | SQ) ":" tff_top_type
312
+
313
+ ?tff_top_type: DOLLAR_TTYPE -> tff_sort_decl
314
+ | "!>" "[" tyvarlist "]" ":" tff_poly_monotype -> tff_poly_decl
315
+ | tff_mapping_type -> tff_symbol_decl
316
+ | tff_atomic_type -> tff_nullary_decl
317
+
318
+ tyvarlist: VAR ("," VAR)*
319
+
320
+ // The TF1 polymorphic body ("!> [A] : (A > A)" and similar) is parsed only
321
+ // so tff_poly_decl below can raise a NAMED refusal instead of a generic
322
+ // parse failure -- a type VARIABLE (VAR) is otherwise never a legal atomic
323
+ // type (see tff_atomic_type), so this sub-grammar is kept separate rather
324
+ // than widening the ordinary (monomorphic) one. Never given transformer
325
+ // methods: tff_poly_decl discards its children unconditionally.
326
+ ?tff_poly_monotype: tff_poly_atomic_type
327
+ | "(" tff_poly_mapping_type ")"
328
+ tff_poly_mapping_type: tff_poly_domain ">" tff_poly_atomic_type
329
+ tff_poly_domain: tff_poly_atomic_type
330
+ | "(" tff_poly_xprod ")"
331
+ tff_poly_xprod: tff_poly_atomic_type ("*" tff_poly_atomic_type)+
332
+ tff_poly_atomic_type: LOWER | SQ | DOLLARWORD | VAR
333
+
334
+ tff_mapping_type: tff_domain ">" tff_atomic_type
335
+ ?tff_domain: tff_atomic_type -> domain_single
336
+ | "(" tff_xprod ")" -> domain_prod
337
+ tff_xprod: tff_atomic_type ("*" tff_atomic_type)+
338
+
339
+ tff_atomic_type: LOWER -> plain_type
340
+ | SQ -> plain_type
341
+ | DOLLARWORD -> dollar_type
342
+
343
+ VAR: /[A-Z][A-Za-z0-9_]*/
344
+ LOWER: /[a-z][A-Za-z0-9_]*/
345
+ SQ: /'(\\.|[^'\\])*'/
346
+ DOLLARWORD: /\$[a-z_]+/
347
+ DOLLAR_TTYPE.2: "$tType"
348
+ NUMBER: /-?[0-9]+(\.[0-9]+)?/
349
+
350
+ %import common.WS
351
+ %ignore WS
352
+ %ignore /%[^\r\n]*/
353
+ %ignore /\/\*(.|\n)*?\*\//
354
+ """
355
+
356
+
357
+ def _cap(name: str) -> str:
358
+ """Capitalise the first letter (invert to_tptp's predicate lower-casing)."""
359
+ return name[:1].upper() + name[1:] if name else name
360
+
361
+
362
+ def _functor_name(token) -> str:
363
+ """Return the bare functor/constant name from a LOWER or single-quoted token.
364
+
365
+ A TPTP **single-quoted atom** ``'…'`` is an arbitrary functor name (the form
366
+ OWL→FOL dumps use for IRIs, e.g. ``'http___example_org_Thing'``). The
367
+ surrounding quotes are stripped and the TPTP escapes ``\\'`` / ``\\\\`` are
368
+ unescaped; a plain LOWER token is returned unchanged. The resulting name may
369
+ contain characters (underscores, etc.) that are not legal MSFLParser tokens —
370
+ use :func:`unicode_logic_kit.fol.sanitize_names` before re-parsing the rendered
371
+ Unicode.
372
+ """
373
+ s = str(token)
374
+ if len(s) >= 2 and s[0] == "'" and s[-1] == "'":
375
+ return re.sub(r"\\(.)", r"\1", s[1:-1])
376
+ return s
377
+
378
+
379
+ # =============================================================================
380
+ # TF0 type declarations: parsed shapes and atomic-type resolution
381
+ # =============================================================================
382
+
383
+ _ARITHMETIC_SORTS = frozenset({"$int", "$rat", "$real"})
384
+
385
+
386
+ @dataclass(frozen=True)
387
+ class _TffSortDecl:
388
+ """A parsed ``tff(name, type, S: $tType).`` sort declaration."""
389
+
390
+ name: str
391
+
392
+
393
+ @dataclass(frozen=True)
394
+ class _TffPredDecl:
395
+ """A parsed predicate type declaration: name, and each argument's
396
+ resolved sort (``None`` for ``$i``, else a capitalised kit-level sort
397
+ name -- see :func:`_resolve_type_str`)."""
398
+
399
+ name: str
400
+ arg_sorts: Tuple[Optional[str], ...]
401
+
402
+
403
+ @dataclass(frozen=True)
404
+ class _TffFuncDecl:
405
+ """A parsed function/constant type declaration (``arg_sorts == ()``
406
+ means a 0-ary function, i.e. a constant, in :func:`parse_tff_problem`'s
407
+ Signature assembly)."""
408
+
409
+ name: str
410
+ arg_sorts: Tuple[Optional[str], ...]
411
+ result_sort: Optional[str]
412
+
413
+
414
+ def _resolve_type_str(type_str: str, *, context: str) -> Optional[str]:
415
+ """Return the kit-level sort name for one TFF atomic-type string, or
416
+ ``None`` for ``"$i"`` (TPTP's default individual type -- "no sort
417
+ annotation needed", matching a plain unsorted :class:`Quantifier` /
418
+ :class:`Constant`). ``context`` (e.g. ``"a quantified variable's"``,
419
+ ``"an argument of 'p''s"``) is spliced into the error message naming
420
+ WHERE the offending type was used.
421
+
422
+ Raises:
423
+ TptpParsingError: the type is ``"$o"`` (boolean may only appear as
424
+ a PREDICATE's own overall result type -- never a variable's,
425
+ argument's, or function-result type) or an unrecognised
426
+ ``"$..."`` built-in.
427
+ NotImplementedError: the type is ``"$int"``/``"$rat"``/``"$real"``
428
+ -- TPTP's arithmetic sorts, out of scope for this TF0-only
429
+ reader (see module docstring).
430
+ """
431
+ if type_str == "$i":
432
+ return None
433
+ if type_str == "$o":
434
+ raise TptpParsingError(
435
+ f"SYNTAX_ERROR: '$o' (boolean) cannot be {context} type -- only "
436
+ "a predicate's own overall result may be $o."
437
+ )
438
+ if type_str in _ARITHMETIC_SORTS:
439
+ raise NotImplementedError(
440
+ f"TF0 reader: {context} type {type_str!r} needs TPTP's "
441
+ "arithmetic sorts ($int/$rat/$real), which are out of scope for "
442
+ "this TF0-only reader (see module docstring 'Scope')."
443
+ )
444
+ if type_str.startswith("$"):
445
+ raise TptpParsingError(
446
+ f"SYNTAX_ERROR: unsupported TPTP built-in type {type_str!r} as "
447
+ f"{context} type."
448
+ )
449
+ return _cap(type_str)
450
+
451
+
452
+ class _TptpTransformer(Transformer):
453
+ """Turn the Lark parse tree into the toolkit AST."""
454
+
455
+ # --- whole-file / statements ---
456
+ def file(self, items):
457
+ return list(items)
458
+
459
+ def stmt(self, items):
460
+ keyword = str(items[0])
461
+ if keyword == "thf":
462
+ raise TptpParsingError(_THF_REFUSAL)
463
+ if keyword not in ("fof", "cnf", "tff"):
464
+ raise TptpParsingError(
465
+ f"SYNTAX_ERROR: unsupported TPTP statement '{keyword}' "
466
+ "(only fof, cnf, and tff are supported)."
467
+ )
468
+ name, role, formula = str(items[1]), str(items[2]), items[3]
469
+ # items[4] / items[5]: the optional 4th (source) / 5th (useful_info)
470
+ # annotation fields (None when absent -- see the grammar's
471
+ # annotation_term rule). This reader models the statement's own
472
+ # FORMULA, never its provenance/derivation metadata -- accepted
473
+ # syntactically, discarded here.
474
+ return TptpFormula(name, role, formula)
475
+
476
+ def stmt_type(self, items):
477
+ """A ``tff(name, type, <decl>).`` type-declaration statement.
478
+
479
+ Returns one of :class:`_TffSortDecl` / :class:`_TffPredDecl` /
480
+ :class:`_TffFuncDecl` — never a :class:`TptpFormula`, since a type
481
+ declaration declares vocabulary, not a formula. Only
482
+ :func:`parse_tff_problem` consumes these; :func:`parse_tptp` refuses
483
+ a problem containing one (see that function). ``items[4]``/
484
+ ``items[5]`` (the optional annotation fields) are discarded exactly
485
+ like :meth:`stmt` discards them.
486
+ """
487
+ keyword, name, role, decl = str(items[0]), str(items[1]), str(items[2]), items[3]
488
+ if keyword == "thf":
489
+ raise TptpParsingError(_THF_REFUSAL)
490
+ if keyword != "tff":
491
+ raise TptpParsingError(
492
+ f"SYNTAX_ERROR: a type declaration (role 'type') is only "
493
+ f"valid inside a 'tff(...)' statement, got '{keyword}(...)'."
494
+ )
495
+ if role != "type":
496
+ raise TptpParsingError(
497
+ f"SYNTAX_ERROR: expected role 'type' for a TFF type "
498
+ f"declaration, got {role!r}."
499
+ )
500
+ return decl
501
+
502
+ def fof_name(self, items):
503
+ return str(items[0])
504
+
505
+ # --- include directives (spliced in by _resolve_includes, post-parse) ---
506
+ def include_stmt(self, items):
507
+ file_tok, names = items
508
+ selection = tuple(names) if names is not None else None
509
+ return _IncludeDirective(_functor_name(file_tok), selection)
510
+
511
+ def name_list(self, items):
512
+ return list(items)
513
+
514
+ # --- connectives ---
515
+ def iff(self, items):
516
+ return Iff(items[0], items[1])
517
+
518
+ def xor(self, items):
519
+ return Xor(items[0], items[1])
520
+
521
+ def implies(self, items):
522
+ return Implies(items[0], items[1])
523
+
524
+ def rev_implies(self, items):
525
+ # TPTP a <= b means "a if b", i.e. b => a.
526
+ return Implies(items[1], items[0])
527
+
528
+ def or_(self, items):
529
+ return Or(items[0], items[1])
530
+
531
+ def nor(self, items):
532
+ return Not(Or(items[0], items[1]))
533
+
534
+ def and_(self, items):
535
+ return And(items[0], items[1])
536
+
537
+ def nand(self, items):
538
+ return Not(And(items[0], items[1]))
539
+
540
+ def neg(self, items):
541
+ return Not(items[0])
542
+
543
+ # --- quantifiers ---
544
+ def forall(self, items):
545
+ return self._quantify("∀", items[0], items[1])
546
+
547
+ def exists(self, items):
548
+ return self._quantify("∃", items[0], items[1])
549
+
550
+ def _quantify(self, qtype, variables, body):
551
+ # Each element of `variables` is either a plain Variable (untyped,
552
+ # from a bare typed_var with no ":sort") or a (Variable, sort) pair
553
+ # (typed_var's optional sort was present) -- see typed_var below.
554
+ for var in reversed(variables):
555
+ if isinstance(var, tuple):
556
+ bound, sort = var
557
+ body = SortedQuantifier(qtype, bound, sort, body)
558
+ else:
559
+ body = Quantifier(qtype, var, body)
560
+ return body
561
+
562
+ def varlist(self, items):
563
+ return list(items)
564
+
565
+ def typed_var(self, items):
566
+ """One quantifier variable, optionally TFF-typed: ``X`` or ``X: sort``.
567
+
568
+ Returns a bare :class:`Variable` (untyped -- matches fof/cnf's
569
+ pre-existing behaviour exactly) when no ``: sort`` was given, or a
570
+ ``(Variable, sort)`` pair when one was -- :meth:`_quantify` turns the
571
+ pair into a :class:`SortedQuantifier` binder. ``$i`` resolves to
572
+ "untyped" (a bare :class:`Variable`), matching TPTP's own "no type
573
+ given" default; ``$o``/arithmetic sorts/``$tType`` are refused (see
574
+ :func:`_resolve_type_str`).
575
+ """
576
+ if len(items) == 1:
577
+ return Variable(str(items[0]).lower())
578
+ var_tok, type_str = items
579
+ sort = _resolve_type_str(type_str, context="a quantified variable's")
580
+ var = Variable(str(var_tok).lower())
581
+ return var if sort is None else (var, sort)
582
+
583
+ # --- atoms ---
584
+ def equality(self, items):
585
+ return Atom("=", [items[0], items[1]])
586
+
587
+ def disequality(self, items):
588
+ return Atom("≠", [items[0], items[1]])
589
+
590
+ def pred_app(self, items):
591
+ return Atom(_cap(_functor_name(items[0])), items[1])
592
+
593
+ def prop_atom(self, items):
594
+ return Atom(_cap(_functor_name(items[0])), [])
595
+
596
+ def dollar_atom(self, items):
597
+ word = str(items[0])
598
+ if word in _DOLLAR_PRED:
599
+ return Atom(_DOLLAR_PRED[word], items[1])
600
+ raise TptpParsingError(
601
+ f"SYNTAX_ERROR: unsupported TPTP dollar-word predicate '{word}'."
602
+ )
603
+
604
+ def bool_const(self, items):
605
+ word = str(items[0])
606
+ if word in ("$true", "$false"):
607
+ return Atom(word, [])
608
+ raise TptpParsingError(
609
+ f"SYNTAX_ERROR: unsupported TPTP dollar-word '{word}'."
610
+ )
611
+
612
+ # --- terms ---
613
+ def var(self, items):
614
+ return Variable(str(items[0]).lower())
615
+
616
+ def constant(self, items):
617
+ return Constant(_functor_name(items[0]))
618
+
619
+ def number(self, items):
620
+ return Number(_numeral_from_text(str(items[0])))
621
+
622
+ def func_app(self, items):
623
+ return Function(_functor_name(items[0]), items[1])
624
+
625
+ def dollar_func_app(self, items):
626
+ word = str(items[0])
627
+ if word in _DOLLAR_FUNC:
628
+ return Function(_DOLLAR_FUNC[word], items[1])
629
+ raise TptpParsingError(
630
+ f"SYNTAX_ERROR: unsupported TPTP dollar-word function '{word}'."
631
+ )
632
+
633
+ def termlist(self, items):
634
+ return list(items)
635
+
636
+ # --- TF0 type declarations ---
637
+ def tff_type_decl(self, items):
638
+ name_tok, shape_payload = items
639
+ name = _functor_name(name_tok)
640
+ shape, payload = shape_payload
641
+ if shape == "sort":
642
+ return _TffSortDecl(_cap(name))
643
+ if shape == "nullary":
644
+ if payload == "$o":
645
+ return _TffPredDecl(_cap(name), ())
646
+ return _TffFuncDecl(name, (), _resolve_type_str(payload, context="this symbol's"))
647
+ if shape == "mapping":
648
+ domain, result = payload
649
+ arg_sorts = tuple(
650
+ _resolve_type_str(t, context=f"an argument of {name!r}'s") for t in domain)
651
+ if result == "$o":
652
+ return _TffPredDecl(_cap(name), arg_sorts)
653
+ return _TffFuncDecl(
654
+ name, arg_sorts, _resolve_type_str(result, context=f"{name!r}'s result"))
655
+ raise AssertionError(f"tff_type_decl: unreachable shape {shape!r}") # pragma: no cover
656
+
657
+ def tff_sort_decl(self, items):
658
+ return ("sort", None)
659
+
660
+ def tff_poly_decl(self, items):
661
+ raise _Tf1PolymorphismError(_TF1_REFUSAL)
662
+
663
+ def tff_symbol_decl(self, items):
664
+ return ("mapping", items[0])
665
+
666
+ def tff_nullary_decl(self, items):
667
+ return ("nullary", items[0])
668
+
669
+ def tff_mapping_type(self, items):
670
+ domain, result = items
671
+ return (domain, result)
672
+
673
+ def domain_single(self, items):
674
+ return (items[0],)
675
+
676
+ def domain_prod(self, items):
677
+ return items[0]
678
+
679
+ def tff_xprod(self, items):
680
+ return tuple(items)
681
+
682
+ def plain_type(self, items):
683
+ return _functor_name(items[0])
684
+
685
+ def dollar_type(self, items):
686
+ return str(items[0])
687
+
688
+
689
+ _FORMULA_PARSER = Lark(_GRAMMAR, start="formula", parser="earley")
690
+ _FILE_PARSER = Lark(_GRAMMAR, start="file", parser="earley")
691
+ # The LALR(1) twins, tried first -- see "Two parsers" in the module
692
+ # docstring. Construction costs +0.06 s at import for the pair (measured),
693
+ # against 45x on every non-trivial parse. lark raises GrammarError on any
694
+ # shift/reduce or reduce/reduce collision, so these two lines are themselves
695
+ # the assertion that the grammar above is LALR(1).
696
+ _FORMULA_PARSER_FAST = Lark(_GRAMMAR, start="formula", parser="lalr")
697
+ _FILE_PARSER_FAST = Lark(_GRAMMAR, start="file", parser="lalr")
698
+ _TRANSFORMER = _TptpTransformer()
699
+
700
+ #: A DOL ``logic <Name>.<Sublogic>`` line is what HETS puts in front of every
701
+ #: ``GET /theory`` rendering (``logic TPTP.FOF``, ``logic CASL.SulFOL=``,
702
+ #: ``logic OWL.NP-sROIQx-D|Literal|...``). It is not TPTP, and neither is the
703
+ #: CASL ``%{ ... }%`` block that follows it -- TPTP's only comment forms are
704
+ #: ``%`` to end-of-line and ``/* ... */``, both already in the grammar's
705
+ #: %ignore lines above. Detected here only so the refusal can NAME it and
706
+ #: point at the stripper; the grammar is deliberately NOT widened, because a
707
+ #: reader that treated ``%{ ... }%`` as a comment would accept a CASL theory,
708
+ #: ignore its whole body and return an EMPTY formula list -- a silent empty
709
+ #: answer to a wrong-translation request.
710
+ #:
711
+ #: The WHOLE first line must be ``logic`` plus ONE DOL logic reference
712
+ #: (:data:`_HETS_LOGIC_REFERENCE`): a logic NAME, which is an identifier
713
+ #: (``TPTP``, ``CASL``, ``SoftFOL``, ``HasCASL``, ``OWL`` ...), optionally
714
+ #: followed by ``.`` and a free-form sublogic. The first character of the word
715
+ #: after ``logic`` is therefore a LETTER, never an operator, so the guard
716
+ #: cannot fire on a legitimate formula that happens to use ``logic`` as a
717
+ #: predicate or constant name: ``logic & p``, ``logic &p``, ``logic|q``,
718
+ #: ``logic =a``, ``logic != b``, ``logic <=> q``, ``logic(X)`` all keep parsing
719
+ #: exactly as before. (An earlier test, ``logic`` plus any whitespace-free
720
+ #: word, refused ``logic &p`` and ``logic =a`` -- valid formulas that parsed
721
+ #: before the pointer existed.)
722
+ #:
723
+ #: On top of the shape, :func:`_parse` consults it only AFTER both parsers have
724
+ #: failed, so by construction no text that parses is ever refused here; the
725
+ #: shape is what keeps the pointer from naming the wrong cause for a text that
726
+ #: does not.
727
+ _HETS_LOGIC_REFERENCE = r"[A-Za-z][A-Za-z0-9_]*(?:\.\S*)?"
728
+ _HETS_THEORY_HEADER = re.compile(r"logic[ \t]+" + _HETS_LOGIC_REFERENCE + r"\Z")
729
+
730
+
731
+ def _refuse_hets_theory_header(text: str) -> None:
732
+ """Refuse a HETS ``/theory`` rendering by name. Called by :func:`_parse`
733
+ once the text has FAILED to parse, never before: a text that parses is not
734
+ a Hets rendering, whatever its first line looks like."""
735
+ stripped = text.lstrip()
736
+ first_line = stripped.split("\n", 1)[0].rstrip()
737
+ if not _HETS_THEORY_HEADER.fullmatch(first_line):
738
+ return
739
+ raise TptpParsingError(
740
+ "SYNTAX_ERROR: this is not a TPTP problem but a Hets theory "
741
+ f"rendering (it begins {first_line!r}; a '%{{ ... }}%' block is CASL "
742
+ "comment syntax, not TPTP). Strip the header first with "
743
+ "unicode_logic_kit.hets.strip_hets_theory_header(text), or fetch it "
744
+ "already stripped with HetsClient.theory_tptp().")
745
+
746
+
747
+ #: What carries no syntax of its own in TPTP text: a single-quoted atom (its escapes as in the
748
+ #: grammar's ``SQ``), a ``%`` comment to the end of the line, a ``/* ... */`` block comment.
749
+ _QUOTED_OR_COMMENT = re.compile(r"'(?:\\.|[^'\\])*'|%[^\r\n]*|/\*.*?\*/", re.DOTALL)
750
+
751
+ #: A ``thf`` statement: the keyword opens the text or follows the ``.`` that ends the
752
+ #: statement before it. (A ``.`` elsewhere is the point of a numeral, and a digit follows it.)
753
+ _THF_STATEMENT = re.compile(r"(?:\A|\.)\s*thf\s*\(")
754
+
755
+ #: TF1 syntax: the type binder ``!>``, and a quantifier variable whose type is ``$tType``
756
+ #: (``![A: $tType]``). TF0 uses ``$tType`` only in ``name: $tType``, a lower-case name.
757
+ _TF1_SYNTAX = re.compile(r"!>|[\[,]\s*[A-Z][A-Za-z0-9_]*\s*:\s*\$tType")
758
+
759
+
760
+ def _refuse_out_of_scope_syntax(text: str, *, problem: bool) -> None:
761
+ """Refuse THF and TF1 by name before the first-order grammar is tried.
762
+
763
+ A THF statement has a body the first-order grammar cannot read (``p @ A``, a type
764
+ declaration ``p : $i > $o``), and TF1 writes a type variable with ``!>`` or with a
765
+ quantifier over ``$tType``; each ended in a syntax error that does not say what the
766
+ text is. The statement's KIND and the binder are found in the text with comments and
767
+ quoted atoms taken out, so neither a comment nor a quoted atom is ever refused, and
768
+ the check does not depend on the body parsing.
769
+
770
+ Args:
771
+ text: the text about to be parsed.
772
+ problem: whether ``text`` is a whole problem (statements). A bare formula has no
773
+ statement keyword: ``thf(a)`` there is the atom ``Thf(a)``.
774
+
775
+ Raises:
776
+ TptpParsingError: ``text`` has a ``thf`` statement.
777
+ _Tf1PolymorphismError: ``text`` uses ``!>`` or a ``$tType`` quantifier variable
778
+ (a :class:`TptpParsingError` and a :class:`NotImplementedError`).
779
+ """
780
+ if "thf" not in text and "!>" not in text and "$tType" not in text:
781
+ return
782
+ code = _QUOTED_OR_COMMENT.sub(" ", text)
783
+ if problem and _THF_STATEMENT.search(code):
784
+ raise TptpParsingError(_THF_REFUSAL)
785
+ if _TF1_SYNTAX.search(code):
786
+ raise _Tf1PolymorphismError(_TF1_REFUSAL)
787
+
788
+
789
+ #: Parse-tree nodes whose ``VAR`` tokens are not variables of a formula: the
790
+ #: discarded 4th/5th annotation fields of a statement (an opaque term may carry
791
+ #: one), the statement's name, and the declarations that are not formulas.
792
+ _NOT_A_FORMULA = frozenset({
793
+ "annotation_term", "annotation_term_list", "fof_name", "include_stmt",
794
+ "name_list", "stmt_type",
795
+ })
796
+
797
+
798
+ def _refuse_merged_variables(tree) -> None:
799
+ """Refuse, by name, a formula in which lower-casing turns two TPTP variables
800
+ into one.
801
+
802
+ :class:`_TptpTransformer` reads a TPTP variable as its lower-cased kit
803
+ :class:`Variable`, and a TPTP variable is case-sensitive: ``Xa`` and ``XA``
804
+ are two variables that both become ``xa``. Where that merges them the formula
805
+ is read as a DIFFERENT formula (``![Xa, XA]: p(Xa, XA)`` as ``∀xa ∀xa
806
+ P(xa, xa)``), and nothing said so. A kit variable here is a NAME, resolved to
807
+ the nearest enclosing binder of that name; TPTP resolves an occurrence to the
808
+ nearest enclosing binder of the EXACT spelling, or leaves it free. The two
809
+ readings of an occurrence therefore differ exactly when
810
+
811
+ * the nearest binder of the lower-cased name is not the nearest binder of the
812
+ exact spelling (one variable is captured by another's quantifier, or a free
813
+ variable by a binder), or
814
+ * two spellings of one lower-cased name occur free in the same formula.
815
+
816
+ Those are refused, naming both variables. Anything else reads as it always
817
+ did, byte for byte: ``(![Xa]: p(Xa)) & (![XA]: q(XA))`` has two binders that
818
+ never meet, so the kit's ``(∀xa P(xa)) ∧ (∀xa Q(xa))`` is the same formula.
819
+ Each statement of a problem is checked on its own. No renaming is invented
820
+ here: a formula that needs one is refused and the caller renames.
821
+ """
822
+ if tree.data == "file":
823
+ roots = [child for child in tree.children
824
+ if isinstance(child, Tree) and child.data == "stmt"]
825
+ else:
826
+ roots = [tree]
827
+ for root in roots:
828
+ free: Dict[str, str] = {} # lower-cased name -> first free spelling
829
+ # (node, binders); binders is None or (spelling, enclosing binders)
830
+ stack: List[Tuple[Any, Any]] = [(root, None)]
831
+ while stack:
832
+ node, binders = stack.pop()
833
+ if not isinstance(node, Tree) or node.data in _NOT_A_FORMULA:
834
+ continue
835
+ if node.data == "var":
836
+ _check_variable_occurrence(str(node.children[0]), binders, free)
837
+ elif node.data in ("forall", "exists"):
838
+ varlist, body = node.children
839
+ for typed_var in varlist.children:
840
+ binders = (str(typed_var.children[0]), binders)
841
+ stack.append((body, binders))
842
+ else:
843
+ for child in reversed(node.children):
844
+ stack.append((child, binders))
845
+
846
+
847
+ def _check_variable_occurrence(spelling: str, binders: Any, free: Dict[str, str]) -> None:
848
+ """One occurrence of the TPTP variable ``spelling`` under ``binders`` — see
849
+ :func:`_refuse_merged_variables`."""
850
+ lowered = spelling.lower()
851
+ exact = by_name = None
852
+ scope = binders
853
+ while scope is not None and (exact is None or by_name is None):
854
+ if exact is None and scope[0] == spelling:
855
+ exact = scope
856
+ if by_name is None and scope[0].lower() == lowered:
857
+ by_name = scope
858
+ scope = scope[1]
859
+ if by_name is not None and by_name is not exact:
860
+ _refuse_variable_pair(by_name[0], spelling, lowered)
861
+ if by_name is None:
862
+ first = free.setdefault(lowered, spelling)
863
+ if first != spelling:
864
+ _refuse_variable_pair(first, spelling, lowered)
865
+
866
+
867
+ def _refuse_variable_pair(one: str, other: str, lowered: str) -> None:
868
+ raise TptpParsingError(
869
+ f"SYNTAX_ERROR: the TPTP variables {one!r} and {other!r} are different "
870
+ f"variables that differ only by letter case, and this reader turns a "
871
+ f"TPTP variable into a kit variable by lower-casing it: both would "
872
+ f"become {lowered!r}, and the formula would be read with one variable "
873
+ f"where TPTP has two. Rename one of them so that they differ by more "
874
+ f"than letter case.")
875
+
876
+
877
+ def _parse(text: str, parser: Lark, what: str, *, fast: Optional[Lark] = None):
878
+ """Parse + transform with unified error handling (unwrapping lark's VisitError).
879
+
880
+ ``fast`` is the LALR(1) twin of ``parser``; it is tried first and Earley
881
+ takes over on ``UnexpectedInput``, so no text that parsed before stops
882
+ parsing and every syntax-error message is Earley's, unchanged. Only the
883
+ PARSE is retried — see "Two parsers" in the module docstring.
884
+
885
+ A HETS theory rendering is named only once BOTH parsers have refused the
886
+ text (see :func:`_refuse_hets_theory_header`): the pointer replaces a syntax
887
+ error with a better one, and can never turn a text that parses into a
888
+ refusal.
889
+ """
890
+ _refuse_out_of_scope_syntax(text, problem=what == "problem")
891
+ tree = None
892
+ if fast is not None:
893
+ try:
894
+ tree = fast.parse(text)
895
+ except UnexpectedInput:
896
+ tree = None
897
+ if tree is None:
898
+ try:
899
+ tree = parser.parse(text)
900
+ except TptpParsingError:
901
+ raise
902
+ except Exception as exc:
903
+ _refuse_hets_theory_header(text)
904
+ raise TptpParsingError(
905
+ f"SYNTAX_ERROR: could not parse TPTP {what}: {exc}")
906
+ _refuse_merged_variables(tree)
907
+ try:
908
+ return _TRANSFORMER.transform(tree)
909
+ except VisitError as exc:
910
+ original = exc.orig_exc
911
+ # NotImplementedError alongside ParsingError: the TF0-scope refusals
912
+ # (TF1 polymorphism, $int/$rat/$real -- see _resolve_type_str /
913
+ # tff_poly_decl) are meant to surface as NotImplementedError, not get
914
+ # folded into a generic SYNTAX_ERROR string, so its type must survive
915
+ # this unwrap exactly like ParsingError's already does.
916
+ if isinstance(original, (ParsingError, NotImplementedError)):
917
+ raise original
918
+ raise TptpParsingError(f"SYNTAX_ERROR: in TPTP {what}: {original}")
919
+
920
+
921
+ # =============================================================================
922
+ # Include resolution — shared by parse_tptp / parse_tptp_problem /
923
+ # parse_tff_problem and their load_* siblings (see the module docstring's
924
+ # "Includes" section).
925
+ # =============================================================================
926
+
927
+ def _locate_include(file_name: str, base_dir: str, search_paths) -> str:
928
+ """Resolve one ``include(file_name)`` directive to an existing file path.
929
+
930
+ Tried in order: (1) ``base_dir/file_name`` (relative to the file
931
+ containing the include directive itself), (2) each root in
932
+ ``search_paths`` joined with ``file_name``, in order, then (3)
933
+ ``os.environ["TPTP"]`` joined with ``file_name``, if that environment
934
+ variable is set — the de facto TPTP-library convention (an include like
935
+ ``include('Axioms/SET001+0.ax')`` is root-relative, not relative to
936
+ whatever problem file referred to it).
937
+
938
+ Raises:
939
+ TptpParsingError: none of the tried candidates exist — names the
940
+ include's path and every candidate location tried.
941
+ """
942
+ roots = [base_dir, *search_paths]
943
+ tptp_env = os.environ.get("TPTP")
944
+ if tptp_env:
945
+ roots.append(tptp_env)
946
+ tried = []
947
+ for root in roots:
948
+ candidate = os.path.join(root, file_name)
949
+ tried.append(candidate)
950
+ if os.path.isfile(candidate):
951
+ return candidate
952
+ raise TptpParsingError(
953
+ f"SYNTAX_ERROR: include({file_name!r}) could not be found -- tried: "
954
+ + ", ".join(repr(t) for t in tried) + "."
955
+ )
956
+
957
+
958
+ def _apply_selection(items: list, selection: Tuple[str, ...], file_name: str) -> list:
959
+ """Filter an included file's already-resolved ``items`` down to
960
+ ``selection``, by each :class:`TptpFormula`'s own TPTP statement name,
961
+ in the order ``selection`` names them (not the file's own order).
962
+
963
+ Raises:
964
+ TptpParsingError: ``items`` contains anything other than
965
+ :class:`TptpFormula` records (a TF0 type declaration — its own
966
+ statement name is not tracked separately from the symbol it
967
+ declares, see :class:`_TffSortDecl`/:class:`_TffPredDecl`/
968
+ :class:`_TffFuncDecl`, so a selection list cannot be matched
969
+ against it), or ``selection`` names something absent from the
970
+ included file.
971
+ """
972
+ non_formula = [item for item in items if not isinstance(item, TptpFormula)]
973
+ if non_formula:
974
+ raise TptpParsingError(
975
+ f"SYNTAX_ERROR: include({file_name!r}, [...]) cannot use a "
976
+ "selection list on a file that declares TFF vocabulary (a "
977
+ "'tff(name, type, ...).' statement) -- its own statement name "
978
+ "is not tracked separately from the symbol it declares, so a "
979
+ "selection list cannot be matched against it; include the "
980
+ "whole file (drop the selection list) instead."
981
+ )
982
+ by_name = {}
983
+ for formula in items:
984
+ by_name.setdefault(formula.name, formula)
985
+ selected = []
986
+ for name in selection:
987
+ if name not in by_name:
988
+ raise TptpParsingError(
989
+ f"SYNTAX_ERROR: include({file_name!r}, [...]) selects "
990
+ f"{name!r}, which is not a formula name in {file_name!r} "
991
+ f"(available: {sorted(by_name)!r})."
992
+ )
993
+ selected.append(by_name[name])
994
+ return selected
995
+
996
+
997
+ def _resolve_includes(items: list, base_dir: Optional[str], search_paths,
998
+ chain: Tuple[Tuple[str, str], ...]) -> list:
999
+ """Recursively splice every :class:`_IncludeDirective` in ``items`` with
1000
+ the (itself include-resolved) statements of the file it names, in place
1001
+ — preserving surrounding statement order.
1002
+
1003
+ ``chain`` is the tuple of ``(name_as_written, real_path)`` pairs for
1004
+ every include currently "open" (the stack of files that led here),
1005
+ used both to detect a cycle (a repeated ``real_path``) and to name the
1006
+ full trail in the resulting error. :func:`_load_and_resolve` seeds it
1007
+ with the loaded file's own path; a bare-text :func:`parse_tptp` call
1008
+ starts it empty.
1009
+
1010
+ Raises:
1011
+ TptpParsingError: an include is encountered with ``base_dir=None``
1012
+ (no including file's directory is known — see the module
1013
+ docstring), the named file cannot be found (see
1014
+ :func:`_locate_include`), or the chain cycles back to a file
1015
+ already open.
1016
+ """
1017
+ resolved = []
1018
+ for item in items:
1019
+ if not isinstance(item, _IncludeDirective):
1020
+ resolved.append(item)
1021
+ continue
1022
+ if base_dir is None:
1023
+ raise TptpParsingError(
1024
+ f"SYNTAX_ERROR: include({item.file_name!r}) cannot be "
1025
+ "resolved -- no including file's directory is known "
1026
+ "(base_dir=None, the default, for bare text); read the "
1027
+ "problem via load_tptp / load_tptp_problem / "
1028
+ "load_tff_problem, or pass base_dir explicitly."
1029
+ )
1030
+ path = _locate_include(item.file_name, base_dir, search_paths)
1031
+ real = os.path.realpath(path)
1032
+ if any(real == seen_real for _seen_name, seen_real in chain):
1033
+ trail = " -> ".join(repr(name) for name, _real in chain)
1034
+ raise TptpParsingError(
1035
+ f"SYNTAX_ERROR: circular include: {trail} -> "
1036
+ f"{item.file_name!r}."
1037
+ )
1038
+ with open(path, "r", encoding="utf-8") as handle:
1039
+ sub_text = handle.read()
1040
+ sub_items = _parse(sub_text, _FILE_PARSER, "problem",
1041
+ fast=_FILE_PARSER_FAST)
1042
+ sub_resolved = _resolve_includes(
1043
+ sub_items, os.path.dirname(path), search_paths,
1044
+ chain + ((item.file_name, real),))
1045
+ if item.selection is not None:
1046
+ sub_resolved = _apply_selection(sub_resolved, item.selection, item.file_name)
1047
+ resolved.extend(sub_resolved)
1048
+ return resolved
1049
+
1050
+
1051
+ def _finalize_formulas(items: list) -> list:
1052
+ """Confirm every item in an include-resolved ``items`` list is a
1053
+ :class:`TptpFormula` — the shared tail of :func:`parse_tptp` /
1054
+ :func:`load_tptp` / :func:`parse_tptp_problem` / :func:`load_tptp_problem`,
1055
+ none of which can represent a TF0 type declaration."""
1056
+ for item in items:
1057
+ if not isinstance(item, TptpFormula):
1058
+ raise TptpParsingError(
1059
+ "SYNTAX_ERROR: this problem contains a TFF type declaration "
1060
+ "('tff(name, type, ...).'), which parse_tptp/load_tptp cannot "
1061
+ "represent as a TptpFormula — use parse_tff_problem() instead, "
1062
+ "which returns the declared Signature alongside the formulas."
1063
+ )
1064
+ return items
1065
+
1066
+
1067
+ def _load_and_resolve(path: str, search_paths) -> Tuple[list, str]:
1068
+ """Read ``path``, parse it, and resolve its includes with the cycle
1069
+ ``chain`` (see :func:`_resolve_includes`) seeded by ``path`` itself —
1070
+ so a chain that loops back to the very file
1071
+ :func:`load_tptp`/:func:`load_tptp_problem`/:func:`load_tff_problem`
1072
+ started from is caught, not only a cycle confined to files reached
1073
+ purely via nested includes.
1074
+
1075
+ Returns:
1076
+ ``(items, text)`` — ``items`` fully include-resolved (no
1077
+ :class:`_IncludeDirective` survives), ``text`` the loaded file's own
1078
+ raw contents (for :func:`load_tptp_problem`'s header scan, which
1079
+ reads only the top-level file's ``%`` comments, never an included
1080
+ file's).
1081
+ """
1082
+ real_path = os.path.realpath(path)
1083
+ with open(path, "r", encoding="utf-8") as handle:
1084
+ text = handle.read()
1085
+ items = _parse(text, _FILE_PARSER, "problem", fast=_FILE_PARSER_FAST)
1086
+ items = _resolve_includes(
1087
+ items, os.path.dirname(path), search_paths, ((path, real_path),))
1088
+ return items, text
1089
+
1090
+
1091
+ def parse_tptp_formula(text: str) -> Node:
1092
+ """Parse a single bare TPTP FOF/CNF formula (no ``fof(...)`` wrapper) into a Node.
1093
+
1094
+ Args:
1095
+ text: a TPTP formula, e.g. ``"![X]: (p(X) => q(X))"``.
1096
+
1097
+ Returns:
1098
+ The formula as a toolkit :class:`Node`.
1099
+
1100
+ Raises:
1101
+ ParsingError: if ``text`` is not a well-formed TPTP formula.
1102
+ """
1103
+ return _parse(text, _FORMULA_PARSER, "formula",
1104
+ fast=_FORMULA_PARSER_FAST)
1105
+
1106
+
1107
+ def parse_tptp(text: str, *, base_dir: Optional[str] = None, search_paths=()) -> list:
1108
+ """Parse a whole TPTP problem into a list of :class:`TptpFormula` records.
1109
+
1110
+ Args:
1111
+ text: the contents of a TPTP problem — one or more ``fof(...)`` /
1112
+ ``cnf(...)`` / ``tff(...)`` statements, and/or ``include(...)``
1113
+ directives (``%`` line comments and ``/* */`` block comments are
1114
+ ignored). A ``tff`` AXIOM/CONJECTURE/... statement is accepted
1115
+ exactly like ``fof`` (its typed quantifiers build
1116
+ :class:`~unicode_logic_kit.fol.nodes.SortedQuantifier`), but a
1117
+ ``tff(name, type, ...).`` TYPE DECLARATION is not (see
1118
+ ``Raises`` below).
1119
+ base_dir: the directory an ``include(...)`` in ``text`` resolves
1120
+ relative to first. ``None`` (the default) means "no including
1121
+ file's directory is known" — an ``include`` then raises (see
1122
+ ``Raises``); :func:`load_tptp` sets this to the loaded file's
1123
+ own directory automatically. See the module docstring's
1124
+ "Includes" section for the full resolution order.
1125
+ search_paths: further roots tried, in order, after ``base_dir``,
1126
+ for an ``include`` not found relative to it (plus
1127
+ ``os.environ["TPTP"]``, if set) — see "Includes".
1128
+
1129
+ Returns:
1130
+ A list of :class:`TptpFormula` ``(name, role, formula)`` in source
1131
+ order, with every ``include`` spliced in at its own position.
1132
+
1133
+ Raises:
1134
+ ParsingError: if the text is not a well-formed TPTP problem, if it
1135
+ contains a ``tff(name, type, ...).`` type declaration — this
1136
+ function has no ``TptpFormula`` shape to return a type
1137
+ declaration as; use :func:`parse_tff_problem` instead, which
1138
+ returns the declared :class:`~unicode_logic_kit.fol.signature
1139
+ .Signature` alongside the formulas — or if an ``include``
1140
+ cannot be resolved (``base_dir=None``, a missing file, or a
1141
+ circular chain; see "Includes").
1142
+ """
1143
+ items = _parse(text, _FILE_PARSER, "problem", fast=_FILE_PARSER_FAST)
1144
+ items = _resolve_includes(items, base_dir, search_paths, ())
1145
+ return _finalize_formulas(items)
1146
+
1147
+
1148
+ def load_tptp(path: str, *, search_paths=()) -> list:
1149
+ """Read a TPTP problem file and :func:`parse_tptp` its contents.
1150
+
1151
+ Args:
1152
+ path: path to a ``.p`` / ``.tptp`` file. Every ``include(...)`` it
1153
+ contains resolves relative to ``path``'s own directory first
1154
+ (then ``search_paths`` — see :func:`parse_tptp`), and a cycle
1155
+ back to ``path`` itself is caught, not only one confined to
1156
+ files reached purely via nested includes.
1157
+ search_paths: see :func:`parse_tptp`.
1158
+
1159
+ Returns:
1160
+ A list of :class:`TptpFormula` records.
1161
+ """
1162
+ items, _text = _load_and_resolve(path, search_paths)
1163
+ return _finalize_formulas(items)
1164
+
1165
+
1166
+ def _conflict(kind: str, name: str, existing, new) -> TptpParsingError:
1167
+ return TptpParsingError(
1168
+ f"SYNTAX_ERROR: {kind} '{name}' is declared more than once with "
1169
+ f"different types ({existing!r} vs {new!r})."
1170
+ )
1171
+
1172
+
1173
+ def _build_signature_and_formulas(items: list) -> Tuple[Signature, List[TptpFormula]]:
1174
+ """The shared, include-resolution-independent guts of
1175
+ :func:`parse_tff_problem` / :func:`load_tff_problem`: split an already
1176
+ include-resolved ``items`` list into a
1177
+ :class:`~unicode_logic_kit.fol.signature.Signature` and promoted formulas.
1178
+ See :func:`parse_tff_problem` for the full contract."""
1179
+ decls = [i for i in items if not isinstance(i, TptpFormula)]
1180
+ formulas = [i for i in items if isinstance(i, TptpFormula)]
1181
+
1182
+ sorts: set = set()
1183
+ predicates: Dict[str, PredicateDecl] = {}
1184
+ functions: Dict[str, FunctionDecl] = {}
1185
+ constants: Dict[str, ConstantDecl] = {}
1186
+ const_sorts: Dict[str, str] = {}
1187
+
1188
+ for d in decls:
1189
+ if isinstance(d, _TffSortDecl):
1190
+ sorts.add(d.name)
1191
+ elif isinstance(d, _TffPredDecl):
1192
+ new_decl = PredicateDecl(d.name, len(d.arg_sorts), d.arg_sorts)
1193
+ existing = predicates.get(d.name)
1194
+ if existing is not None and existing != new_decl:
1195
+ raise _conflict("predicate", d.name, existing, new_decl)
1196
+ predicates[d.name] = new_decl
1197
+ elif isinstance(d, _TffFuncDecl):
1198
+ if not d.arg_sorts:
1199
+ new_const = ConstantDecl(d.name, d.result_sort)
1200
+ existing_const = constants.get(d.name)
1201
+ if existing_const is not None and existing_const != new_const:
1202
+ raise _conflict("constant", d.name, existing_const, new_const)
1203
+ constants[d.name] = new_const
1204
+ if d.result_sort is not None:
1205
+ const_sorts[d.name] = d.result_sort
1206
+ else:
1207
+ new_func = FunctionDecl(d.name, len(d.arg_sorts), d.arg_sorts, d.result_sort)
1208
+ existing_func = functions.get(d.name)
1209
+ if existing_func is not None and existing_func != new_func:
1210
+ raise _conflict("function", d.name, existing_func, new_func)
1211
+ functions[d.name] = new_func
1212
+ else: # pragma: no cover -- file/stmt_type can only produce the three above
1213
+ raise AssertionError(f"parse_tff_problem: unreachable decl type {type(d).__name__!r}")
1214
+
1215
+ for decl in predicates.values():
1216
+ sorts.update(s for s in decl.arg_sorts if s is not None)
1217
+ for decl in functions.values():
1218
+ sorts.update(s for s in decl.arg_sorts if s is not None)
1219
+ if decl.result_sort is not None:
1220
+ sorts.add(decl.result_sort)
1221
+ for decl in constants.values():
1222
+ if decl.sort is not None:
1223
+ sorts.add(decl.sort)
1224
+ for f in formulas:
1225
+ for n in f.formula.walk():
1226
+ if isinstance(n, SortedQuantifier):
1227
+ sorts.add(n.sort)
1228
+
1229
+ signature = Signature(predicates=predicates, functions=functions,
1230
+ constants=constants, sorts=frozenset(sorts))
1231
+
1232
+ def _promote(node: Node) -> Node:
1233
+ node = node.map_children(_promote)
1234
+ if isinstance(node, Constant) and node.name in const_sorts:
1235
+ return SortedConstant(node.name, const_sorts[node.name])
1236
+ return node
1237
+
1238
+ promoted = [TptpFormula(f.name, f.role, _promote(f.formula)) for f in formulas]
1239
+ return signature, promoted
1240
+
1241
+
1242
+ def parse_tff_problem(
1243
+ text: str, *, base_dir: Optional[str] = None, search_paths=()
1244
+ ) -> Tuple[Signature, List[TptpFormula]]:
1245
+ """Parse a whole TF0 (monomorphic typed) TPTP ``tff`` problem.
1246
+
1247
+ The typed sibling of :func:`parse_tptp`: reads every ``tff(name, type,
1248
+ ...).`` TYPE DECLARATION into a :class:`~unicode_logic_kit.fol.signature
1249
+ .Signature` (one entry per declared sort/predicate/function/constant —
1250
+ an argument or result type of ``$i`` resolves to ``None`` — "no concrete
1251
+ sort", TPTP's own default — everything else to the corresponding
1252
+ capitalised kit-level sort name), and every other statement
1253
+ (``axiom``/``conjecture``/...) into a :class:`TptpFormula` exactly like
1254
+ :func:`parse_tptp` does, EXTRA step: any bare
1255
+ :class:`~unicode_logic_kit.fol.nodes.Constant` occurrence in a formula
1256
+ whose name was declared with a concrete sort is promoted to a
1257
+ :class:`~unicode_logic_kit.fol.nodes.SortedConstant` — a TFF formula BODY
1258
+ carries no inline sort annotation for a constant (only a bound variable
1259
+ does, via ``X: sort``), so this is the only place that information can
1260
+ be recovered and reattached to the AST.
1261
+
1262
+ ``include(...)`` directives are resolved exactly like :func:`parse_tptp`
1263
+ (splicing recursively, ``base_dir``/``search_paths`` work the same way —
1264
+ see the module docstring's "Includes" section) — WITH ONE RESTRICTION: a
1265
+ ``include('path', [name1, ...])`` selection list can only be applied to
1266
+ an included file that is itself pure ``fof``/``cnf``/``tff`` FORMULAS,
1267
+ never one that also declares TFF vocabulary (a ``tff(name, type,
1268
+ ...).`` statement) — a type declaration's own statement name is not
1269
+ tracked separately from the symbol it declares (see
1270
+ :class:`_TffSortDecl`/:class:`_TffPredDecl`/:class:`_TffFuncDecl`), so a
1271
+ selection list cannot be matched against it; that combination raises
1272
+ (see ``Raises``). A *whole-file* include (no selection list) always
1273
+ works, decls and formulas alike.
1274
+
1275
+ Args:
1276
+ text: the contents of a TF0 TPTP problem — a mix of
1277
+ ``tff(name, type, Sym: Type).`` declarations,
1278
+ ``tff(name, axiom|conjecture|..., <formula>).`` statements, and
1279
+ ``include(...)`` directives (``fof``/``cnf`` statements may also
1280
+ appear; their formulas are included unchanged, exactly as
1281
+ :func:`parse_tptp` would read them).
1282
+ base_dir: see :func:`parse_tptp`; :func:`load_tff_problem` sets this
1283
+ automatically.
1284
+ search_paths: see :func:`parse_tptp`.
1285
+
1286
+ Returns:
1287
+ ``(signature, formulas)`` — ``formulas`` in source order (type
1288
+ declarations are consumed into ``signature``, not returned as
1289
+ formulas). ``signature.sorts`` also includes every sort named by a
1290
+ :class:`~unicode_logic_kit.fol.nodes.SortedQuantifier` occurring in
1291
+ the formulas even if that sort was never separately declared with
1292
+ its own ``$tType`` statement (real TF0 files always do, but this
1293
+ keeps the reader lenient rather than dropping information).
1294
+
1295
+ Raises:
1296
+ ParsingError: the text is not a well-formed TPTP problem, the same
1297
+ symbol is declared twice with two DIFFERENT types, or an
1298
+ ``include`` cannot be resolved (unresolvable path, circular
1299
+ chain, or a selection list combined with TFF vocabulary, all as
1300
+ described above).
1301
+ NotImplementedError: a type declaration uses TF1 polymorphism
1302
+ (``!> [...] : ...``) or one of TPTP's arithmetic sorts
1303
+ (``$int``/``$rat``/``$real``) — see :func:`_resolve_type_str` /
1304
+ ``_TptpTransformer.tff_poly_decl``.
1305
+ """
1306
+ items = _parse(text, _FILE_PARSER, "problem", fast=_FILE_PARSER_FAST)
1307
+ items = _resolve_includes(items, base_dir, search_paths, ())
1308
+ return _build_signature_and_formulas(items)
1309
+
1310
+
1311
+ def load_tff_problem(path: str, *, search_paths=()) -> Tuple[Signature, List[TptpFormula]]:
1312
+ """Read a TF0 TPTP problem file and :func:`parse_tff_problem` its
1313
+ contents, resolving ``include(...)`` directives exactly like
1314
+ :func:`load_tptp` (``base_dir`` set to ``path``'s own directory, cycle
1315
+ detection seeded with ``path`` itself — see :func:`load_tptp`)."""
1316
+ items, _text = _load_and_resolve(path, search_paths)
1317
+ return _build_signature_and_formulas(items)
1318
+
1319
+
1320
+ # ---------------------------------------------------------------------------
1321
+ # TPTP header metadata
1322
+ # ---------------------------------------------------------------------------
1323
+ #
1324
+ # TPTP problem files carry a standardised block of '%' comment lines near the top
1325
+ # recording the file's provenance and, for problems drawn from the TPTP library,
1326
+ # its ground-truth verdict and empirical difficulty rating, e.g.:
1327
+ #
1328
+ # % File : PUZ001-1 : TPTP v8.1.0. Released v1.0.0.
1329
+ # % Domain : Puzzles
1330
+ # % Problem : Dreadbury Mansion
1331
+ # % Status : Theorem
1332
+ # % Rating : 0.43 v8.1.0, 0.36 v7.4.0
1333
+ #
1334
+ # _GRAMMAR above ``%``-ignores every comment, so none of this ever reaches the Lark
1335
+ # parser or the TptpFormula records built from it. The functions below recover it by
1336
+ # a separate, deterministic line scan of the *raw* text, run independently of (and
1337
+ # before) the grammar-based formula parse, then hand formula parsing off unchanged to
1338
+ # :func:`parse_tptp` — this extends the module's contract rather than altering it (see
1339
+ # the regression tests pinning parse_tptp / parse_tptp_formula / load_tptp / TptpFormula
1340
+ # unchanged, in tests/test_tptp_header.py).
1341
+
1342
+ # A header field line: optional leading whitespace, '%', optional whitespace, one of
1343
+ # the five recognised TPTP field names (matched case-sensitively, per the TPTP
1344
+ # standard — a lowercase "% status : ..." line is not a field line), optional
1345
+ # whitespace, ':', optional whitespace, then the rest of the line as the raw value.
1346
+ _HEADER_FIELD_RE = re.compile(
1347
+ r"^\s*%\s*(File|Domain|Problem|Status|Rating)\s*:\s*(.*)$"
1348
+ )
1349
+ # The first float-looking token anywhere in a Rating value, e.g. "0.43" out of
1350
+ # "0.43 v8.1.0, 0.36 v7.4.0" (a version tag like "v8.1.0" is not itself a match
1351
+ # candidate since it lacks a leading digit, but even if it were, "0.43" still wins
1352
+ # because re.search returns the leftmost match and it appears first in the string).
1353
+ _RATING_RE = re.compile(r"[-+]?[0-9]+\.[0-9]+")
1354
+
1355
+
1356
+ @dataclass(frozen=True)
1357
+ class TptpHeader:
1358
+ """Metadata recovered from a TPTP problem file's ``%`` comment header.
1359
+
1360
+ ``status`` / ``rating`` / ``domain`` / ``problem`` / ``file`` are ``None`` when
1361
+ the corresponding ``% <Field> : ...`` line is absent (or, for ``status``/
1362
+ ``rating``, present but unparseable — see :func:`_parse_tptp_header`).
1363
+ ``comments`` always holds *every* raw ``%`` line verbatim (stripped only of its
1364
+ trailing newline), in source order, so nothing is lost even for header lines this
1365
+ reader does not specifically interpret (``% Version``, ``% Refs``, banner lines
1366
+ of dashes, ...).
1367
+ """
1368
+
1369
+ status: Optional[str]
1370
+ rating: Optional[float]
1371
+ domain: Optional[str]
1372
+ problem: Optional[str]
1373
+ file: Optional[str]
1374
+ comments: Tuple[str, ...]
1375
+
1376
+
1377
+ @dataclass(frozen=True)
1378
+ class TptpProblem:
1379
+ """A whole TPTP problem: its parsed ``formulas`` plus the recovered ``header``."""
1380
+
1381
+ formulas: Tuple[TptpFormula, ...]
1382
+ header: TptpHeader
1383
+
1384
+
1385
+ def _parse_tptp_header(text: str) -> TptpHeader:
1386
+ """Scan raw TPTP text for its ``%`` header block, independent of the grammar.
1387
+
1388
+ Every line whose first non-whitespace character is ``%`` is collected verbatim
1389
+ (trailing newline stripped) into ``comments``, in source order. Among those, a
1390
+ line matching :data:`_HEADER_FIELD_RE` (``% <Field> : <value>``, field names
1391
+ matched case-sensitively, whitespace around ``%``/the field name/``:``
1392
+ unconstrained) populates the matching :class:`TptpHeader` attribute — the
1393
+ *first* occurrence of each field wins; later repeats of the same field are still
1394
+ recorded in ``comments`` but do not overwrite it.
1395
+
1396
+ Field values are interpreted as follows:
1397
+
1398
+ - ``Status``: the first whitespace-delimited token of the value (e.g.
1399
+ ``"Theorem"`` out of ``"Theorem"``, or ``"CounterSatisfiable"``), kept as a
1400
+ plain string — not validated against a fixed vocabulary. ``None`` if the value
1401
+ has no token (e.g. it is empty).
1402
+ - ``Rating``: the first float literal found anywhere in the value (e.g. ``0.43``
1403
+ out of ``"0.43 v8.1.0, 0.36 v7.4.0"``); ``None`` if none is found (e.g. the
1404
+ value is ``"?"``).
1405
+ - ``File`` / ``Domain`` / ``Problem``: the value, whitespace-trimmed, verbatim
1406
+ (further ``:`` characters inside the value, e.g. a ``File`` value's own
1407
+ ``name : version`` sub-structure, are kept as literal text).
1408
+
1409
+ Args:
1410
+ text: the raw contents of a TPTP problem file (or any TPTP text).
1411
+
1412
+ Returns:
1413
+ A :class:`TptpHeader`; every field is ``None`` and ``comments`` is ``()`` if
1414
+ the text has no ``%`` lines at all.
1415
+ """
1416
+ comments = []
1417
+ fields = {}
1418
+ for line in text.splitlines():
1419
+ if not line.lstrip().startswith("%"):
1420
+ continue
1421
+ comments.append(line)
1422
+ match = _HEADER_FIELD_RE.match(line)
1423
+ if not match:
1424
+ continue
1425
+ field, value = match.group(1), match.group(2)
1426
+ if field not in fields:
1427
+ fields[field] = value
1428
+
1429
+ status_raw = fields.get("Status")
1430
+ status_tokens = status_raw.split() if status_raw is not None else []
1431
+ status = status_tokens[0] if status_tokens else None
1432
+
1433
+ rating_raw = fields.get("Rating")
1434
+ rating_match = _RATING_RE.search(rating_raw) if rating_raw is not None else None
1435
+ rating = float(rating_match.group(0)) if rating_match else None
1436
+
1437
+ domain = fields.get("Domain")
1438
+ problem = fields.get("Problem")
1439
+ file_ = fields.get("File")
1440
+
1441
+ return TptpHeader(
1442
+ status=status,
1443
+ rating=rating,
1444
+ domain=domain.strip() if domain is not None else None,
1445
+ problem=problem.strip() if problem is not None else None,
1446
+ file=file_.strip() if file_ is not None else None,
1447
+ comments=tuple(comments),
1448
+ )
1449
+
1450
+
1451
+ def parse_tptp_problem(
1452
+ text: str, *, base_dir: Optional[str] = None, search_paths=()
1453
+ ) -> TptpProblem:
1454
+ """Parse a whole TPTP problem into its formulas *and* its header metadata.
1455
+
1456
+ Combines :func:`parse_tptp` (formula parsing, delegated to unchanged) with an
1457
+ independent raw-text scan for the standard ``%`` header block (see
1458
+ :func:`_parse_tptp_header`) — the two do not interact, so ``formulas`` here is
1459
+ exactly what :func:`parse_tptp` returns for the same ``text``/``base_dir``/
1460
+ ``search_paths`` (same order, same :class:`TptpFormula` records — every
1461
+ ``include`` spliced in), just wrapped in a tuple alongside the header.
1462
+ The header scan itself only ever reads ``text``'s OWN ``%`` lines, never
1463
+ an included file's.
1464
+
1465
+ Args:
1466
+ text: the contents of a TPTP problem file.
1467
+ base_dir: see :func:`parse_tptp`; :func:`load_tptp_problem` sets
1468
+ this automatically.
1469
+ search_paths: see :func:`parse_tptp`.
1470
+
1471
+ Returns:
1472
+ A :class:`TptpProblem` with ``formulas`` (in source order) and ``header``.
1473
+
1474
+ Raises:
1475
+ ParsingError: if the text is not a well-formed TPTP problem, or an
1476
+ ``include`` cannot be resolved (same conditions as
1477
+ :func:`parse_tptp`; the header scan alone never raises).
1478
+ """
1479
+ return TptpProblem(
1480
+ formulas=tuple(parse_tptp(text, base_dir=base_dir, search_paths=search_paths)),
1481
+ header=_parse_tptp_header(text),
1482
+ )
1483
+
1484
+
1485
+ def load_tptp_problem(path: str, *, search_paths=()) -> TptpProblem:
1486
+ """Read a TPTP problem file and :func:`parse_tptp_problem` its contents.
1487
+
1488
+ Mirrors :func:`load_tptp`'s file handling (whole-file read, UTF-8,
1489
+ ``base_dir``/cycle-chain seeded from ``path`` itself — see
1490
+ :func:`load_tptp`).
1491
+
1492
+ Args:
1493
+ path: path to a ``.p`` / ``.tptp`` file.
1494
+ search_paths: see :func:`parse_tptp`.
1495
+
1496
+ Returns:
1497
+ A :class:`TptpProblem`.
1498
+ """
1499
+ items, text = _load_and_resolve(path, search_paths)
1500
+ return TptpProblem(
1501
+ formulas=tuple(_finalize_formulas(items)),
1502
+ header=_parse_tptp_header(text),
1503
+ )