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,637 @@
1
+ """Modal frame conditions and the named axioms they correspond to.
2
+
3
+ The kit reasons about modality on six routes — the standard translation
4
+ (:mod:`unicode_logic_kit.fol.qml`), a labelled tableau
5
+ (:mod:`unicode_logic_kit.atp.modal_tableau`), finite-frame enumeration
6
+ (:mod:`unicode_logic_kit.atp.kripke_enum`), natural deduction
7
+ (:mod:`unicode_logic_kit.atp.fitch`), the hybrid translation
8
+ (:mod:`unicode_logic_kit.fol.modal_translation`) and the higher-order
9
+ embeddings (:mod:`unicode_logic_kit.hol.isabelle_modal`,
10
+ :mod:`unicode_logic_kit.hol.thf_modal`). Each used to keep its OWN copy of the
11
+ frame table, which is how a route silently ended up understanding fewer
12
+ systems than its neighbour. This module is the single source of truth they
13
+ all read: what a named system consists of, what each condition means, and —
14
+ the part that makes it checkable — the modal axiom each condition
15
+ corresponds to.
16
+
17
+ The correspondence is not decoration. ``tests/test_modal_frames.py``
18
+ BRUTE-FORCES it: for every first-order condition here, over every frame on
19
+ up to three worlds and every valuation, "the axiom is valid on this frame"
20
+ and "the frame satisfies this condition" must agree. A wrong entry in this
21
+ module therefore fails a test rather than quietly teaching one route a
22
+ different modal logic than another.
23
+
24
+ The Geach family
25
+ -----------------
26
+ Most named conditions are instances of ONE schema. With the Scott–Lemmon
27
+ (Geach) coordinates ``G(m, n, r, s)``::
28
+
29
+ axiom ◇^m □^n p → □^r ◇^s p
30
+ condition ∀w,u,v (w R^m u ∧ w R^r v → ∃t (u R^n t ∧ v R^s t))
31
+
32
+ reflexivity is ``G(0,1,0,0)``, transitivity ``G(0,1,2,0)``, symmetry
33
+ ``G(0,0,1,1)``, seriality ``G(0,1,0,1)``, euclideanness ``G(1,0,1,1)``,
34
+ directedness ``G(1,1,1,1)``, partial functionality ``G(1,0,1,0)`` and
35
+ density ``G(0,2,1,0)``. The named conditions keep their own hand-written
36
+ axioms (readable, and unchanged since before this module existed), and a
37
+ test proves each equivalent to the Geach-generated one — so the unification
38
+ is machine-checked rather than asserted in a comment.
39
+
40
+ ``G(m,n,r,s)`` is also accepted directly as a frame name, which is what
41
+ makes the infinite family reachable without inventing a name per instance::
42
+
43
+ qml_is_valid(formula, frame="G(1,1,1,1)") # ≡ frame=".2"-style
44
+
45
+ What is NOT first-order definable
46
+ ----------------------------------
47
+ Three named axioms have no first-order frame condition at all: Löb
48
+ (``□(□p→p)→□p``, the GL system), McKinsey (``□◇p→◇□p``, S4.1) and
49
+ Grzegorczyk (``□(□(p→□p)→p)→p``, Grz). They are marked
50
+ ``first_order=False`` and carried ONLY by the higher-order routes, which
51
+ assert the schema itself, quantified over propositions. Every route that
52
+ reasons over an unbounded domain of worlds via a single first-order sentence
53
+ (:mod:`unicode_logic_kit.fol.qml`, :mod:`unicode_logic_kit.fol.modal_translation`,
54
+ :mod:`unicode_logic_kit.atp.fitch`, the labelled tableau) refuses them by name
55
+ — the boundary is stated, never approximated.
56
+
57
+ On a *finite, bounded* frame each of the three still has a purely
58
+ STRUCTURAL characterisation instead of a first-order sentence — irreflexive
59
+ (Löb, alongside the separately-listed "trans"), antisymmetric (Grz,
60
+ alongside "refl"/"trans") and "every world reaches some terminal point, a
61
+ world whose only successor is itself" (McKinsey, as the registry only ever
62
+ uses it: alongside "refl"/"trans", i.e. restricted to preorders). That is
63
+ what lets :func:`holds_on_finite_frame` decide all three and
64
+ :mod:`unicode_logic_kit.atp.kripke_enum` refute them by bounded finite-model
65
+ search — checked, not merely claimed, in ``tests/test_modal_frame_registry.py``
66
+ and ``tests/test_finite_frame_conditions.py``. It does not make them
67
+ first-order over frames of every cardinality, so :func:`unguarded_frame_axiom`
68
+ and the routes named above still refuse them exactly as before.
69
+ """
70
+
71
+ import re
72
+ from typing import Callable, Dict, FrozenSet, Iterable, Optional, Tuple
73
+
74
+ from ._identifiers import variable_pattern
75
+ from .nodes import (
76
+ And, Atom, Box, Diamond, Implies, Node, Not, Or, Quantifier, Variable,
77
+ )
78
+
79
+ __all__ = [
80
+ "FrameCondition", "FRAME_CONDITIONS", "FRAMES", "GeachSpec",
81
+ "resolve_frame", "parse_geach", "geach_axiom", "modal_axiom",
82
+ "MODAL_AXIOMS", "holds_on_finite_frame", "is_first_order",
83
+ "UnsupportedFrameCondition", "unguarded_frame_axiom",
84
+ "require_supported", "AXIOM_ALIASES",
85
+ ]
86
+
87
+
88
+ class UnsupportedFrameCondition(NotImplementedError):
89
+ """A route was handed a frame condition it cannot express soundly.
90
+
91
+ Raised instead of ignoring the condition — a route that silently drops
92
+ "directed" from ``S4.2`` would report countermodels that the frame
93
+ class excludes, which is exactly the failure this exception exists to
94
+ make impossible.
95
+ """
96
+
97
+
98
+ # ---------------------------------------------------------------------------
99
+ # The Geach / Scott–Lemmon family
100
+ # ---------------------------------------------------------------------------
101
+
102
+ class GeachSpec(tuple):
103
+ """The four Scott–Lemmon coordinates ``(m, n, r, s)`` of ``◇^m □^n p →
104
+ □^r ◇^s p``."""
105
+
106
+ __slots__ = ()
107
+
108
+ def __new__(cls, m: int, n: int, r: int, s: int):
109
+ for value in (m, n, r, s):
110
+ if not isinstance(value, int) or isinstance(value, bool) or value < 0:
111
+ raise ValueError(
112
+ f"GeachSpec: coordinates must be non-negative ints, got "
113
+ f"{(m, n, r, s)!r}")
114
+ return super().__new__(cls, (m, n, r, s))
115
+
116
+ @property
117
+ def m(self) -> int:
118
+ return self[0]
119
+
120
+ @property
121
+ def n(self) -> int:
122
+ return self[1]
123
+
124
+ @property
125
+ def r(self) -> int:
126
+ return self[2]
127
+
128
+ @property
129
+ def s(self) -> int:
130
+ return self[3]
131
+
132
+ def __repr__(self) -> str: # pragma: no cover - repr
133
+ return f"G({self.m},{self.n},{self.r},{self.s})"
134
+
135
+
136
+ def parse_geach(name: str) -> Optional[GeachSpec]:
137
+ """``"G(1,1,1,1)"`` → :class:`GeachSpec`; ``None`` if ``name`` is not one.
138
+
139
+ Accepts the condition spelling ``"geach:1,1,1,1"`` too — that is how a
140
+ Geach condition travels inside a frame's condition tuple, so every route
141
+ can keep treating conditions as plain strings.
142
+ """
143
+ text = name.strip()
144
+ if text.startswith("geach:"):
145
+ body = text[len("geach:"):]
146
+ elif text.startswith("G(") and text.endswith(")"):
147
+ body = text[2:-1]
148
+ else:
149
+ return None
150
+ parts = [p.strip() for p in body.split(",")]
151
+ if len(parts) != 4 or not all(p.isdigit() for p in parts):
152
+ raise ValueError(
153
+ f"frames: {name!r} is not a Geach spec — expected G(m,n,r,s) with "
154
+ "four non-negative integers, e.g. G(1,1,1,1)")
155
+ return GeachSpec(*(int(p) for p in parts))
156
+
157
+
158
+ def geach_condition_name(spec: GeachSpec) -> str:
159
+ """The condition string a frame table stores for ``spec``."""
160
+ return f"geach:{spec.m},{spec.n},{spec.r},{spec.s}"
161
+
162
+
163
+ def _iterate(op, times: int, formula: Node) -> Node:
164
+ for _ in range(times):
165
+ formula = op(formula)
166
+ return formula
167
+
168
+
169
+ def geach_axiom(spec: GeachSpec, atom: str = "P") -> Node:
170
+ """The modal schema ``◇^m □^n p → □^r ◇^s p`` for ``spec``."""
171
+ p = Atom(atom, ())
172
+ left = _iterate(Diamond, spec.m, _iterate(Box, spec.n, p))
173
+ right = _iterate(Box, spec.r, _iterate(Diamond, spec.s, p))
174
+ return Implies(left, right)
175
+
176
+
177
+ # ---------------------------------------------------------------------------
178
+ # Named axioms
179
+ # ---------------------------------------------------------------------------
180
+
181
+ def _p(name: str = "P") -> Node:
182
+ return Atom(name, ())
183
+
184
+
185
+ #: Axiom name → builder of its modal schema over propositional letters.
186
+ #: Two-letter schemas take both names; the rest ignore the second.
187
+ MODAL_AXIOMS: Dict[str, Callable[..., Node]] = {
188
+ # The Geach instances (their frame conditions are in FRAME_CONDITIONS).
189
+ "T": lambda p="P", q="Q": Implies(Box(_p(p)), _p(p)),
190
+ "D": lambda p="P", q="Q": Implies(Box(_p(p)), Diamond(_p(p))),
191
+ "B": lambda p="P", q="Q": Implies(_p(p), Box(Diamond(_p(p)))),
192
+ "4": lambda p="P", q="Q": Implies(Box(_p(p)), Box(Box(_p(p)))),
193
+ "5": lambda p="P", q="Q": Implies(Diamond(_p(p)), Box(Diamond(_p(p)))),
194
+ "CD": lambda p="P", q="Q": Implies(Diamond(_p(p)), Box(_p(p))),
195
+ "C4": lambda p="P", q="Q": Implies(Box(Box(_p(p))), Box(_p(p))),
196
+ ".2": lambda p="P", q="Q": Implies(Diamond(Box(_p(p))), Box(Diamond(_p(p)))),
197
+ # Not Geach instances, but still first-order.
198
+ ".3": lambda p="P", q="Q": Or(Box(Implies(Box(_p(p)), _p(q))),
199
+ Box(Implies(Box(_p(q)), _p(p)))),
200
+ "Mshift": lambda p="P", q="Q": Box(Implies(Box(_p(p)), _p(p))),
201
+ "Ver": lambda p="P", q="Q": Box(_p(p)),
202
+ # No first-order frame condition exists for these three.
203
+ "McKinsey": lambda p="P", q="Q": Implies(Box(Diamond(_p(p))),
204
+ Diamond(Box(_p(p)))),
205
+ "Loeb": lambda p="P", q="Q": Implies(Box(Implies(Box(_p(p)), _p(p))),
206
+ Box(_p(p))),
207
+ "Grz": lambda p="P", q="Q": Implies(
208
+ Box(Implies(Box(Implies(_p(p), Box(_p(p)))), _p(p))), _p(p)),
209
+ # Theorems of every normal modal logic — no frame condition, listed so a
210
+ # caller can ask for the formula and check that the kit validates it.
211
+ "K": lambda p="P", q="Q": Implies(Box(Implies(_p(p), _p(q))),
212
+ Implies(Box(_p(p)), Box(_p(q)))),
213
+ "N": lambda p="P", q="Q": Box(Or(_p(p), Not(_p(p)))),
214
+ "P": lambda p="P", q="Q": Not(Box(And(_p(p), Not(_p(p))))),
215
+ }
216
+
217
+ #: The alternative names the literature uses, mapped onto this module's.
218
+ #: ``M`` is deliberately NOT accepted for ``T``: this module uses ``Mshift``
219
+ #: for the shift-reflexivity axiom ``□(□p→p)``, and one letter cannot mean
220
+ #: both without inviting exactly the mistake a frame table must not make.
221
+ AXIOM_ALIASES: Dict[str, str] = {
222
+ "G": ".2", "G1": ".2", "C": ".2", # ◇□p → □◇p, one axiom
223
+ "H": ".3", # □(□p→q) ∨ □(□q→p)
224
+ "Q": "C4", # □□p → □p, one axiom
225
+ "W": "Loeb", # □(□p→p) → □p, one axiom
226
+ "L": "Loeb",
227
+ "Alt1": "CD", "Alt3": "CD", # ◇p→□p, ◇p∧◇q→◇(p∧q)
228
+ "Alt2": "Mshift", # ◇p→◇(p∧◇p)
229
+ "Dense": "C4", "Density": "C4",
230
+ ".1": "McKinsey",
231
+ "Grzegorczyk": "Grz",
232
+ }
233
+
234
+
235
+ def modal_axiom(name: str, p: str = "P", q: str = "Q") -> Node:
236
+ """The modal schema of a named axiom, over the given letters.
237
+
238
+ Accepts the aliases the literature uses (:data:`AXIOM_ALIASES`), so
239
+ ``modal_axiom("W")`` and ``modal_axiom("Loeb")`` build the same formula.
240
+ """
241
+ key = AXIOM_ALIASES.get(name, name)
242
+ if key not in MODAL_AXIOMS:
243
+ raise ValueError(
244
+ f"frames: unknown modal axiom {name!r} (use one of "
245
+ f"{sorted(MODAL_AXIOMS)} or an alias {sorted(AXIOM_ALIASES)})")
246
+ return MODAL_AXIOMS[key](p, q)
247
+
248
+
249
+ # ---------------------------------------------------------------------------
250
+ # Frame conditions
251
+ # ---------------------------------------------------------------------------
252
+
253
+ class FrameCondition(tuple):
254
+ """One frame condition: what it is called, what it means, which axiom it
255
+ corresponds to, and whether it is first-order definable at all."""
256
+
257
+ __slots__ = ()
258
+
259
+ def __new__(cls, name: str, axiom: str, description: str,
260
+ geach: Optional[GeachSpec] = None, first_order: bool = True):
261
+ return super().__new__(cls, (name, axiom, description, geach,
262
+ first_order))
263
+
264
+ name = property(lambda self: self[0])
265
+ axiom = property(lambda self: self[1])
266
+ description = property(lambda self: self[2])
267
+ geach = property(lambda self: self[3])
268
+ first_order = property(lambda self: self[4])
269
+
270
+
271
+ #: Condition name → :class:`FrameCondition`. The ``geach`` coordinates are
272
+ #: checked against the hand-written axiom by the test suite.
273
+ FRAME_CONDITIONS: Dict[str, FrameCondition] = {
274
+ "refl": FrameCondition(
275
+ "refl", "T", "∀w Rww — reflexive", GeachSpec(0, 1, 0, 0)),
276
+ "trans": FrameCondition(
277
+ "trans", "4", "∀w,v,u (Rwv ∧ Rvu → Rwu) — transitive",
278
+ GeachSpec(0, 1, 2, 0)),
279
+ "sym": FrameCondition(
280
+ "sym", "B", "∀w,v (Rwv → Rvw) — symmetric", GeachSpec(0, 0, 1, 1)),
281
+ "serial": FrameCondition(
282
+ "serial", "D", "∀w ∃v Rwv — serial", GeachSpec(0, 1, 0, 1)),
283
+ "eucl": FrameCondition(
284
+ "eucl", "5", "∀w,v,u (Rwv ∧ Rwu → Rvu) — euclidean",
285
+ GeachSpec(1, 0, 1, 1)),
286
+ "directed": FrameCondition(
287
+ "directed", ".2",
288
+ "∀w,v,u (Rwv ∧ Rwu → ∃t (Rvt ∧ Rut)) — directed / convergent",
289
+ GeachSpec(1, 1, 1, 1)),
290
+ "functional": FrameCondition(
291
+ "functional", "CD",
292
+ "∀w,v,u (Rwv ∧ Rwu → v = u) — partially functional (at most one "
293
+ "successor)", GeachSpec(1, 0, 1, 0)),
294
+ "dense": FrameCondition(
295
+ "dense", "C4", "∀w,v (Rwv → ∃u (Rwu ∧ Ruv)) — dense",
296
+ GeachSpec(0, 2, 1, 0)),
297
+ "connected": FrameCondition(
298
+ "connected", ".3",
299
+ "∀w,v,u (Rwv ∧ Rwu → Rvu ∨ Ruv) — connected / no branching to the "
300
+ "right. Note there is deliberately NO 'or v = u' escape: that weaker "
301
+ "condition belongs to the OTHER common .3 formulation "
302
+ "(◇p ∧ ◇q → ◇(p ∧ ◇q) ∨ ◇(q ∧ ◇p)); for the □(□p→q) ∨ □(□q→p) form "
303
+ "used here the exact correspondent needs Rvv when v = u, and the "
304
+ "brute-force correspondence test confirms it on every frame up to "
305
+ "three worlds. Over reflexive frames the two coincide"),
306
+ "shift_refl": FrameCondition(
307
+ "shift_refl", "Mshift",
308
+ "∀w,v (Rwv → Rvv) — shift-reflexive (every accessible world is "
309
+ "reflexive; the axiom □(□p→p), also reached by ◇p→◇(p∧◇p))"),
310
+ "empty": FrameCondition(
311
+ "empty", "Ver", "∀w,v ¬Rwv — no accessible worlds at all (the Verum "
312
+ "system, where □p holds vacuously)"),
313
+ # Not first-order definable: carried only by the higher-order routes.
314
+ "loeb": FrameCondition(
315
+ "loeb", "Loeb",
316
+ "Gödel–Löb provability: transitive + converse well-founded — NOT "
317
+ "first-order definable; the higher-order routes assert the Löb schema "
318
+ "itself",
319
+ first_order=False),
320
+ "mckinsey": FrameCondition(
321
+ "mckinsey", "McKinsey",
322
+ "NOT first-order definable; the higher-order routes assert the "
323
+ "McKinsey schema □◇p → ◇□p itself", first_order=False),
324
+ "grz": FrameCondition(
325
+ "grz", "Grz",
326
+ "reflexive + transitive + antisymmetric (a partial order) — NOT "
327
+ "first-order definable over frames of every cardinality (this finite "
328
+ "characterisation, checked by holds_on_finite_frame, replaces the "
329
+ "vacuous-on-finite-frames 'no infinite ascending chain' condition; "
330
+ "antisymmetry is the load-bearing part — refl+trans alone is NOT "
331
+ "enough, e.g. the two-world full cluster is refl+trans but not "
332
+ "antisymmetric and fails the Grz schema); the higher-order routes "
333
+ "assert the Grzegorczyk schema itself", first_order=False),
334
+ }
335
+
336
+
337
+ def is_first_order(condition: str) -> bool:
338
+ """Whether ``condition`` has a first-order frame condition at all."""
339
+ if parse_geach(condition) is not None:
340
+ return True
341
+ spec = FRAME_CONDITIONS.get(condition)
342
+ if spec is None:
343
+ raise ValueError(f"frames: unknown frame condition {condition!r}")
344
+ return spec.first_order
345
+
346
+
347
+ # ---------------------------------------------------------------------------
348
+ # Named systems
349
+ # ---------------------------------------------------------------------------
350
+
351
+ #: System name → its frame conditions. ``D`` and ``KD`` (and ``B`` /
352
+ #: ``KTB``) are the same system under both spellings the literature uses.
353
+ FRAMES: Dict[str, Tuple[str, ...]] = {
354
+ "K": (),
355
+ "D": ("serial",),
356
+ "KD": ("serial",),
357
+ "T": ("refl",),
358
+ "KB": ("sym",),
359
+ "B": ("refl", "sym"), # Brouwer, K + T + B
360
+ "KTB": ("refl", "sym"),
361
+ "K4": ("trans",),
362
+ "K5": ("eucl",),
363
+ "K45": ("trans", "eucl"),
364
+ "KD4": ("serial", "trans"),
365
+ "KD5": ("serial", "eucl"),
366
+ "KD45": ("serial", "trans", "eucl"),
367
+ "S4": ("refl", "trans"),
368
+ "S5": ("refl", "trans", "sym"),
369
+ "S4.1": ("refl", "trans", "mckinsey"), # + McKinsey (HOL routes prove;
370
+ # kripke_enum refutes, bounded)
371
+ "S4.2": ("refl", "trans", "directed"), # convergent (.2)
372
+ "S4.3": ("refl", "trans", "connected"), # no-branching (.3)
373
+ "KCD": ("functional",), # ◇p → □p
374
+ "KC4": ("dense",), # □□p → □p
375
+ "KShift": ("shift_refl",), # □(□p → p)
376
+ "Ver": ("empty",), # □p — the Verum system
377
+ "GL": ("trans", "loeb"), # HOL routes prove; kripke_enum
378
+ # refutes, bounded
379
+ "Grz": ("refl", "trans", "grz"), # HOL routes prove; kripke_enum
380
+ # refutes, bounded
381
+ }
382
+
383
+
384
+ def resolve_frame(frame: str) -> Tuple[str, ...]:
385
+ """The conditions of a named system, or of a ``G(m,n,r,s)`` spec.
386
+
387
+ (Named ``resolve_frame`` rather than the more obvious
388
+ ``frame_conditions``: that would collide with the
389
+ :data:`FRAME_CONDITIONS` registry on a case-insensitive filesystem —
390
+ Sphinx writes one stub file per documented name, and the two would
391
+ overwrite each other on Windows.)
392
+
393
+ Raises:
394
+ ValueError: ``frame`` is neither a known system name nor a
395
+ well-formed Geach spec — the message lists what is available.
396
+ """
397
+ spec = parse_geach(frame)
398
+ if spec is not None:
399
+ return (geach_condition_name(spec),)
400
+ if frame not in FRAMES:
401
+ raise ValueError(
402
+ f"unknown frame {frame!r} (use one of {sorted(FRAMES)}, or a "
403
+ "Scott–Lemmon spec like 'G(1,1,1,1)')")
404
+ return FRAMES[frame]
405
+
406
+
407
+ def require_supported(route: str, conditions: Iterable[str],
408
+ supported: Iterable[str], *, hint: str = "") -> None:
409
+ """Refuse, by name, any condition a route cannot express soundly.
410
+
411
+ The alternative — ignoring it — would make the route answer about a
412
+ LARGER frame class than the caller asked for, i.e. report countermodels
413
+ that the named system excludes. Every route calls this instead.
414
+ """
415
+ allowed = set(supported)
416
+ for condition in conditions:
417
+ if condition in allowed:
418
+ continue
419
+ if parse_geach(condition) is not None and "geach" in allowed:
420
+ continue
421
+ spec = FRAME_CONDITIONS.get(condition)
422
+ what = spec.description if spec is not None else condition
423
+ raise UnsupportedFrameCondition(
424
+ f"{route}: the frame condition {condition!r} ({what}) is not "
425
+ f"expressible here." + (f" {hint}" if hint else ""))
426
+
427
+
428
+ # ---------------------------------------------------------------------------
429
+ # Finite frames — the checker the enumerator and the tests share
430
+ # ---------------------------------------------------------------------------
431
+
432
+ def _steps(edges: FrozenSet[Tuple[int, int]], n: int,
433
+ k: int) -> FrozenSet[Tuple[int, int]]:
434
+ """``R^k`` over ``range(n)``: pairs joined by a path of EXACTLY ``k``
435
+ edges (``R^0`` is the identity)."""
436
+ reachable = {(w, w) for w in range(n)}
437
+ for _ in range(k):
438
+ reachable = {(w, v) for (w, mid) in reachable
439
+ for (mid2, v) in edges if mid == mid2}
440
+ return frozenset(reachable)
441
+
442
+
443
+ def holds_on_finite_frame(condition: str, edges: FrozenSet[Tuple[int, int]],
444
+ n: int) -> bool:
445
+ """Does the finite frame ``(range(n), edges)`` satisfy ``condition``?
446
+
447
+ Every FIRST-ORDER condition in :data:`FRAME_CONDITIONS` is decided here,
448
+ Geach specs included. The three conditions marked ``first_order=False``
449
+ (``"loeb"``, ``"grz"``, ``"mckinsey"``) are decided here TOO, via the
450
+ structural characterisation each has on a finite frame instead of a
451
+ first-order sentence valid at every cardinality: ``"loeb"`` checks
452
+ irreflexivity (combined with the separately-listed ``"trans"`` by the
453
+ ``"GL"`` system, that is "transitive + irreflexive", the standard
454
+ finite Gödel–Löb characterisation), ``"grz"`` checks antisymmetry
455
+ (combined with ``"refl"``/``"trans"`` by the ``"Grz"`` system, that is a
456
+ finite partial order), and ``"mckinsey"`` checks "every world reaches
457
+ some terminal point, a world whose only successor is itself" — the
458
+ registry only ever uses ``"mckinsey"`` alongside ``"refl"``/``"trans"``
459
+ (``"S4.1"``), i.e. restricted to preorders, and the characterisation is
460
+ scoped to exactly that generality. Delegating a finite check to
461
+ ``entry.geach is None`` → :func:`_named_holds` covers all three, since
462
+ none of them is a Geach instance. This does NOT make them first-order
463
+ over frames of every cardinality — :func:`unguarded_frame_axiom` and the
464
+ routes that reason over an unbounded domain of worlds still refuse them
465
+ by name (see the module docstring). An unknown condition name still
466
+ raises :class:`ValueError`.
467
+ """
468
+ spec = parse_geach(condition)
469
+ if spec is None:
470
+ entry = FRAME_CONDITIONS.get(condition)
471
+ if entry is None:
472
+ raise ValueError(f"frames: unknown frame condition {condition!r}")
473
+ if entry.geach is None:
474
+ return _named_holds(condition, edges, n)
475
+ spec = entry.geach
476
+ from_u = _steps(edges, n, spec.n)
477
+ from_v = _steps(edges, n, spec.s)
478
+ left = _steps(edges, n, spec.m)
479
+ right = _steps(edges, n, spec.r)
480
+ for (w, u) in left:
481
+ for (w2, v) in right:
482
+ if w != w2:
483
+ continue
484
+ if not any((u, t) in from_u and (v, t) in from_v
485
+ for t in range(n)):
486
+ return False
487
+ return True
488
+
489
+
490
+ def _named_holds(condition: str, edges: FrozenSet[Tuple[int, int]],
491
+ n: int) -> bool:
492
+ """The conditions that are not Geach instances: the hand-written
493
+ first-order ones, plus the finite-frame structural checks for the three
494
+ conditions with no first-order frame axiom at every cardinality (Löb,
495
+ Grz, McKinsey — see :func:`holds_on_finite_frame`'s docstring)."""
496
+ if condition == "connected":
497
+ return all(not ((w, v) in edges and (w, u) in edges)
498
+ or (v, u) in edges or (u, v) in edges
499
+ for w in range(n) for v in range(n) for u in range(n))
500
+ if condition == "shift_refl":
501
+ return all((v, v) in edges for (_w, v) in edges)
502
+ if condition == "empty":
503
+ return not edges
504
+ if condition == "loeb":
505
+ # Gödel–Löb, finite characterisation: irreflexive. Combined with the
506
+ # "trans" the "GL" system supplies separately, this is "transitive +
507
+ # irreflexive" — on a FINITE frame that is exactly "transitive +
508
+ # converse well-founded" (a cycle, which transitivity would collapse
509
+ # into a self-loop, is the only way a finite domain could fail to be
510
+ # well-founded, and irreflexivity forbids self-loops).
511
+ return all((w, w) not in edges for w in range(n))
512
+ if condition == "grz":
513
+ # Grzegorczyk, finite characterisation: antisymmetric. Combined with
514
+ # "refl"/"trans" the "Grz" system supplies separately, this is a
515
+ # finite partial order — "no infinite strictly-ascending chain" is
516
+ # automatic once the frame is finite, so antisymmetry is the only
517
+ # load-bearing extra condition (see FRAME_CONDITIONS["grz"]).
518
+ return all(w == v for w in range(n) for v in range(n)
519
+ if (w, v) in edges and (v, w) in edges)
520
+ if condition == "mckinsey":
521
+ # McKinsey, restricted to preorders — the only generality the
522
+ # registry ever needs it in (alongside "refl"/"trans" via "S4.1"):
523
+ # every world reaches some terminal point, a world whose only
524
+ # successor is itself. One R-step suffices for "reaches" because the
525
+ # frame is transitive, so R already contains every multi-step path.
526
+ def _is_terminal(v: int) -> bool:
527
+ successors = {u for (w2, u) in edges if w2 == v}
528
+ return successors == {v}
529
+ return all(any((w, v) in edges and _is_terminal(v) for v in range(n))
530
+ for w in range(n))
531
+ raise ValueError(f"frames: unknown frame condition {condition!r}")
532
+
533
+
534
+ # ---------------------------------------------------------------------------
535
+ # One first-order emitter for the routes that need no world guard
536
+ # ---------------------------------------------------------------------------
537
+
538
+ _FORALL, _EXISTS = "∀", "∃"
539
+
540
+
541
+ def unguarded_frame_axiom(condition: str, relation: str = "R", *,
542
+ prefix: str = "v") -> Node:
543
+ """The first-order frame axiom of ``condition`` over ``relation``.
544
+
545
+ Written WITHOUT a sort guard, which is what the natural-deduction route
546
+ (:mod:`unicode_logic_kit.atp.fitch`) and the hybrid translation
547
+ (:mod:`unicode_logic_kit.fol.modal_translation`) need — both quantify over
548
+ worlds only. The quantified-modal route
549
+ (:mod:`unicode_logic_kit.fol.qml`) keeps its own ``World``-guarded
550
+ emitters, because there worlds and objects share one domain.
551
+
552
+ The axiom is closed over its own bound variables (named ``prefix`` + a
553
+ digit), so it can never capture anything in the formula it is conjoined
554
+ with. ``prefix`` must be a legal VARIABLE on its own — one term-valued
555
+ letter — because ``prefix`` + digits has to be a name the kit's own parser
556
+ reads back: the default used to be ``"_fw"``, and an axiom printed as
557
+ ``∀_fw0 R(_fw0, _fw0)`` is text ``api.parse_any`` rejects (see
558
+ :func:`unicode_logic_kit.fol._identifiers.fresh_variables`).
559
+
560
+ Raises:
561
+ UnsupportedFrameCondition: ``condition`` has no first-order frame
562
+ condition (Löb, McKinsey, Grz).
563
+ ValueError: ``condition`` is not a known condition at all.
564
+ """
565
+ if not re.fullmatch(variable_pattern(), prefix):
566
+ raise ValueError(
567
+ f"unguarded_frame_axiom: prefix {prefix!r} is not a legal variable "
568
+ f"name, so {prefix!r} + digits is not one either — the axiom would "
569
+ f"print as text this kit's own parser rejects")
570
+ w, u, v, s = (Variable(f"{prefix}0"), Variable(f"{prefix}1"),
571
+ Variable(f"{prefix}2"), Variable(f"{prefix}3"))
572
+ R = lambda a, b: Atom(relation, (a, b))
573
+ fa = lambda var, body: Quantifier(_FORALL, var, body)
574
+ ex = lambda var, body: Quantifier(_EXISTS, var, body)
575
+
576
+ spec = parse_geach(condition)
577
+ if spec is None:
578
+ entry = FRAME_CONDITIONS.get(condition)
579
+ if entry is None:
580
+ raise ValueError(f"frames: unknown frame condition {condition!r}")
581
+ if not entry.first_order:
582
+ raise UnsupportedFrameCondition(
583
+ f"frames: {condition!r} ({entry.description}) has no "
584
+ "first-order frame axiom")
585
+ if condition == "refl":
586
+ return fa(w, R(w, w))
587
+ if condition == "trans":
588
+ return fa(w, fa(u, fa(v, Implies(And(R(w, u), R(u, v)), R(w, v)))))
589
+ if condition == "sym":
590
+ return fa(w, fa(u, Implies(R(w, u), R(u, w))))
591
+ if condition == "serial":
592
+ return fa(w, ex(u, R(w, u)))
593
+ if condition == "eucl":
594
+ return fa(w, fa(u, fa(v, Implies(And(R(w, u), R(w, v)), R(u, v)))))
595
+ if condition == "directed":
596
+ return fa(w, fa(u, fa(v, Implies(
597
+ And(R(w, u), R(w, v)), ex(s, And(R(u, s), R(v, s)))))))
598
+ if condition == "connected":
599
+ return fa(w, fa(u, fa(v, Implies(
600
+ And(R(w, u), R(w, v)), Or(R(u, v), R(v, u))))))
601
+ if condition == "functional":
602
+ return fa(w, fa(u, fa(v, Implies(
603
+ And(R(w, u), R(w, v)), Atom("=", (u, v))))))
604
+ if condition == "dense":
605
+ return fa(w, fa(u, Implies(R(w, u), ex(v, And(R(w, v), R(v, u))))))
606
+ if condition == "shift_refl":
607
+ return fa(w, fa(u, Implies(R(w, u), R(u, u))))
608
+ if condition == "empty":
609
+ return fa(w, fa(u, Not(R(w, u))))
610
+ raise ValueError( # pragma: no cover - registry drift
611
+ f"frames: {condition!r} is registered but has no axiom builder")
612
+
613
+ counter = [0]
614
+
615
+ def path(a, b, k):
616
+ if k == 0:
617
+ return Atom("=", (a, b))
618
+ previous, mids, conj = a, [], None
619
+ for _ in range(k - 1):
620
+ z = Variable(f"{prefix}z{counter[0]}")
621
+ counter[0] += 1
622
+ mids.append(z)
623
+ step = R(previous, z)
624
+ conj = step if conj is None else And(conj, step)
625
+ previous = z
626
+ body = R(previous, b)
627
+ if conj is not None:
628
+ body = And(conj, body)
629
+ for z in reversed(mids):
630
+ body = ex(z, body)
631
+ return body
632
+
633
+ body = Implies(And(path(w, u, spec.m), path(w, v, spec.r)),
634
+ ex(s, And(path(u, s, spec.n), path(v, s, spec.s))))
635
+ for var in (v, u, w):
636
+ body = fa(var, body)
637
+ return body
@@ -0,0 +1,31 @@
1
+ # PREDICATE, CONSTANT, NAME, VARIABLE, and SORT used to be declared here as
2
+ # fixed ASCII regexes. They are now GENERATED AT RUNTIME by
3
+ # unicode_logic_kit/fol/_identifiers.py, from the running interpreter's own
4
+ # Unicode letter tables, and spliced directly into each mode's grammar text
5
+ # by _fol_nodes.build_grammar() — see _identifiers.py's module docstring for
6
+ # why a codepoint range belongs in generated code, not frozen into this file.
7
+ #
8
+ # NUMBER, FORALL, EXISTS, and LAMBDA are unaffected by that widening (none of
9
+ # them classifies a letter) and stay here, %import'd as before.
10
+ #
11
+ # NUMBER carries an optional leading "-" so that a NEGATIVE literal prints and
12
+ # reads back: Number(-3) renders as `-3`, and before this the text `P(-3)` was a
13
+ # syntax error. The sibling grammars (prover9_input, tptp_input) always had it.
14
+ # It sits next to the binary minus without taking it over: a NUMBER is only
15
+ # accepted where a term STARTS, so `x-3`, `x -3` and `x - 3` all stay
16
+ # subtraction, and `x - -3` is subtraction of -3. A count bound stays a
17
+ # non-negative integer: `∃≥-2 x P(x)` (and `∃≥2.5 x P(x)`) now reach the count
18
+ # rule (_count_transform / _sorted_count_transform), which refuses them with a
19
+ # CountBoundError, a ParsingError: a one-line SYNTAX_ERROR naming the bound, as
20
+ # for any other parse error, not a raw lark VisitError. Audited against the
21
+ # old grammar over 2455 `-`-adjacent texts in all 26 dialect/parser-kind
22
+ # combinations: no text that parsed before parses to a different tree or fails
23
+ # now.
24
+
25
+ NUMBER.1: /-?[0-9]+(\.[0-9]+)?/
26
+
27
+ FORALL: "∀"
28
+
29
+ EXISTS: "∃"
30
+
31
+ LAMBDA: "λ"