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,551 @@
1
+ """One TPTP word is one kit symbol: the shared collision check and the render guard.
2
+
3
+ ``Node.to_tptp`` writes a predicate, function or constant under its kit name with
4
+ the FIRST character folded to lower-case (:func:`~unicode_logic_kit.fol._fol_nodes
5
+ .tptp_fold_first_letter`), so two DISTINCT kit names of one kind (``gaseous`` and
6
+ ``Gaseous``; ``Foo`` and ``foo``; ``Bar`` and ``bar``) are written as one word.
7
+ Rendered inside one formula that turns a non-theorem into a tautology:
8
+ ``p(gaseous) <=> p(Gaseous)`` is written ``(p(gaseous) <=> p(gaseous))``.
9
+
10
+ This module is the ONE implementation of the check that refuses it. Two callers
11
+ share it:
12
+
13
+ * the problem writers (:mod:`unicode_logic_kit.atp._tptp_problem` and the TFF
14
+ writers), which check every premise and the conclusion TOGETHER through
15
+ :func:`check_no_symbol_collisions`; and
16
+ * every ``Node.to_tptp()`` call, through the guard below, which checks the ONE
17
+ formula it renders.
18
+
19
+ **What is refused.** Every case below is two kit symbols written as one word, or
20
+ a word no prover reads; the message names both symbols (or the one name) and the
21
+ word:
22
+
23
+ * two DISTINCT names of one namespace that fold to one word (the case above);
24
+ * an arithmetic or comparison symbol and a user symbol that is written like it:
25
+ ``+`` is written ``$sum``, so a function that is itself named ``$sum`` is one
26
+ word with it (``<`` is ``$less``, ``=`` is ``=``, and so on);
27
+ * a number and a constant spelled like it: ``Number(1)`` and ``Constant('1')`` are
28
+ both written ``1``;
29
+ * two VARIABLES that are written as one TPTP variable. A variable is written as
30
+ the upper-case of its name, so ``x`` and ``X`` (and ``ı`` and ``i``) are one
31
+ variable and ``∀x ∃X R(x, X)`` would be written ``![X]: ?[X]: r(X,X)``. The
32
+ scope of a variable is one formula, so the writers check this per formula;
33
+ * a predicate, function or constant name that is written as something that is not
34
+ a TPTP word. An unquoted TPTP name is a lower-case letter followed by letters,
35
+ digits and underscores (``has-part``, ``2008SummerOlympics``, ``_x`` and a
36
+ non-ASCII predicate name are not), and the text of such a rendering is rejected
37
+ by Vampire and by E. A name is refused, never written as it is. A name that is
38
+ written with a leading ``$`` (``$foo``) is one of TPTP's OWN words, so it is
39
+ refused as a RESERVED word, not as a malformed one. (The problem writers rewrite
40
+ such a predicate, function or constant name and record the rewrite, and the
41
+ replacement is chosen so that it equals no sort guard of the problem either, so
42
+ a problem built by them meets this refusal only for a SORT name: the fof writer
43
+ reads a sort as its guard predicate and does not rewrite it.)
44
+ * a variable that is written as something that is not a TPTP variable. A TPTP
45
+ variable is an upper-case letter followed by letters, digits and underscores, and
46
+ a variable is written as the upper-case of its name, so ``ä`` (written ``Ä``),
47
+ ``x-1`` and ``1x`` have no rendering. A variable is BOUND, so the problem writers
48
+ rename it to a fresh legal one without recording anything
49
+ (:func:`legalise_variables`); a single formula has no map to hand back and
50
+ refuses it by name.
51
+
52
+ **The two defined propositions.** ``$true`` and ``$false`` are TPTP's own
53
+ propositions, and this kit's reader (:mod:`unicode_logic_kit.fol.tptp_input`) reads
54
+ them as the NULLARY atoms ``Atom('$true')`` / ``Atom('$false')``
55
+ (:func:`is_tptp_boolean_atom`). They are not symbols of the user's: the single
56
+ formula writes them verbatim, the problem writers neither rewrite nor declare them,
57
+ and the checks here never count them. ``to_z3`` reads them as the constants true
58
+ and false, so that z3 and the TPTP provers answer the question the TPTP text asks.
59
+ Any OTHER ``$``-word, and ``$true`` itself WITH arguments, is refused as reserved.
60
+ The nullary atoms named like the glyphs ``⊤`` and ``⊥`` are the same two propositions
61
+ (:func:`truth_constant_word`): they are written as ``$true`` and ``$false``, never as
62
+ a letter.
63
+
64
+ **Namespaces.** Predicates are one namespace; functions, constants, numerals and
65
+ the arithmetic function words together are the other (a TPTP reader resolves a
66
+ bare identifier by its position, and this kit's own reader does); variables are a
67
+ third, checked per formula. Only two DISTINCT symbols inside the SAME namespace
68
+ that are written as one word are refused. A predicate and a function/constant that
69
+ share a word are not refused here (see the module docstring of
70
+ :mod:`unicode_logic_kit.atp._tptp_problem` for why the WRITERS rename the term
71
+ side instead): the text of one formula is unambiguous by position, and only a
72
+ writer can hand back the name map a caller needs to translate a proof back.
73
+
74
+ **A sort is the guard predicate of its name.** ``∀x:S φ`` is written
75
+ ``![X]: (s(X) => φ)``: the kit's semantics of a sort is that predicate (it is what
76
+ ``to_z3``, the fof writer and the Prover9 writer do), so the sort ``Foo`` and a
77
+ predicate ``foo`` are one word of the predicate namespace and are refused like any
78
+ other pair, and the sort ``Car`` next to the predicate ``Car`` is ONE symbol. The
79
+ TF0 writer cannot say that (it declares a type and a predicate), so it refuses a
80
+ sort and a predicate that share a word (:mod:`unicode_logic_kit.atp.tptp_tff`).
81
+
82
+ **The asymmetry is deliberate, for this release.** A name TPTP cannot spell, or
83
+ a predicate that clashes with a function/constant, is renamed by the writers and
84
+ recorded in the name map; two LEGAL names of one kind that fold together are
85
+ refused by name, never renamed. The same-kind refusal predates the name map and
86
+ is kept so that no existing caller silently receives a symbol renamed behind its
87
+ back; it is not a claim that the two situations differ in principle.
88
+
89
+ **The guard.** ``Node.to_tptp`` is implemented recursively in about seventy
90
+ places across the node families, and several families reduce before rendering
91
+ (a ``SortedQuantifier`` renders through a FOL reduction that introduces a sort
92
+ guard predicate; a ``Count`` renders through its distinct-witnesses expansion; a
93
+ ``Measure`` writes the function ``measure``). A scan of the SOURCE tree would
94
+ miss every name a reduction introduces, so the guard records what is actually
95
+ RENDERED instead: while an outermost ``to_tptp`` call is running, every node
96
+ whose ``to_tptp`` is entered, at any depth, reports the symbol it writes
97
+ (:meth:`~unicode_logic_kit.fol._fol_nodes.Node._tptp_symbol`), and when the
98
+ outermost call returns the recorded symbols are checked once.
99
+
100
+ It is installed centrally. :meth:`Node.__init_subclass__
101
+ <unicode_logic_kit.fol._fol_nodes.Node.__init_subclass__>` replaces whatever
102
+ ``to_tptp`` a new node class resolves to (its own, or one inherited from a mixin)
103
+ with a :class:`GuardedToTptp` descriptor, so no family edits its methods and a
104
+ future family is covered without anyone remembering to ask.
105
+
106
+ * **Only the outermost call scans.** A contextvar holds the log of the render in
107
+ progress; a call that finds one is nested and only records. The contextvar is
108
+ reset in a ``finally``, so a refusal, or a family's own ``NotImplementedError``
109
+ (a modal node has no TPTP form), leaves the next call guarded again. A
110
+ contextvar rather than a module global makes it safe across threads (a new
111
+ thread starts with no render in progress) and across re-entrancy.
112
+ * **Nested calls cost no stack frame per level.** The descriptor's ``__get__``
113
+ returns the ORIGINAL bound method while a render is in progress, so a recursive
114
+ render has the stack depth it had before the guard existed plus ONE frame (the
115
+ outermost call's wrapper) and none per level: a left-nested conjunction of about
116
+ 990 conjuncts still renders, one level fewer than without the guard (measured:
117
+ 992 and 991 against the interpreter's limit of 1000). A wrapper function on
118
+ every level would double the frames per level. The guard is not free in time:
119
+ every rendered node names its symbol once, which makes a render about 1.5 to 2
120
+ times slower than the same render without the guard (measured on a left-nested
121
+ chain of 800 conjuncts and on a balanced formula of 2048 leaves).
122
+ * **Byte-identical output.** The guard never touches the text: for a formula it
123
+ accepts, the string returned is the string the unguarded method returned.
124
+ """
125
+
126
+ import contextvars
127
+ import functools
128
+ import inspect
129
+ import re
130
+ import types
131
+ from typing import Any, Callable, Dict, Iterable, Iterator, List, Optional, Tuple
132
+
133
+ #: ``(namespace, rendered word, kit name, kind label)`` — one symbol a text
134
+ #: contains, which is what :func:`check_symbols` works on. ``namespace`` is
135
+ #: ``"predicate"``, ``"term"`` (functions, constants, numerals and the arithmetic
136
+ #: function words share it) or ``"variable"``. ``kind label`` says what the symbol
137
+ #: is: ``"predicate"``, ``"function"`` and ``"constant/function"`` are NAMES a
138
+ #: user chose; ``"reserved predicate"`` / ``"reserved function"`` are the
139
+ #: comparison and arithmetic symbols, which TPTP spells with a word of its own
140
+ #: (``<`` is ``$less``, ``+`` is ``$sum``); ``"numeral"`` is a number; ``"variable"``
141
+ #: is a variable.
142
+ Symbol = Tuple[str, str, str, str]
143
+
144
+ #: What a node reports it writes (``Node._tptp_symbol``): ``(resolver, kit
145
+ #: name)``. The hook runs once per node of every rendered formula, so it only
146
+ #: names the symbol; ``resolver(kit name)`` (the first-letter fold, the ASCII
147
+ #: transliteration) runs once per DISTINCT name, when the symbols are checked.
148
+ #: A resolver returns ``(namespace, word, kind label)``, optionally followed by
149
+ #: the name to show in a refusal when that is not the kit name itself (a number
150
+ #: shows the text it is written as), or ``None`` for a token that writes nothing.
151
+ Writes = Tuple[Callable[[Any], Optional[Tuple[str, ...]]], Any]
152
+
153
+ #: The kinds that are NAMES a user chose, which must be words TPTP reads.
154
+ _NAME_KINDS = frozenset({"predicate", "function", "constant/function"})
155
+
156
+ #: An unquoted TPTP variable: an upper-case letter, then letters, digits and
157
+ #: underscores (``upper_word`` of the TPTP grammar).
158
+ _LEGAL_VARIABLE = re.compile(r"[A-Z][A-Za-z0-9_]*")
159
+
160
+ #: TPTP's two DEFINED propositions, which the kit's own TPTP reader produces as
161
+ #: nullary atoms of exactly these names. See the module docstring.
162
+ TPTP_BOOLEAN_CONSTANTS = frozenset({"$true", "$false"})
163
+
164
+ #: The unicode glyphs of the same two constants, as the NAME of a nullary atom, and
165
+ #: the TPTP word each one stands for. ``Atom('⊤')`` prints as the glyph, the glyph
166
+ #: reads back as ``Atom('$true')``, and the two atoms are ONE constant on every
167
+ #: route: no route may read the glyph-named atom as a propositional letter.
168
+ TRUTH_GLYPH_ATOMS = {"⊤": "$true", "⊥": "$false"}
169
+
170
+
171
+ def truth_constant_word(atom) -> Optional[str]:
172
+ """The TPTP word of the truth constant ``atom`` is, or ``None`` for any other atom.
173
+
174
+ ``'$true'`` for the nullary atoms ``$true`` and ``⊤``, ``'$false'`` for the nullary
175
+ atoms ``$false`` and ``⊥``. An atom WITH arguments is a user predicate that happens
176
+ to be spelled like one of these names and is none of them.
177
+ """
178
+ if atom.args:
179
+ return None
180
+ if atom.predicate in TPTP_BOOLEAN_CONSTANTS:
181
+ return atom.predicate
182
+ return TRUTH_GLYPH_ATOMS.get(atom.predicate)
183
+
184
+
185
+ def is_tptp_boolean_atom(atom) -> bool:
186
+ """Whether ``atom`` is one of the truth constants: the nullary atom ``$true`` or
187
+ ``$false`` (the propositions TPTP itself defines) or the nullary atom named like
188
+ one of the glyphs ``⊤`` / ``⊥`` (:data:`TRUTH_GLYPH_ATOMS`). None of these is a
189
+ symbol of the user's. A name of these WITH arguments is a user predicate that
190
+ happens to be spelled like a reserved word, and is not."""
191
+ return truth_constant_word(atom) is not None
192
+
193
+ #: An unquoted TPTP predicate, function or constant: a lower-case letter, then
194
+ #: letters, digits and underscores (``lower_word`` of the TPTP grammar).
195
+ _LEGAL_WORD = re.compile(r"[a-z][A-Za-z0-9_]*")
196
+
197
+
198
+ # ---------------------------------------------------------------------------
199
+ # The check — one implementation, two callers.
200
+ # ---------------------------------------------------------------------------
201
+
202
+ def _label(kind: str) -> str:
203
+ return "constant" if kind == "constant/function" else kind
204
+
205
+
206
+ def _describe(original: str, kind: str) -> str:
207
+ if kind == "numeral":
208
+ return f"the number {original}"
209
+ if kind == "reserved predicate":
210
+ return f"the comparison {original!r}"
211
+ if kind == "reserved function":
212
+ return f"the arithmetic function {original!r}"
213
+ return f"the {_label(kind)} {original!r}"
214
+
215
+
216
+ def _why(first_kind: str, second_kind: str, dialect: str = "tptp") -> str:
217
+ kinds = {first_kind, second_kind}
218
+ if "variable" in kinds:
219
+ if dialect == "prover9":
220
+ return ("under prolog_style_variables a Prover9 variable is an "
221
+ "upper-case word, so Node.to_prover9 upper-cases a variable's name")
222
+ return ("a TPTP variable is an upper-case word, so Node.to_tptp "
223
+ "upper-cases a variable's name")
224
+ if "numeral" in kinds:
225
+ return "a number is written as its own text, which is also how a constant of that name is written"
226
+ return ("TPTP spells that comparison or arithmetic symbol with a dollar-word "
227
+ "of its own, which a symbol named like it would also be written as")
228
+
229
+
230
+ def _refusal(where: str, subject: str, first: Tuple[str, str],
231
+ second: Tuple[str, str], rendered: str,
232
+ dialect: str = "tptp") -> NotImplementedError:
233
+ (prior, prior_kind), (original, kind) = first, second
234
+ if prior_kind in _NAME_KINDS and kind in _NAME_KINDS:
235
+ return NotImplementedError(
236
+ f"{where}: distinct {kind} names {prior!r} and {original!r} would "
237
+ f"both render as the TPTP identifier {rendered!r} (Node.to_tptp folds "
238
+ "only the first character to lower-case, so it cannot tell these two "
239
+ "apart) — refusing to silently merge two distinct symbols into one; "
240
+ f"rename one of them before exporting this {subject}."
241
+ )
242
+ return NotImplementedError(
243
+ f"{where}: {_describe(prior, prior_kind)} and {_describe(original, kind)} would "
244
+ f"both render as the {'Prover9' if dialect == 'prover9' else 'TPTP'} identifier "
245
+ f"{rendered!r} ({_why(prior_kind, kind, dialect)}) "
246
+ "— refusing to silently merge two distinct symbols into one; "
247
+ f"rename one of them before exporting this {subject}."
248
+ )
249
+
250
+
251
+ def _illegal(where: str, subject: str, kind: str, original: str,
252
+ rendered: str, *, sort: bool = False) -> NotImplementedError:
253
+ remedy = (
254
+ "rename it, or build the problem with generate_tptp_problem_with_mapping "
255
+ "or generate_tff_problem_with_mapping, which write such a name under a "
256
+ "legal replacement and return the map that undoes it"
257
+ if subject == "formula" else
258
+ "rename it (generate_tff_problem_with_mapping writes a sort under a legal "
259
+ "replacement; the fof writer does not)")
260
+ what = (f"the sort {original!r} (its guard predicate {rendered!r})" if sort
261
+ else f"the {_label(kind)} name {original!r}")
262
+ if rendered.startswith("$"):
263
+ why = (f"which is a RESERVED TPTP word (every word that starts with '$' is "
264
+ "TPTP's own: $true, $false, $sum, $less, the types $i and $o, and the "
265
+ "'$$' system words; a prover reads it as that word or rejects it, so "
266
+ "it can never name a symbol of the user's)")
267
+ else:
268
+ why = ("which is not a TPTP word (an unquoted TPTP name is a lower-case letter "
269
+ "followed by letters, digits and underscores, and no prover reads any "
270
+ "other)")
271
+ return NotImplementedError(
272
+ f"{where}: {what} would be written as {rendered!r}, "
273
+ f"{why} — refusing to write text that is not TPTP; "
274
+ f"{remedy}."
275
+ )
276
+
277
+
278
+ def _illegal_variable(where: str, subject: str, original: str,
279
+ rendered: str) -> NotImplementedError:
280
+ remedy = (
281
+ "rename the variable, or build the problem with generate_tptp_problem_with_mapping, "
282
+ "generate_tff_problem_with_mapping or generate_tff_arith_problem, which write a "
283
+ "variable under a fresh legal name (a variable is bound, so nothing is recorded)"
284
+ if subject == "formula" else
285
+ "rename the variable")
286
+ return NotImplementedError(
287
+ f"{where}: the variable name {original!r} would be written as {rendered!r}, "
288
+ "which is not a TPTP variable (an unquoted TPTP variable is an upper-case "
289
+ "letter followed by letters, digits and underscores, and no prover reads any "
290
+ "other; a variable is written as the upper-case of its name) — refusing to "
291
+ f"write text that is not TPTP; {remedy}."
292
+ )
293
+
294
+
295
+ def check_symbols(symbols: Iterable[Symbol], *, where: str, subject: str,
296
+ dialect: str = "tptp", sorts: Iterable[str] = ()) -> None:
297
+ """Raise ``NotImplementedError`` if two DISTINCT kit symbols of one namespace
298
+ are written as the same TPTP word, or if a name is written as a word that is
299
+ not TPTP.
300
+
301
+ ``symbols`` is every symbol the text contains, in the order they were met;
302
+ the same kit name repeating (the ordinary case of one predicate used twice)
303
+ is not a collision, and a function and a constant of one name are one symbol.
304
+ The message names both kit symbols and the shared word; ``where`` prefixes it
305
+ and ``subject`` ("problem" / "formula") ends it. Collisions are decided over
306
+ every symbol first, then each written name must be a legal TPTP word and each
307
+ written variable a legal TPTP variable, so the more specific refusal wins.
308
+
309
+ ``dialect="prover9"`` asks for the collision check of the VARIABLES only, worded
310
+ for Prover9 (the same ``upper-case of the name``), and none of the TPTP
311
+ legality rules. ``sorts`` names the kit sorts of the problem: a predicate that
312
+ is the guard of one is reported as that sort when its word is illegal.
313
+ """
314
+ seen: Dict[Tuple[str, str], Tuple[str, str]] = {}
315
+ written: Dict[Tuple[str, str, str], str] = {}
316
+ variables: Dict[Tuple[str, str], None] = {}
317
+ for namespace, rendered, original, kind in symbols:
318
+ key = (namespace, rendered)
319
+ prior = seen.get(key)
320
+ if prior is None:
321
+ seen[key] = (original, kind)
322
+ elif (prior[0], prior[1] == "numeral") != (original, kind == "numeral"):
323
+ raise _refusal(where, subject, prior, (original, kind), rendered, dialect)
324
+ if kind in _NAME_KINDS:
325
+ written.setdefault((namespace, rendered, original), kind)
326
+ elif kind == "variable":
327
+ variables.setdefault((rendered, original))
328
+ if dialect != "tptp":
329
+ return
330
+ sort_names = frozenset(sorts)
331
+ for (namespace, rendered, original), kind in written.items():
332
+ if not _LEGAL_WORD.fullmatch(rendered):
333
+ raise _illegal(where, subject, kind, original, rendered,
334
+ sort=kind == "predicate" and original in sort_names)
335
+ for rendered, original in variables:
336
+ if not _LEGAL_VARIABLE.fullmatch(rendered):
337
+ raise _illegal_variable(where, subject, original, rendered)
338
+
339
+
340
+ def _resolve(writes: Writes) -> Optional[Symbol]:
341
+ resolver, name = writes
342
+ found = resolver(name)
343
+ if found is None:
344
+ return None
345
+ return (found[0], found[1], found[3] if len(found) > 3 else name, found[2])
346
+
347
+
348
+ def _symbols_of(formula) -> Iterator[Symbol]:
349
+ for node in formula.walk():
350
+ writes = node._tptp_symbol()
351
+ if writes is not None:
352
+ symbol = _resolve(writes)
353
+ if symbol is not None:
354
+ yield symbol
355
+
356
+
357
+ def check_no_symbol_collisions(formulas: Iterable, *, where: str,
358
+ subject: str = "problem",
359
+ sorts: Iterable[str] = ()) -> None:
360
+ """:func:`check_symbols` over every node of every formula in ``formulas``,
361
+ walked in pre-order — the whole-problem check of the writers.
362
+
363
+ Predicates, functions, constants and numerals are one pool for the whole
364
+ problem. A VARIABLE is scoped to its own formula (``∀x P(x)`` in one premise
365
+ and ``∀X Q(X)`` in another are two quantifiers that bind separately), so the
366
+ variables are checked once per formula. ``sorts`` is passed on to
367
+ :func:`check_symbols`."""
368
+ sorts = frozenset(sorts)
369
+
370
+ def pooled() -> Iterator[Symbol]:
371
+ for formula in formulas:
372
+ variables: List[Symbol] = []
373
+ for symbol in _symbols_of(formula):
374
+ if symbol[0] == "variable":
375
+ variables.append(symbol)
376
+ else:
377
+ yield symbol
378
+ check_symbols(variables, where=where, subject=subject)
379
+ check_symbols(pooled(), where=where, subject=subject, sorts=sorts)
380
+
381
+
382
+ def check_variable_names(formula, *, where: str, subject: str = "problem",
383
+ dialect: str = "tptp") -> None:
384
+ """The variable part of :func:`check_no_symbol_collisions` for ONE formula:
385
+ refuse two distinct variable names that are written as one TPTP variable
386
+ (``x`` and ``X``), and a variable that is written as no TPTP variable at all.
387
+ For a writer that renders a formula itself rather than through
388
+ ``Node.to_tptp``, which runs this check on its own.
389
+
390
+ ``dialect="prover9"`` is the same check for the Prover9 writer: Prover9 writes
391
+ a variable as the upper-case of its name too, so ``x`` and ``X`` are one
392
+ variable there as well; only the collision is refused (Prover9's own reading
393
+ of a name is not the TPTP grammar)."""
394
+ check_symbols((symbol for symbol in _symbols_of(formula) if symbol[0] == "variable"),
395
+ where=where, subject=subject, dialect=dialect)
396
+
397
+
398
+ def legalise_variables(formula):
399
+ """``formula`` with every variable that has no TPTP spelling renamed to a fresh
400
+ one; the very same object when there is none.
401
+
402
+ A variable is written as the upper-case of its name, and a TPTP variable is an
403
+ upper-case letter followed by letters, digits and underscores, so ``ä`` (written
404
+ ``Ä``), ``x-1`` and ``1x`` cannot be written as they are. A variable is BOUND,
405
+ which is why a writer may rename it without recording anything: the rename is
406
+ alpha-conversion, applied to the binder and to every occurrence alike.
407
+
408
+ * **per formula** — the scope of a variable is one formula, so the caller passes
409
+ one formula at a time;
410
+ * **injective** — distinct illegal names get distinct new names (``ä`` and ``Ä``
411
+ stay two variables, which writing both as ``Ä`` would have made one);
412
+ * **capture-free** — a new name is one of :func:`unicode_logic_kit.fol._identifiers
413
+ .fresh_variables` (``x0``, ``x1``, ... written ``X0``, ``X1``, ...) that equals
414
+ no variable of the formula *as written*: it avoids every name of the formula
415
+ and the lower-case of every written form (``X0`` blocks ``x0``), so a legal
416
+ variable is never taken over.
417
+
418
+ Two LEGAL names that are written as one (``x`` and ``X``) are not touched; they
419
+ are refused by :func:`check_variable_names`, as they always were."""
420
+ from . import _identifiers
421
+ from ._fol_nodes import Variable
422
+
423
+ names: List[str] = []
424
+ seen = set()
425
+ for node in formula.walk():
426
+ if isinstance(node, Variable) and node.name not in seen:
427
+ seen.add(node.name)
428
+ names.append(node.name)
429
+ illegal = [n for n in names if not _LEGAL_VARIABLE.fullmatch(n.upper())]
430
+ if not illegal:
431
+ return formula
432
+ avoid = set(_identifiers.variable_names(formula))
433
+ avoid |= {n.upper().lower() for n in avoid}
434
+ fresh = _identifiers.fresh_variables(len(illegal), letter="x", avoid=avoid)
435
+ table = dict(zip(illegal, fresh))
436
+
437
+ def rename(node):
438
+ if isinstance(node, Variable):
439
+ return Variable(table.get(node.name, node.name))
440
+ return node.map_children(rename)
441
+
442
+ return rename(formula)
443
+
444
+
445
+ # ---------------------------------------------------------------------------
446
+ # The guard — one scan per outermost ``to_tptp`` call.
447
+ # ---------------------------------------------------------------------------
448
+
449
+ class _RenderLog:
450
+ """What one outermost render has written so far: every distinct
451
+ ``(resolver, kit name)`` a node reported, in the order first met."""
452
+
453
+ __slots__ = ("symbols",)
454
+
455
+ def __init__(self) -> None:
456
+ self.symbols: Dict[Writes, None] = {}
457
+
458
+ def note(self, node) -> None:
459
+ writes = node._tptp_symbol()
460
+ if writes is not None:
461
+ self.symbols.setdefault(writes)
462
+
463
+
464
+ #: The render in progress in THIS context (thread / task), or ``None``.
465
+ _RENDERING: "contextvars.ContextVar[Optional[_RenderLog]]" = contextvars.ContextVar(
466
+ "unicode_logic_kit_tptp_render", default=None)
467
+
468
+
469
+ def _scan_rendered_symbols(log: _RenderLog) -> None:
470
+ """The scan: called exactly once per outermost ``to_tptp`` call that
471
+ returned a text. Looked up by name at call time so a test can count it."""
472
+ resolved = (_resolve(writes) for writes in log.symbols)
473
+ check_symbols((symbol for symbol in resolved if symbol is not None),
474
+ where="Node.to_tptp", subject="formula")
475
+
476
+
477
+ def _outermost(function: Callable) -> Callable:
478
+ @functools.wraps(function)
479
+ def to_tptp(node, *args, **kwargs):
480
+ log = _RENDERING.get()
481
+ if log is not None: # nested: record, do not scan
482
+ log.note(node)
483
+ return function(node, *args, **kwargs)
484
+ log = _RenderLog()
485
+ token = _RENDERING.set(log)
486
+ try:
487
+ log.note(node)
488
+ text = function(node, *args, **kwargs)
489
+ finally:
490
+ _RENDERING.reset(token)
491
+ _scan_rendered_symbols(log)
492
+ return text
493
+ return to_tptp
494
+
495
+
496
+ class GuardedToTptp:
497
+ """The descriptor :func:`guard_class` puts in place of a ``to_tptp`` method.
498
+
499
+ ``Class.to_tptp`` is the guarded plain function. ``node.to_tptp`` is the
500
+ guarded bound method when no render is in progress, and the ORIGINAL bound
501
+ method (after recording the node) when one is — which is what keeps a
502
+ nested call from costing a stack frame. ``__wrapped__`` is the original
503
+ function.
504
+ """
505
+
506
+ def __init__(self, function: Callable) -> None:
507
+ for attribute in ("__module__", "__name__", "__qualname__", "__doc__"):
508
+ setattr(self, attribute, getattr(function, attribute))
509
+ self.__wrapped__ = function
510
+ self._function = function
511
+ self._guarded = _outermost(function)
512
+
513
+ def __get__(self, node, owner=None):
514
+ if node is None:
515
+ return self._guarded
516
+ log = _RENDERING.get()
517
+ if log is None:
518
+ return types.MethodType(self._guarded, node)
519
+ # Nested: record what this node writes (``_RenderLog.note``, inlined —
520
+ # this runs once per node of every rendered formula) and hand back the
521
+ # ORIGINAL method.
522
+ writes = node._tptp_symbol()
523
+ if writes is not None:
524
+ log.symbols.setdefault(writes)
525
+ return types.MethodType(self._function, node)
526
+
527
+
528
+ def is_guarded(cls: type) -> bool:
529
+ """Whether ``cls.to_tptp`` (resolved through the MRO, unbound) is guarded."""
530
+ return isinstance(inspect.getattr_static(cls, "to_tptp", None), GuardedToTptp)
531
+
532
+
533
+ def guard_class(cls: type) -> None:
534
+ """Make ``cls.to_tptp`` guarded, whatever it resolves to through the MRO.
535
+
536
+ Called from ``Node.__init_subclass__`` for every node class, and once for
537
+ ``Node`` itself. A ``to_tptp`` that is already guarded (inherited) is left
538
+ alone; one inherited from a mixin is wrapped on ``cls``. A ``to_tptp`` that
539
+ is not a plain function (a ``staticmethod``, a property) cannot be guarded
540
+ and is refused at class-creation time rather than left as a hole.
541
+ """
542
+ current = inspect.getattr_static(cls, "to_tptp", None)
543
+ if current is None or isinstance(current, GuardedToTptp):
544
+ return
545
+ if not isinstance(current, types.FunctionType):
546
+ raise TypeError(
547
+ f"{cls.__qualname__}.to_tptp is a {type(current).__name__}, not a plain "
548
+ "method: the single-formula collision guard (fol/_tptp_symbols.py) "
549
+ "can only wrap a plain method. Define to_tptp(self) as a regular "
550
+ "method.")
551
+ setattr(cls, "to_tptp", GuardedToTptp(current))
@@ -0,0 +1,117 @@
1
+ """The two truth constants ``$true`` and ``$false``, as every route reads them.
2
+
3
+ TPTP defines two propositions of its own, ``$true`` and ``$false``, and this kit
4
+ reads them (:mod:`unicode_logic_kit.fol.tptp_input`, the SMT-LIB reader, the QMLTP
5
+ reader) as the NULLARY atoms ``Atom('$true')`` / ``Atom('$false')``. They are the
6
+ constants truth and falsity, not propositional letters of the user's:
7
+
8
+ * a classical route reads them as true and false (``$false ⊢ P`` is valid,
9
+ ``⊢ $true`` is valid, ``⊢ $false`` is not, ``$true ⊢ P`` is not);
10
+ * a modal route reads them the same at every world, an intuitionistic route
11
+ forces ``$true`` at every world and ``$false`` at none, a many-valued route
12
+ gives them the top and the bottom value of the matrix;
13
+ * a logic that has no agreed reading of them refuses them BY NAME, through
14
+ :func:`refuse_truth_constants` (relevant logic has two truths and two falsities,
15
+ linear logic has an additive and a multiplicative unit of each, the Lambek
16
+ calculus has no propositional constants at all).
17
+
18
+ The unicode glyphs are ``⊤`` and ``⊥`` (LaTeX ``\\top`` / ``\\bot``); every unicode
19
+ grammar mode that has propositional atoms reads them back to these two atoms. A
20
+ nullary atom that is merely NAMED like a glyph (``Atom('⊥', ())``, which prints as
21
+ ``⊥`` exactly like ``Atom('$false', ())`` does, and which reads back as
22
+ ``$false``) is the same constant: one glyph has one meaning, so no route reads
23
+ either atom as a propositional letter. ``$true`` / ``⊤`` WITH arguments is an
24
+ ordinary (if oddly spelled) user predicate and is neither constant:
25
+ :func:`truth_value` answers ``None`` for it.
26
+
27
+ This module only RECOGNISES the constants. It builds no formula and has no
28
+ dependency on any route, so every evaluator and prover can import it.
29
+ """
30
+
31
+ from typing import Iterable, Optional, Tuple
32
+
33
+ from ._fol_nodes import Atom, Node
34
+ from ._tptp_symbols import truth_constant_word
35
+
36
+ __all__ = [
37
+ "TRUE_PREDICATE", "FALSE_PREDICATE", "TRUE", "FALSE",
38
+ "truth_value", "is_true_constant", "is_false_constant", "is_truth_constant",
39
+ "truth_constants_in", "refuse_truth_constants",
40
+ ]
41
+
42
+ #: The predicate names of the two constants, as the TPTP reader spells them.
43
+ TRUE_PREDICATE = "$true"
44
+ FALSE_PREDICATE = "$false"
45
+
46
+ #: The two constants as nodes.
47
+ TRUE: Atom = Atom(TRUE_PREDICATE, ())
48
+ FALSE: Atom = Atom(FALSE_PREDICATE, ())
49
+
50
+
51
+ def truth_value(node: Node) -> Optional[bool]:
52
+ """``True`` for the nullary atoms ``$true`` and ``⊤``, ``False`` for ``$false`` and
53
+ ``⊥``, else ``None``.
54
+
55
+ ``None`` also for these names WITH arguments (a user predicate that
56
+ happens to be spelled like a reserved word) and for every node that is not an
57
+ atom, so ``truth_value(node) is None`` reads "an ordinary node".
58
+ """
59
+ if isinstance(node, Atom):
60
+ word = truth_constant_word(node)
61
+ if word is not None:
62
+ return word == TRUE_PREDICATE
63
+ return None
64
+
65
+
66
+ def is_true_constant(node: Node) -> bool:
67
+ """Whether ``node`` is the truth constant true: the nullary atom ``$true`` or ``⊤``."""
68
+ return truth_value(node) is True
69
+
70
+
71
+ def is_false_constant(node: Node) -> bool:
72
+ """Whether ``node`` is the falsity constant: the nullary atom ``$false`` or ``⊥``."""
73
+ return truth_value(node) is False
74
+
75
+
76
+ def is_truth_constant(node: Node) -> bool:
77
+ """Whether ``node`` is one of the two truth constants."""
78
+ return truth_value(node) is not None
79
+
80
+
81
+ def truth_constants_in(formulas: Iterable[Node]) -> Tuple[Atom, ...]:
82
+ """The distinct truth constants occurring anywhere in ``formulas``, in order.
83
+
84
+ Each constant is listed once, as its first occurrence: ``$false`` and ``⊥`` are
85
+ one constant, so a formula that holds both lists the one that comes first.
86
+ """
87
+ seen = []
88
+ values = []
89
+ for formula in formulas:
90
+ for node in formula.walk():
91
+ value = truth_value(node)
92
+ if value is not None and value not in values:
93
+ values.append(value)
94
+ seen.append(node)
95
+ return tuple(seen)
96
+
97
+
98
+ def refuse_truth_constants(formulas: Iterable[Node], route: str, why: str,
99
+ error: type = NotImplementedError) -> None:
100
+ """Raise ``error`` naming ``route`` if ``formulas`` use a truth constant.
101
+
102
+ For a route whose logic has no agreed reading of ``$true`` / ``$false``. The
103
+ message names the route, the constant, and ``why`` the logic has no reading, so
104
+ the refusal is a statement and not a guess. ``error`` is the exception class the
105
+ route already uses for input it does not support (``NotImplementedError`` unless
106
+ the route says otherwise). Returns ``None`` when the formulas use neither
107
+ constant.
108
+ """
109
+ found = truth_constants_in(formulas)
110
+ if not found:
111
+ return
112
+ names = " and ".join(a.predicate for a in found)
113
+ raise error(
114
+ f"{route}: the truth constant {names} has no agreed reading here ({why}); "
115
+ "this route refuses it by name rather than read it as a propositional "
116
+ "letter. Write the formula without the constant, or decide it in a logic "
117
+ "that has one (classical, intuitionistic, modal, K3 / LP / FDE, fuzzy).")