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,436 @@
1
+ """The Lambek calculus **L** — a complete, terminating decision procedure + a checker.
2
+
3
+ The Lambek calculus (Lambek 1958) is the sequent calculus of **ordered** resources:
4
+ on top of dropping weakening and contraction (as linear logic does) it drops
5
+ **exchange**, so a sequent's antecedent is a *sequence* of formulas, not a multiset,
6
+ and ``A, A\\B ⊢ B`` is derivable while ``A\\B, A ⊢ B`` is not. Antecedents must be
7
+ **nonempty** (Lambek's restriction — the version relevant to categorial grammar).
8
+ The connectives are the ``lambek`` parser mode's
9
+ (:mod:`unicode_logic_kit.fol._lambek_nodes`): ``•`` / ``\\`` / ``/``.
10
+
11
+ Rules (cut-free; ``Γ``, ``Δ`` range over sequences, ``Γ`` nonempty where stated)::
12
+
13
+ Ax: A ⊢ A
14
+ •L: Γ, A, B, Δ ⊢ C ⇒ Γ, A•B, Δ ⊢ C
15
+ •R: Γ ⊢ A and Δ ⊢ B ⇒ Γ, Δ ⊢ A•B (contiguous split, in order)
16
+ \\L: Γ ⊢ A and Δ1, B, Δ2 ⊢ C ⇒ Δ1, Γ, A\\B, Δ2 ⊢ C (Γ nonempty)
17
+ \\R: A, Γ ⊢ B ⇒ Γ ⊢ A\\B (Γ nonempty; A prepended)
18
+ /L: Γ ⊢ A and Δ1, B, Δ2 ⊢ C ⇒ Δ1, B/A, Γ, Δ2 ⊢ C (Γ nonempty)
19
+ /R: Γ, A ⊢ B ⇒ Γ ⊢ B/A (Γ nonempty; A appended)
20
+
21
+ **Decidability.** Every rule's premises have strictly smaller total size (node
22
+ count) than its conclusion, and there are finitely many rule instances per sequent,
23
+ so the backward search below — exhaustive, memoised on ``(sequence, goal)`` — is a
24
+ **complete, terminating decision procedure**: L is decidable, and
25
+ ``lambek_prove(...) is None`` *proves* underivability. No depth bound is needed.
26
+
27
+ Deliberately, there is **no classical export**: collapsing ``A\\B`` and ``B/A`` to
28
+ ``A → B`` forgets word order, which is the calculus's entire point. (The collapse is
29
+ *sound* — every L theorem collapses to a classical one, which the test-suite checks
30
+ against Z3 — but it identifies types L keeps apart.)
31
+
32
+ **What is read.** The calculus reads exactly the node classes it has rules for: ``•``
33
+ (``Product``), ``\\`` (``Under``) and ``/`` (``Over``), over atoms. An atom over terms
34
+ (``NP(john)``) is ONE category, identified by its predicate and its terms as written. Every
35
+ other node is refused by name with ``NotImplementedError``: a quantifier (sorted or not), a
36
+ counting or cardinality node, a sorted constant, an equality atom, and a node of another logic
37
+ (``And``, ``Or``, ``Not``, ``Box``, a connective of linear logic, ...), because reading it as one
38
+ more category would answer about another formula (``∀x P(x) ⊢ P(alpha)`` holds in every
39
+ first-order reading and has no derivation between the two categories; ``And(A, B) ⊢ A`` holds
40
+ classically and has none between ``And(A, B)`` and ``A``). Decide such input with a route of its
41
+ own logic.
42
+
43
+ Public API: :class:`LambekSequent`, :class:`LambekDerivation`, :func:`lambek_prove`,
44
+ :func:`lambek_derivable`, :func:`check_lambek_proof`, :func:`verify_lambek_proof`,
45
+ :func:`render_lambek_proof`.
46
+ """
47
+
48
+ from dataclasses import dataclass
49
+ from typing import Callable, Dict, Iterable, List, Optional, Tuple
50
+
51
+ from ..fol.nodes import Node, Product, Under, Over
52
+ from ..fol._truth_constants import refuse_truth_constants
53
+ from ._substructural_input import LAMBEK, refuse_unreadable_input, unreadable_reason
54
+ from .sequent import SequentResult
55
+
56
+ #: Why the Lambek calculus has no reading of the TPTP constants ``$true`` /
57
+ #: ``$false`` (the reason :func:`lambek_prove` and the Isabelle export name when
58
+ #: they refuse one).
59
+ _NO_TRUTH_CONSTANT_WHY = (
60
+ "the Lambek calculus has no propositional constants: its formulas are "
61
+ "category types built from atoms by •, \\ and /, and no type is true or "
62
+ "false of every sequence")
63
+
64
+
65
+ # ---------------------------------------------------------------------------
66
+ # Sequents and derivations
67
+ # ---------------------------------------------------------------------------
68
+
69
+ @dataclass(frozen=True)
70
+ class LambekSequent:
71
+ """A Lambek sequent ``antecedent ⊢ succedent``.
72
+
73
+ ``antecedent`` is a **nonempty sequence** (tuple) of formulas — order matters,
74
+ there is no exchange; ``succedent`` is one formula. Frozen and hashable.
75
+ """
76
+
77
+ antecedent: Tuple[Node, ...]
78
+ succedent: Node
79
+
80
+ def __post_init__(self):
81
+ """Coerce the antecedent to a tuple so the sequent stays hashable."""
82
+ object.__setattr__(self, "antecedent", tuple(self.antecedent))
83
+
84
+ def __str__(self) -> str:
85
+ """Render as ``A, B ⊢ C`` using each formula's Unicode form."""
86
+ left = ", ".join(f.to_unicode_str() for f in self.antecedent)
87
+ return f"{left} ⊢ {self.succedent.to_unicode_str()}"
88
+
89
+
90
+ @dataclass(frozen=True)
91
+ class LambekDerivation:
92
+ """A node of a Lambek-calculus derivation tree.
93
+
94
+ ``conclusion`` is the sequent this node derives; ``rule`` is the rule name
95
+ (``"Ax"``, ``"•R"``, ``"\\L"``, …); ``premises`` are the sub-derivations whose
96
+ conclusions are this rule's premises, **in the rule's written order** (for the
97
+ two-premise ``\\L`` / ``/L`` the argument premise ``Γ ⊢ A`` comes first; for
98
+ ``•R`` the left part of the split comes first).
99
+ """
100
+
101
+ conclusion: LambekSequent
102
+ rule: str
103
+ premises: Tuple["LambekDerivation", ...] = ()
104
+
105
+ def __post_init__(self):
106
+ """Coerce ``premises`` to a tuple so the derivation stays hashable."""
107
+ object.__setattr__(self, "premises", tuple(self.premises))
108
+
109
+ def render(self) -> str:
110
+ """Render this derivation as an indented proof tree."""
111
+ return render_lambek_proof(self)
112
+
113
+
114
+ def render_lambek_proof(derivation: "LambekDerivation", indent: int = 0) -> str:
115
+ """Render a derivation as an indented tree (conclusion first, premises below)."""
116
+ pad = " " * indent
117
+ lines = [f"{pad}{derivation.conclusion} [{derivation.rule}]"]
118
+ for premise in derivation.premises:
119
+ lines.append(render_lambek_proof(premise, indent + 1))
120
+ return "\n".join(lines)
121
+
122
+
123
+ def _size(f: Node) -> int:
124
+ """Return the node count of a formula (atomic categories count 1)."""
125
+ return 1 + sum(_size(c) for c in f._child_nodes())
126
+
127
+
128
+ # ---------------------------------------------------------------------------
129
+ # Backward proof search (a decision procedure — see the module docstring)
130
+ # ---------------------------------------------------------------------------
131
+
132
+ def _prove(seq: Tuple[Node, ...], goal: Node,
133
+ memo: Dict) -> Optional[LambekDerivation]:
134
+ """Exhaustive backward search for a derivation of ``seq ⊢ goal``.
135
+
136
+ Terminates because every premise's total node count is strictly smaller than
137
+ its conclusion's; memoised on ``(seq, goal)`` (both successes and failures —
138
+ sound here precisely because the search is unbounded and exhaustive).
139
+ """
140
+ key = (seq, goal)
141
+ if key in memo:
142
+ return memo[key]
143
+ memo[key] = None # pre-set: harmless (no rule can revisit an equal sequent)
144
+
145
+ result = None
146
+
147
+ # Ax
148
+ if len(seq) == 1 and seq[0] == goal:
149
+ result = LambekDerivation(LambekSequent(seq, goal), "Ax")
150
+
151
+ # Right rules on the goal's main connective.
152
+ if result is None and isinstance(goal, Product):
153
+ for i in range(1, len(seq)): # both parts nonempty
154
+ p1 = _prove(seq[:i], goal.left, memo)
155
+ if p1 is None:
156
+ continue
157
+ p2 = _prove(seq[i:], goal.right, memo)
158
+ if p2 is not None:
159
+ result = LambekDerivation(LambekSequent(seq, goal), "•R", (p1, p2))
160
+ break
161
+ if result is None and isinstance(goal, Under):
162
+ # goal = A\B: prepend A on the LEFT.
163
+ p = _prove((goal.left,) + seq, goal.right, memo)
164
+ if p is not None:
165
+ result = LambekDerivation(LambekSequent(seq, goal), "\\R", (p,))
166
+ if result is None and isinstance(goal, Over):
167
+ # goal = B/A: append A on the RIGHT (fields: left=B, right=A).
168
+ p = _prove(seq + (goal.right,), goal.left, memo)
169
+ if p is not None:
170
+ result = LambekDerivation(LambekSequent(seq, goal), "/R", (p,))
171
+
172
+ # Left rules on each antecedent position.
173
+ if result is None:
174
+ for i, f in enumerate(seq):
175
+ if isinstance(f, Product):
176
+ p = _prove(seq[:i] + (f.left, f.right) + seq[i + 1:], goal, memo)
177
+ if p is not None:
178
+ result = LambekDerivation(LambekSequent(seq, goal), "•L", (p,))
179
+ break
180
+ elif isinstance(f, Under):
181
+ # f = A\B at position i: a nonempty Γ ending just before i derives A.
182
+ for j in range(i - 1, -1, -1):
183
+ p_arg = _prove(seq[j:i], f.left, memo)
184
+ if p_arg is None:
185
+ continue
186
+ p_main = _prove(seq[:j] + (f.right,) + seq[i + 1:], goal, memo)
187
+ if p_main is not None:
188
+ result = LambekDerivation(LambekSequent(seq, goal), "\\L",
189
+ (p_arg, p_main))
190
+ break
191
+ if result is not None:
192
+ break
193
+ elif isinstance(f, Over):
194
+ # f = B/A at position i: a nonempty Γ starting just after i derives A.
195
+ for j in range(i + 2, len(seq) + 1):
196
+ p_arg = _prove(seq[i + 1:j], f.right, memo)
197
+ if p_arg is None:
198
+ continue
199
+ p_main = _prove(seq[:i] + (f.left,) + seq[j:], goal, memo)
200
+ if p_main is not None:
201
+ result = LambekDerivation(LambekSequent(seq, goal), "/L",
202
+ (p_arg, p_main))
203
+ break
204
+ if result is not None:
205
+ break
206
+
207
+ memo[key] = result
208
+ return result
209
+
210
+
211
+ def lambek_prove(sequence: Iterable[Node], goal: Node) -> Optional[LambekDerivation]:
212
+ """Decide ``sequence ⊢ goal`` in the Lambek calculus L; return a derivation or None.
213
+
214
+ This is a genuine **decision procedure** (see the module docstring): a
215
+ ``None`` *proves* the sequent underivable in L. The returned derivation is
216
+ re-validated by :func:`check_lambek_proof` before it is handed back, so a
217
+ search bug can only lose proofs, never invent them.
218
+
219
+ The sequent is read through the connectives ``• \\ /`` and atoms: an atom over terms is one
220
+ category, and every other node (a quantifier, a counting or cardinality node, a sorted
221
+ constant, an equality atom, a connective of another logic) is refused by name (see the
222
+ module docstring).
223
+
224
+ Raises:
225
+ ValueError: ``sequence`` is empty — L requires nonempty antecedents (Lambek's
226
+ restriction).
227
+ NotImplementedError: a truth constant ``$true`` / ``$false``, or a node L has no rule
228
+ for (a quantifier, a counting or cardinality node, a sorted constant, an
229
+ equality atom, a connective of another logic) occurs in the sequent; the message
230
+ names the node and what to use instead.
231
+ """
232
+ seq = tuple(sequence)
233
+ if not seq:
234
+ raise ValueError(
235
+ "the Lambek calculus requires a nonempty antecedent sequence "
236
+ "(Lambek's restriction)")
237
+ refuse_truth_constants([*seq, goal], "lambek_prove", _NO_TRUTH_CONSTANT_WHY)
238
+ refuse_unreadable_input([*seq, goal], "lambek_prove", LAMBEK)
239
+ derivation = _prove(seq, goal, {})
240
+ if derivation is not None and not check_lambek_proof(derivation):
241
+ raise RuntimeError(
242
+ "internal error: the Lambek search assembled a derivation its own "
243
+ "checker rejects — please report this")
244
+ return derivation
245
+
246
+
247
+ def lambek_derivable(sequence: Iterable[Node], goal: Node) -> bool:
248
+ """Return True iff ``sequence ⊢ goal`` is derivable in L (a decision procedure).
249
+
250
+ Refuses an empty sequence and every node L has no rule for, as :func:`lambek_prove` does.
251
+ """
252
+ return lambek_prove(sequence, goal) is not None
253
+
254
+
255
+ # ---------------------------------------------------------------------------
256
+ # The checker
257
+ # ---------------------------------------------------------------------------
258
+ #
259
+ # Each rule checker has signature (conclusion: LambekSequent, premises:
260
+ # List[LambekSequent]) and returns None when the step is licensed, or an error
261
+ # string. Antecedents are compared as SEQUENCES — order is the whole point.
262
+
263
+ def _r_ax(concl, prems):
264
+ """Ax: ``A ⊢ A`` — a single antecedent formula equal to the succedent."""
265
+ if prems:
266
+ return "Ax takes no premises"
267
+ if len(concl.antecedent) != 1 or concl.antecedent[0] != concl.succedent:
268
+ return "Ax: the antecedent must be exactly the succedent formula"
269
+ return None
270
+
271
+
272
+ def _r_prod_l(concl, prems):
273
+ """•L: from ``Γ, A, B, Δ ⊢ C`` infer ``Γ, A•B, Δ ⊢ C``."""
274
+ if len(prems) != 1:
275
+ return "•L has one premise"
276
+ ant, p = concl.antecedent, prems[0]
277
+ for i, f in enumerate(ant):
278
+ if isinstance(f, Product):
279
+ want = ant[:i] + (f.left, f.right) + ant[i + 1:]
280
+ if p.succedent == concl.succedent and p.antecedent == want:
281
+ return None
282
+ return "•L: needs A•B in the antecedent; the premise unfolds it in place"
283
+
284
+
285
+ def _r_prod_r(concl, prems):
286
+ """•R: from ``Γ ⊢ A`` and ``Δ ⊢ B`` infer ``Γ, Δ ⊢ A•B`` (in order, both nonempty)."""
287
+ if len(prems) != 2:
288
+ return "•R has two premises"
289
+ g = concl.succedent
290
+ if not isinstance(g, Product):
291
+ return "•R must conclude a product A•B"
292
+ p1, p2 = prems
293
+ if not p1.antecedent or not p2.antecedent:
294
+ return "•R: both parts of the split must be nonempty"
295
+ if (p1.succedent == g.left and p2.succedent == g.right
296
+ and p1.antecedent + p2.antecedent == concl.antecedent):
297
+ return None
298
+ return "•R: premises Γ⊢A then Δ⊢B with Γ,Δ the conclusion's antecedent in order"
299
+
300
+
301
+ def _r_under_l(concl, prems):
302
+ """\\L: from ``Γ ⊢ A`` and ``Δ1, B, Δ2 ⊢ C`` infer ``Δ1, Γ, A\\B, Δ2 ⊢ C``."""
303
+ if len(prems) != 2:
304
+ return "\\L has two premises"
305
+ p_arg, p_main = prems
306
+ if not p_arg.antecedent:
307
+ return "\\L: the argument premise's antecedent Γ must be nonempty"
308
+ if p_main.succedent != concl.succedent:
309
+ return "\\L: the main premise must share the conclusion's succedent"
310
+ ant, g = concl.antecedent, p_arg.antecedent
311
+ for i, f in enumerate(ant):
312
+ if not isinstance(f, Under) or f.left != p_arg.succedent:
313
+ continue
314
+ # Γ must sit immediately LEFT of the A\B occurrence.
315
+ if i - len(g) < 0 or ant[i - len(g):i] != g:
316
+ continue
317
+ d1, d2 = ant[:i - len(g)], ant[i + 1:]
318
+ if p_main.antecedent == d1 + (f.right,) + d2:
319
+ return None
320
+ return "\\L: needs Δ1, Γ, A\\B, Δ2 with Γ ⊢ A and Δ1, B, Δ2 ⊢ C"
321
+
322
+
323
+ def _r_under_r(concl, prems):
324
+ """\\R: from ``A, Γ ⊢ B`` infer ``Γ ⊢ A\\B`` (Γ nonempty)."""
325
+ if len(prems) != 1:
326
+ return "\\R has one premise"
327
+ g = concl.succedent
328
+ if not isinstance(g, Under):
329
+ return "\\R must conclude A\\B"
330
+ if not concl.antecedent:
331
+ return "\\R: the conclusion's antecedent Γ must be nonempty"
332
+ p = prems[0]
333
+ if p.succedent == g.right and p.antecedent == (g.left,) + concl.antecedent:
334
+ return None
335
+ return "\\R: the premise must be A, Γ ⊢ B (A prepended on the LEFT)"
336
+
337
+
338
+ def _r_over_l(concl, prems):
339
+ """/L: from ``Γ ⊢ A`` and ``Δ1, B, Δ2 ⊢ C`` infer ``Δ1, B/A, Γ, Δ2 ⊢ C``."""
340
+ if len(prems) != 2:
341
+ return "/L has two premises"
342
+ p_arg, p_main = prems
343
+ if not p_arg.antecedent:
344
+ return "/L: the argument premise's antecedent Γ must be nonempty"
345
+ if p_main.succedent != concl.succedent:
346
+ return "/L: the main premise must share the conclusion's succedent"
347
+ ant, g = concl.antecedent, p_arg.antecedent
348
+ for i, f in enumerate(ant):
349
+ if not isinstance(f, Over) or f.right != p_arg.succedent:
350
+ continue
351
+ # Γ must sit immediately RIGHT of the B/A occurrence.
352
+ if ant[i + 1:i + 1 + len(g)] != g:
353
+ continue
354
+ d1, d2 = ant[:i], ant[i + 1 + len(g):]
355
+ if p_main.antecedent == d1 + (f.left,) + d2:
356
+ return None
357
+ return "/L: needs Δ1, B/A, Γ, Δ2 with Γ ⊢ A and Δ1, B, Δ2 ⊢ C"
358
+
359
+
360
+ def _r_over_r(concl, prems):
361
+ """/R: from ``Γ, A ⊢ B`` infer ``Γ ⊢ B/A`` (Γ nonempty)."""
362
+ if len(prems) != 1:
363
+ return "/R has one premise"
364
+ g = concl.succedent
365
+ if not isinstance(g, Over):
366
+ return "/R must conclude B/A"
367
+ if not concl.antecedent:
368
+ return "/R: the conclusion's antecedent Γ must be nonempty"
369
+ p = prems[0]
370
+ if p.succedent == g.left and p.antecedent == concl.antecedent + (g.right,):
371
+ return None
372
+ return "/R: the premise must be Γ, A ⊢ B (A appended on the RIGHT)"
373
+
374
+
375
+ _LAMBEK_RULES: Dict[str, Callable] = {
376
+ "Ax": _r_ax,
377
+ "•L": _r_prod_l, "•R": _r_prod_r,
378
+ "\\L": _r_under_l, "\\R": _r_under_r,
379
+ "/L": _r_over_l, "/R": _r_over_r,
380
+ }
381
+
382
+
383
+ def _verify(deriv: "LambekDerivation"):
384
+ """Recursive worker: return ``(rule, error)`` or ``(None, None)`` on success."""
385
+ if not isinstance(deriv, LambekDerivation):
386
+ return ("?", f"expected a LambekDerivation, got {type(deriv).__name__}")
387
+ if not deriv.conclusion.antecedent:
388
+ return (deriv.rule, "Lambek sequents require a nonempty antecedent "
389
+ "(Lambek's restriction)")
390
+ for child in deriv.premises:
391
+ rule, err = _verify(child)
392
+ if err is not None:
393
+ return rule, err
394
+ fn = _LAMBEK_RULES.get(deriv.rule)
395
+ if fn is None:
396
+ return deriv.rule, f"unknown Lambek rule {deriv.rule!r}"
397
+ err = fn(deriv.conclusion, [c.conclusion for c in deriv.premises])
398
+ if err is not None:
399
+ return deriv.rule, err if err.startswith(deriv.rule) else f"{deriv.rule}: {err}"
400
+ return None, None
401
+
402
+
403
+ def _formulas_of(derivation: "LambekDerivation") -> List[Node]:
404
+ """Every formula of every sequent of a derivation tree."""
405
+ formulas: List[Node] = []
406
+ pending = [derivation]
407
+ while pending:
408
+ node = pending.pop()
409
+ if isinstance(node, LambekDerivation) and isinstance(node.conclusion, LambekSequent):
410
+ formulas.extend(node.conclusion.antecedent)
411
+ formulas.append(node.conclusion.succedent)
412
+ pending.extend(node.premises)
413
+ return formulas
414
+
415
+
416
+ def verify_lambek_proof(derivation: "LambekDerivation") -> SequentResult:
417
+ """Check a Lambek derivation and return a
418
+ :class:`~unicode_logic_kit.atp.sequent.SequentResult`.
419
+
420
+ Recursively re-validates that every node's conclusion follows from its
421
+ premises' conclusions by the node's rule — antecedents compared as **ordered
422
+ sequences** and required nonempty throughout — returning the end-sequent and,
423
+ on failure, the first offending rule and reason. A derivation whose sequents hold a
424
+ node L has no rule for (a quantifier, an equality atom, a connective of another logic,
425
+ ...) is not a derivation of L: it does not check, and the reason names the node
426
+ (``error_rule`` is ``"formula"``).
427
+ """
428
+ unreadable = unreadable_reason(_formulas_of(derivation), "verify_lambek_proof", LAMBEK)
429
+ err_rule, err = ("formula", unreadable) if unreadable is not None else _verify(derivation)
430
+ end = derivation.conclusion if isinstance(derivation, LambekDerivation) else None
431
+ return SequentResult(err is None, end, err_rule, err)
432
+
433
+
434
+ def check_lambek_proof(derivation: "LambekDerivation") -> bool:
435
+ """Return True iff ``derivation`` is a valid cut-free L derivation (sound)."""
436
+ return verify_lambek_proof(derivation).ok