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,510 @@
1
+ """The key of an atom on a route that identifies an atom by its written form.
2
+
3
+ A propositional route (a truth table, a many-valued or fuzzy evaluator, the intuitionistic
4
+ and counterfactual deciders, a trace of a linear-time formula) takes every ground atom as a
5
+ letter of its own and names the letter by the KEY of the atom: the text it prints as, with
6
+ every constant written by its bare name (``key_text`` of ``fol/_msfl_nodes.py``:
7
+ ``'P'``, ``'P(a)'``, ``'Likes(a, b)'``). The text of the FORMULA (``Atom.to_unicode_str()``)
8
+ writes a constant in quotes when its bare name would read as something else (``P('a')``);
9
+ the key does not, so a valuation typed as ``{"P(a)": True}`` keeps meaning the atom ``P``
10
+ over the element ``a``, as it always did.
11
+
12
+ An atom whose constants are all bare-readable has ONE spelling: its key and its formula text
13
+ are the same string. An atom with a quoted constant has TWO, and a caller's valuation may be
14
+ keyed in either: the guide taught ``atom.to_unicode_str()`` as "the key", and a hand-built atom
15
+ over ``Constant("a")`` prints ``Likes('a', 'b')`` since quoted constants exist. So a lookup in a
16
+ table that comes FROM THE CALLER goes through :func:`find_key`, which reads the key and then the
17
+ formula text; a table the kit builds itself and reads back is keyed by :func:`atom_key` alone.
18
+ The two spellings cannot name two atoms because a key that holds a complete quoted constant
19
+ (that of ``Constant("'a'")``: its key ``P('a')`` is also the formula text of ``P`` over the
20
+ constant ``a``) is refused where a key is made.
21
+
22
+ The key is faithful only while two different atoms never have one. Two do, and the route would
23
+ read them as ONE letter and answer about another problem:
24
+
25
+ * the numeral ``1`` and a constant named ``1`` (``Number(1)`` and ``Constant('1')``: the
26
+ TPTP reader makes the pair from ``p(1)`` and ``p('1')``): ``P(1) ⊢ P('1')`` would be valid;
27
+ * a free variable ``x`` and a constant named ``x``: ``P(c) → P(x)`` would be valid for
28
+ ``c = Constant('x')``, although a free variable is a parameter (one unknown element), not
29
+ the constant of its name;
30
+ * a constant named like a compound term (``Constant('f(a)')`` and ``f(a)``).
31
+
32
+ :class:`AtomKeys` is the one place the key is built. It gives every atom of one problem its
33
+ key and refuses, by name, a problem in which two different atoms would get the same key (the
34
+ refusal every route that names a symbol by its text already gives for a numeral and a constant
35
+ of one spelling). :func:`atom_key` is the key alone, for a route that already knows the
36
+ problem has no such pair.
37
+
38
+ A sorted constant ``c:S`` is the constant ``c`` (it denotes an element of ``S``): its key is
39
+ the key of the plain constant, so ``Mortal(c:S)`` and ``Mortal(c)`` are ONE letter. A route
40
+ that cannot state what membership of the sort means for its truth values refuses the sorted
41
+ constant by name instead (``sorted_constants="refuse"``).
42
+
43
+ The operators of an agent are named in the same way: a model keeps the relation of ``K_a`` under
44
+ the name ``"K:" + agent_key(a)``, so two different agent terms with one key (the numeral ``1``
45
+ and the constant ``'1'``) would be ONE agent. :func:`refuse_alike_agents` refuses such a pair,
46
+ for the routes that file a relation, a tableau branch or a first-order predicate under that name.
47
+ """
48
+
49
+ import re
50
+ from collections.abc import Mapping
51
+ from functools import lru_cache
52
+ from typing import Any, Container, Dict, Iterable, List, Optional, Tuple, Type
53
+
54
+ from ._fol_nodes import Atom, Constant, Function, Node, Number, Variable
55
+ from ._identifiers import (
56
+ _UNSPELLABLE, _unquote_constant, is_bare_constant, quoted_name_pattern)
57
+ from ._modal_nodes import (
58
+ Believes, CommonKnowledge, DistributedKnowledge, EverybodyKnows, Knows, Says, Wants)
59
+ from ._msfl_nodes import SortedConstant, key_text
60
+ from ._truth_constants import truth_value
61
+
62
+ __all__ = ["atom_key", "other_key", "find_key", "find_own_key", "plain_atom",
63
+ "refuse_sorted_constant", "AtomKeys", "agent_key", "refuse_alike_agents"]
64
+
65
+
66
+ @lru_cache(maxsize=1)
67
+ def _free_standing_quoted_token():
68
+ """A complete QUOTED_NAME token with no letter, digit or apostrophe next to it.
69
+
70
+ Where the text of a formula writes a constant in quotes, the token stands free: what
71
+ comes before it is the start, ``(`` or a space, and what follows is the end, ``)``, ``,``, a
72
+ space, ``:`` or ``}``. The pattern asks for less (anything but a letter, a digit or an
73
+ apostrophe on either side), so it finds every such token and some text that is none.
74
+ """
75
+ return re.compile(r"(?<![^\W_])(?<!')" + quoted_name_pattern() + r"(?![^\W_])(?!')")
76
+
77
+
78
+ @lru_cache(maxsize=8192)
79
+ def _has_text(name: str) -> bool:
80
+ """Whether a constant of this (not bare-readable) name has a text, in quotes.
81
+
82
+ The empty name and a name with a control character have none, so the formula text of an atom
83
+ that holds such a constant does not exist.
84
+ """
85
+ return name != "" and _UNSPELLABLE.search(name) is None
86
+
87
+
88
+ def _refuse_key_that_reads_as_text(key: str, route: str, error: Type[Exception]) -> None:
89
+ """Refuse a key that holds a complete quoted constant.
90
+
91
+ A key writes every name as it is, so a quote in a key is part of a NAME (a constant named
92
+ ``'a'``, one named ``ab, 'b'``, a proposition a TPTP file calls ``'p(\\'a\\')'``). The text
93
+ of a formula writes a quote only around a constant. A key that holds a whole quoted
94
+ constant, standing free, may therefore be the text of ANOTHER atom as a formula: ``P('a')``
95
+ is the key of ``P`` over the constant named ``'a'`` and the text of ``P`` over the constant
96
+ ``a``. A valuation may be keyed either way, so one entry would answer for two atoms.
97
+
98
+ The check is on the safe side. It refuses every key that is the formula text of another atom
99
+ with a quoted constant, and also a key such as ``P(rock 'n' roll)``, which is the text of no
100
+ formula. A name with one apostrophe (``D'Alembert``), with apostrophes next to letters or
101
+ digits (``3',5'-cyclic``), or two such names in one atom, is not refused.
102
+ """
103
+ found = _free_standing_quoted_token().search(key)
104
+ if found is None:
105
+ return
106
+ token = found.group(0)
107
+ inner = _unquote_constant(token)
108
+ raise error(
109
+ f"{route}: the key of this atom, {key!r}, reads as the text of another atom. A key "
110
+ f"writes every name as it is, and a name here holds apostrophes, so the key holds "
111
+ f"{token}, which is how a formula writes the constant named \"{inner}\" (in quotes). "
112
+ f"A valuation may be keyed by the key of an atom or by its text as a formula, so one "
113
+ f"entry would answer for two atoms. Rename the symbol (a name that merely holds an "
114
+ f"apostrophe, such as D'Alembert, is fine).")
115
+
116
+
117
+ def _survey(atom: Node) -> Tuple[bool, bool, bool]:
118
+ """One walk over ``atom``: ``(has a sorted constant, its formula text writes a quote, a name
119
+ of it may hold an apostrophe)``.
120
+
121
+ The third is what :func:`_refuse_key_that_reads_as_text` is asked about: a key holds a quote
122
+ only where a name does, so an atom whose names hold none is never scanned.
123
+ """
124
+ if type(atom) is Atom and not atom.args:
125
+ return False, False, "'" in atom.predicate # a proposition holds no constant
126
+ has_sorted = False
127
+ has_quoted = False
128
+ has_no_text = False
129
+ apostrophe = False
130
+ # The same nodes as ``atom.walk()``, but without the generic ``_child_nodes`` (which reads
131
+ # the dataclass fields of every node): a key is made for every atom of every evaluation,
132
+ # and an atom is a handful of constants, variables and numerals. A node of another kind
133
+ # takes the generic way.
134
+ # ``Any``: the node kinds are told apart by their class below, which a type checker does
135
+ # not follow, and each branch reads the fields of the class it found.
136
+ stack: List[Any] = [atom]
137
+ while stack:
138
+ node = stack.pop()
139
+ kind = type(node)
140
+ if kind is Number:
141
+ continue
142
+ if kind is Variable:
143
+ apostrophe = apostrophe or "'" in node.name
144
+ continue
145
+ if kind is Atom:
146
+ apostrophe = apostrophe or "'" in node.predicate
147
+ stack.extend(node.args)
148
+ continue
149
+ if kind is Function:
150
+ apostrophe = apostrophe or "'" in node.name
151
+ stack.extend(node.args)
152
+ continue
153
+ if kind is SortedConstant:
154
+ has_sorted = True
155
+ elif kind is not Constant:
156
+ stack.extend(node._child_nodes())
157
+ if not isinstance(node, (Constant, SortedConstant)):
158
+ # a node of a kind not listed here: its own name is not looked at, so the key
159
+ # is scanned to be sure
160
+ apostrophe = True
161
+ continue
162
+ has_sorted = has_sorted or isinstance(node, SortedConstant)
163
+ name = node.name
164
+ if not isinstance(name, str):
165
+ has_no_text = True
166
+ elif is_bare_constant(name):
167
+ continue # a bare name holds no apostrophe
168
+ else:
169
+ apostrophe = apostrophe or "'" in name
170
+ if _has_text(name):
171
+ has_quoted = True
172
+ else:
173
+ has_no_text = True
174
+ # an atom that holds a constant without a text has no formula text at all
175
+ return has_sorted, has_quoted and not has_no_text, apostrophe
176
+
177
+
178
+ def _key(atom: Node, route: str, error: Type[Exception]) -> Tuple[str, Node, bool]:
179
+ """``(key, plain atom, whether its formula text differs from the key)`` of ``atom``.
180
+
181
+ The one place a key is rendered. A key that reads as the formula text of another atom is
182
+ refused here (:func:`_refuse_key_that_reads_as_text`).
183
+ """
184
+ has_sorted, quoted, apostrophe = _survey(atom)
185
+ plain = _plain(atom) if has_sorted else atom
186
+ key = key_text(plain)
187
+ if apostrophe:
188
+ _refuse_key_that_reads_as_text(key, route, error)
189
+ return key, plain, quoted
190
+
191
+
192
+ def _has_sorted_constant(node: Node) -> bool:
193
+ """Whether a sorted constant occurs anywhere inside ``node``."""
194
+ return any(isinstance(n, SortedConstant) for n in node.walk())
195
+
196
+
197
+ def _plain(node: Node) -> Node:
198
+ """``node`` with every sorted constant ``c:S`` replaced by the plain constant ``c``."""
199
+ if isinstance(node, SortedConstant):
200
+ return Constant(node.name)
201
+ return node.map_children(_plain)
202
+
203
+
204
+ def plain_atom(atom: Node) -> Node:
205
+ """The atom with each sorted constant ``c:S`` read as the constant ``c`` (the atom itself
206
+ when it has none)."""
207
+ return _plain(atom) if _has_sorted_constant(atom) else atom
208
+
209
+
210
+ def atom_key(atom: Node) -> str:
211
+ """The key of an atom: the text of the atom with every constant written by its bare name.
212
+
213
+ This is the definition of a key. The valuations, models and traces that the evaluators of
214
+ the kit read (``satisfies_modal``, ``fuzzy_evaluate``, the many-valued, matrix,
215
+ intuitionistic and counterfactual evaluators, ``ltl_trace_satisfies``, the probabilistic
216
+ programs) are keyed by it, and so is every table the kit RETURNS (the countermodels of the
217
+ modal, intuitionistic and counterfactual deciders, the models of a tableau, the columns of a
218
+ truth table). A sorted constant ``c:S`` is written ``c``, as the constant it is.
219
+
220
+ The key is NOT the text of the formula. ``Constant("k2")`` is written ``'k2'`` in the formula
221
+ text, so that the text reads back as that constant, and ``k2`` in the key: for the atom
222
+ ``Likes`` over the constants ``a`` and ``b`` the key is ``Likes(a, b)`` and the text of the
223
+ formula is ``Likes('a', 'b')``. For an atom whose constants are all bare-readable
224
+ (``Likes(alice, bob)``) the two are the same string. The evaluators also read a valuation
225
+ that a caller keyed by the text of the atom as a formula, ``atom.to_unicode_str()``, so
226
+ either spelling is found.
227
+
228
+ No check is made that another atom does not have the same key; :class:`AtomKeys` does that
229
+ for the atoms of one problem.
230
+
231
+ Raises:
232
+ NotImplementedError: the key would hold a complete quoted constant, because a name of
233
+ the atom holds apostrophes (``Constant("'a'")``, ``Constant("ab, 'b'")``): such a key
234
+ is also the formula text of another atom (``P`` over the constant ``a``), and one
235
+ key must not name two atoms. A name that merely holds an apostrophe
236
+ (``D'Alembert``, ``3',5'-cyclic``) has its key.
237
+ """
238
+ return _key(atom, "atom_key", NotImplementedError)[0]
239
+
240
+
241
+ def other_key(atom: Node) -> Optional[str]:
242
+ """The OTHER spelling of the key of an atom: its text as a formula, when that differs.
243
+
244
+ ``None`` when no constant of the atom is written in quotes (every constant reads back
245
+ bare), where the formula text and :func:`atom_key` are one string and nothing is rendered; else
246
+ the text of the same plain atom as a formula (``Likes('a', 'b')`` for the key
247
+ ``Likes(a, b)``). A valuation a caller keyed by ``atom.to_unicode_str()`` holds this one.
248
+ The two spellings are different strings whenever the second exists, and the refusal of a
249
+ key that holds a quoted constant (:func:`atom_key`) keeps them from naming two atoms.
250
+
251
+ Raises:
252
+ NotImplementedError: as :func:`atom_key`.
253
+ """
254
+ has_sorted, quoted, apostrophe = _survey(atom)
255
+ plain = _plain(atom) if has_sorted else atom
256
+ if apostrophe:
257
+ _refuse_key_that_reads_as_text(key_text(plain), "other_key", NotImplementedError)
258
+ return plain.to_unicode_str() if quoted else None
259
+
260
+
261
+ def _locate(table: Container[str], key: str, plain: Node, quoted: bool,
262
+ route: str) -> Optional[str]:
263
+ """The body of :func:`find_key`, for an atom whose ``key`` is made already."""
264
+ if not quoted:
265
+ return key if key in table else None
266
+ if key in table:
267
+ if isinstance(table, Mapping):
268
+ other = plain.to_unicode_str()
269
+ if other in table:
270
+ a, b = table[key], table[other]
271
+ if not (a is b or a == b):
272
+ raise ValueError(
273
+ f"{route}: one atom, two entries. The atom {other} is keyed both "
274
+ f"{key!r} (value {a!r}) and {other!r} (value {b!r}) in this table, "
275
+ f"and the two values differ. A key written with the bare names of the "
276
+ f"constants and one written as the text of the formula are two "
277
+ f"spellings of ONE atom, and the table says two different things "
278
+ f"about it. Keep one of the two entries.")
279
+ return key
280
+ other = plain.to_unicode_str()
281
+ return other if other in table else None
282
+
283
+
284
+ def find_own_key(table: Container[str], atom: Node) -> Optional[str]:
285
+ """The key under which a table THE KIT BUILT holds ``atom``, or ``None``.
286
+
287
+ For a route that fills a table itself, keyed by :func:`atom_key`, and reads it back (the rows
288
+ of a truth table, the assignments an enumeration tries): there is no second spelling to look
289
+ for, and the lookup is as cheap as it was before the formula text of a constant was quoted.
290
+ A table that comes from a caller is read by :func:`find_key`.
291
+ """
292
+ key = atom_key(atom)
293
+ return key if key in table else None
294
+
295
+
296
+ def find_key(keys: Container[str], atom: Node) -> Optional[str]:
297
+ """The key under which ``keys`` holds ``atom``, or ``None``.
298
+
299
+ ``keys`` is what a caller's table offers for ``in``: the set of keys true at one world, a
300
+ dict from key to value. The atom is looked up under :func:`atom_key` first (the bare names of
301
+ its constants, ``Likes(a, b)``) and then under its text as a formula
302
+ (``Likes('a', 'b')``), the key a caller gets from ``atom.to_unicode_str()``. An atom whose two
303
+ spellings are one string is looked up once and nothing is rendered a second time.
304
+
305
+ Raises:
306
+ ValueError: ``keys`` is a mapping that holds both spellings of the atom with different
307
+ values (one atom, two entries); equal values, or both spellings in a set, are no
308
+ conflict.
309
+ NotImplementedError: as :func:`atom_key`.
310
+ """
311
+ key, plain, quoted = _key(atom, "find_key", NotImplementedError)
312
+ return _locate(keys, key, plain, quoted, "find_key")
313
+
314
+
315
+ def refuse_sorted_constant(atom: Node, route: str,
316
+ error: Type[Exception] = NotImplementedError) -> None:
317
+ """Refuse an atom that holds a sorted constant, by name.
318
+
319
+ For a route that has no reading of what ``c:S`` being an element of ``S`` means for its
320
+ values: reading ``S(c)`` as unrelated to the annotation would answer about another
321
+ problem. ``error`` is the exception class raised (the exception a route already uses for
322
+ the input it refuses).
323
+
324
+ Raises:
325
+ NotImplementedError: a sorted constant occurs in ``atom`` (or the class ``error``).
326
+ """
327
+ for sub in atom.walk():
328
+ if isinstance(sub, SortedConstant):
329
+ raise error(
330
+ f"{route}: the sorted constant {sub.to_unicode_str()} in the atom "
331
+ f"{atom.to_unicode_str()!r} has no reading here. {sub.name}:{sub.sort} is the "
332
+ f"constant {sub.name} and lies in the sort {sub.sort}, a fact about the sort "
333
+ f"that this route has no way to state for its values, and reading the atom as "
334
+ f"another letter would answer about another problem. Write the constant "
335
+ f"without its sort, and the fact as an atom ({sub.sort}({sub.name})) if it "
336
+ f"matters.")
337
+
338
+
339
+ def _first_difference(a: Node, b: Node) -> Tuple[Node, Node]:
340
+ """The first pair of corresponding sub-terms of two atoms that are not equal."""
341
+ if isinstance(a, Atom) and isinstance(b, Atom):
342
+ same_head = a.predicate == b.predicate
343
+ a_args, b_args = a.args, b.args
344
+ elif isinstance(a, Function) and isinstance(b, Function):
345
+ same_head = a.name == b.name
346
+ a_args, b_args = a.args, b.args
347
+ else:
348
+ return a, b
349
+ if same_head and len(a_args) == len(b_args):
350
+ for x, y in zip(a_args, b_args):
351
+ if x != y:
352
+ return _first_difference(x, y)
353
+ return a, b
354
+
355
+
356
+ def _why_alike(a: Node, b: Node) -> str:
357
+ """One sentence on why two different terms are written alike."""
358
+ for numeral, constant in ((a, b), (b, a)):
359
+ if isinstance(numeral, Number) and isinstance(constant, Constant):
360
+ return (f"The numeral {numeral.to_unicode_str()} and a constant named "
361
+ f"{constant.name!r} are one symbol on such a route (a numeral is the "
362
+ f"constant of its value, written as its digits). Write the number as a "
363
+ f"constant of another name, or rename the constant.")
364
+ for variable, constant in ((a, b), (b, a)):
365
+ if isinstance(variable, Variable) and isinstance(constant, Constant):
366
+ return (f"A free variable is a parameter, one unknown element of the problem, and "
367
+ f"not the constant named {variable.name!r}; a symbol named by its text "
368
+ f"cannot tell them apart. Rename the constant or the variable.")
369
+ return ("They are different terms with one written form, which a letter named by text "
370
+ "cannot tell apart. Rename one of the symbols.")
371
+
372
+
373
+ class AtomKeys:
374
+ """The keys of the atoms of ONE problem, refusing two different atoms with one key.
375
+
376
+ Create one per problem and pass every atom of it through :meth:`key`; the first atom
377
+ that has the key of a different atom ends in a ``NotImplementedError`` that names both.
378
+ Two atoms are the same atom when their nodes are equal after each sorted constant has
379
+ been read as the plain constant (``Number(1)`` and ``Number(1.0)`` are one node). A route
380
+ that reads a valuation from its caller passes each atom through :meth:`find` instead, which
381
+ does the same and then looks the atom up in the caller's table under either spelling.
382
+
383
+ Args:
384
+ route: what to call the route in a refusal (``'is_tautology'``, ``'int_prove'``).
385
+ sorted_constants: ``"read"`` (the default) keys ``c:S`` as the constant ``c``;
386
+ ``"refuse"`` refuses a sorted constant by name, for a route that has no reading of
387
+ what membership of a sort means for its values.
388
+ error: the exception class a refusal raises, ``NotImplementedError`` by default; a
389
+ route whose callers already handle another class for the input it refuses
390
+ (``ValueError``) names it here.
391
+ """
392
+
393
+ def __init__(self, route: str, sorted_constants: str = "read",
394
+ error: Type[Exception] = NotImplementedError) -> None:
395
+ if sorted_constants not in ("read", "refuse"):
396
+ raise ValueError(f"AtomKeys: sorted_constants must be 'read' or 'refuse', "
397
+ f"got {sorted_constants!r}.")
398
+ self._route = route
399
+ self._refuse_sorted = sorted_constants == "refuse"
400
+ self._error = error
401
+ self._atoms: Dict[str, Node] = {}
402
+
403
+ def key(self, atom: Node) -> str:
404
+ """The key of ``atom``, which is also recorded as the atom that has it.
405
+
406
+ Raises:
407
+ NotImplementedError: the atom holds a sorted constant on a route that refuses
408
+ it, the key would hold a complete quoted constant (that of
409
+ ``Constant("'a'")`` is also the formula text of the atom over the constant
410
+ ``a``), or a different atom already has the same key (the class ``error`` of
411
+ the constructor).
412
+ """
413
+ return self._record(atom)[0]
414
+
415
+ def find(self, table: Container[str], atom: Node) -> Optional[str]:
416
+ """The key under which a caller's ``table`` holds ``atom``, or ``None``.
417
+
418
+ What :func:`find_key` does, after what :meth:`key` does: the atom is recorded and
419
+ checked against the other atoms of the problem first, so the refusal of two different
420
+ atoms with one key keeps working where a route reads a caller's valuation. ``table`` is
421
+ what the caller's valuation offers for ``in`` (a set of keys, a dict from key to value).
422
+
423
+ Raises:
424
+ ValueError: ``table`` is a mapping that holds both spellings of the atom (its key
425
+ and its text as a formula) with different values.
426
+ NotImplementedError: as :meth:`key`.
427
+ """
428
+ key, plain, quoted = self._record(atom)
429
+ return _locate(table, key, plain, quoted, self._route)
430
+
431
+ def _record(self, atom: Node) -> Tuple[str, Node, bool]:
432
+ """``(key, plain atom, whether its formula text differs)`` of ``atom``, recorded."""
433
+ if self._refuse_sorted:
434
+ refuse_sorted_constant(atom, self._route, self._error)
435
+ key, plain, quoted = _key(atom, self._route, self._error)
436
+ known = self._atoms.setdefault(key, plain)
437
+ if known is not plain and known != plain:
438
+ a, b = _first_difference(known, plain)
439
+ # "have one key": as formulas the two atoms are written differently (a constant
440
+ # named like a variable is written in quotes), and it is the key, the text with
441
+ # every constant written by its name, that they share.
442
+ raise self._error(
443
+ f"{self._route}: two different atoms have one key and are both written "
444
+ f"{key!r}: one has the term {a!r} where the other has {b!r}. This route "
445
+ f"names an atom by its written form, so it would read them as one letter and "
446
+ f"answer about another problem. {_why_alike(a, b)}")
447
+ return key, plain, quoted
448
+
449
+ def letters(self, formulas: Iterable[Node]) -> List[str]:
450
+ """The distinct keys of the atoms of ``formulas`` that are letters, in first-seen order.
451
+
452
+ The truth constants ``$true`` and ``$false`` are not letters and are left out.
453
+ """
454
+ keys: List[str] = []
455
+ seen = set()
456
+ for formula in formulas:
457
+ for node in formula.walk():
458
+ if isinstance(node, Atom) and truth_value(node) is None:
459
+ key = self.key(node)
460
+ if key not in seen:
461
+ seen.add(key)
462
+ keys.append(key)
463
+ return keys
464
+
465
+
466
+ def agent_key(agent: Node) -> str:
467
+ """The name of the relation an operator of ``agent`` reads: its name, else its key text.
468
+
469
+ A modal operator of an agent (``K_a``, ``B_a``, ``Say_a``, ``Want_a``) reads the relation
470
+ named ``"K:" + agent_key(a)`` (and so on) of a model, the convention of
471
+ :mod:`~unicode_logic_kit.semantics.kripke`. A term without a name of its own (a numeral)
472
+ is keyed by its text with every constant written by its bare name, like an atom.
473
+ """
474
+ return getattr(agent, "name", None) or key_text(agent)
475
+
476
+
477
+ def _agents_of(node: Node) -> Tuple[Node, ...]:
478
+ """The agent terms of one modal operator (none for any other node)."""
479
+ if isinstance(node, (Knows, Believes, Says, Wants)):
480
+ return (node.agent,)
481
+ if isinstance(node, (EverybodyKnows, DistributedKnowledge, CommonKnowledge)):
482
+ return tuple(node.group)
483
+ return ()
484
+
485
+
486
+ def refuse_alike_agents(formulas: Iterable[Node], route: str) -> None:
487
+ """Refuse two different agent terms that would name ONE relation, by name.
488
+
489
+ The operators of an agent are named by :func:`agent_key`, so two different terms with one
490
+ key (the numeral ``1`` and the constant ``'1'``; a free variable ``x`` and the constant
491
+ ``x``) would be one agent on a route that files a relation under that name, and a formula
492
+ about two agents would be read as one about a single agent.
493
+
494
+ Raises:
495
+ NotImplementedError: two different agent terms of ``formulas`` have one
496
+ :func:`agent_key`.
497
+ """
498
+ agents: Dict[str, Node] = {}
499
+ for formula in formulas:
500
+ for node in formula.walk():
501
+ for agent in _agents_of(node):
502
+ agent = _plain(agent) # ``a:S`` is the agent ``a``
503
+ key = agent_key(agent)
504
+ known = agents.setdefault(key, agent)
505
+ if known != agent:
506
+ raise NotImplementedError(
507
+ f"{route}: two different agents are both named {key!r}: {known!r} "
508
+ f"and {agent!r}. This route files the operators of an agent under "
509
+ f"the relation of that name, so it would read them as ONE agent and "
510
+ f"answer about another problem. {_why_alike(known, agent)}")