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,705 @@
1
+ """Intuitionistic sequent calculus **LJ** — a derivation checker.
2
+
3
+ Gentzen's **LJ** is the sequent calculus for intuitionistic logic. Structurally it is
4
+ the classical **LK** of :mod:`unicode_logic_kit.atp.sequent` with one decisive
5
+ restriction: a sequent's **succedent holds at most one formula**. That single change
6
+ blocks the classical theorems that fail intuitionistically — the ``→L`` rule's left
7
+ premise is ``Γ ⊢ A`` (the succedent is *replaced* by ``A``, not kept alongside ``Δ``),
8
+ so excluded middle, double-negation elimination, and Peirce's law have no LJ
9
+ derivation, exactly as the Kripke semantics in
10
+ :mod:`unicode_logic_kit.semantics.intuitionistic` confirms.
11
+
12
+ It reuses the LK :class:`~unicode_logic_kit.atp.sequent.Sequent` /
13
+ :class:`~unicode_logic_kit.atp.sequent.Derivation` data model and the ``sequent`` /
14
+ ``derive`` / ``axiom`` helpers; only the rule set and the checker are new.
15
+ ``render_sequent_proof`` renders an LJ derivation unchanged.
16
+
17
+ Rules: ``Ax``, the structural ``WL`` / ``WR`` / ``CL`` / ``Cut``, the connective rules
18
+ ``¬L`` / ``¬R``, ``∧L`` / ``∧R``, ``∨L`` / ``∨R1`` / ``∨R2`` (disjunction-right is split,
19
+ unlike LK), ``→L`` / ``→R``, ``↔L`` / ``↔R``, and the quantifier rules ``∀L`` / ``∀R``,
20
+ ``∃L`` / ``∃R`` (with the eigenvariable condition on ``∀R`` / ``∃L``).
21
+
22
+ FALSUM. The nullary atoms ``$true`` and ``$false`` (``⊤`` and ``⊥`` in the unicode
23
+ syntax), and the nullary atoms NAMED ``⊤`` and ``⊥`` (the reserved falsum atom of the
24
+ Fitch checker among them), are genuine constants, not propositional letters:
25
+ ``Γ ⊢ ⊤`` and ``Γ, ⊥ ⊢ C`` are axioms of the checker (``⊤`` on the left and ``⊥`` on
26
+ the right license nothing), and the decision procedure reads ``⊥`` as its own
27
+ ex-falso sentinel and ``⊤`` as ``⊥→⊥``. So ``⊥ ⊢ p``, ``⊢ ⊤`` and ``⊢ ¬⊥`` are
28
+ derivable, ``⊢ ⊥`` and ``⊤ ⊢ p`` are not — the same verdicts the Kripke semantics
29
+ (:func:`unicode_logic_kit.semantics.intuitionistic.int_valid`, the ground truth both are
30
+ checked against) gives them (``⊤`` forced at every world, ``⊥`` at none). The
31
+ checker's axiom is these two constants and nothing else: any other atom has no ex
32
+ falso, and ``P ∧ ¬P`` still has to be refuted by the connective rules.
33
+
34
+ ``int_prove`` / ``int_decide`` (below) add a **decision procedure** — Dyckhoff's
35
+ contraction-free **G4ip** calculus — alongside this derivation *checker*; see their
36
+ docstrings.
37
+
38
+ Public API: :func:`check_lj_proof`, :func:`verify_lj_proof`, :func:`int_prove`,
39
+ :func:`int_decide`.
40
+ """
41
+
42
+ from typing import Callable, Dict, FrozenSet, List, Optional, Tuple
43
+
44
+ from ..fol.nodes import (
45
+ Node, Atom, Not, And, Or, Xor, Implies, Iff, Variable,
46
+ Quantifier, SortedQuantifier, SortedConstant, Constant,
47
+ )
48
+ from ..fol._atom_keys import AtomKeys, atom_key
49
+ from ..fol._so_nodes import SecondOrderQuantifier
50
+ from ..fol._truth_constants import (
51
+ truth_value, is_true_constant, is_false_constant,
52
+ )
53
+ from ..semantics._modal_reject import reject_equality_in
54
+ from .sequent import (
55
+ Sequent, Derivation, SequentResult, sequent, derive, axiom,
56
+ _ms_eq, _seq_eq, _candidates, _two_match, _canon_derivation, _eigenvar_not_free,
57
+ )
58
+ from .fitch import _subst_var, _q_kind, _is_term
59
+
60
+
61
+ # ---------------------------------------------------------------------------
62
+ # Rule checkers (single-conclusion: every succedent has length ≤ 1)
63
+ # ---------------------------------------------------------------------------
64
+
65
+ def _r_axiom(concl, prems, extra):
66
+ """Ax: ``Γ, A ⊢ A`` — the one succedent formula occurs in the antecedent.
67
+
68
+ Two more axioms for the truth constants, which are not letters: ``Γ ⊢ $true``
69
+ and ``Γ, $false ⊢ C`` (``C`` any single formula, or no succedent at all).
70
+ ``$true`` on the LEFT and ``$false`` on the RIGHT license nothing, so an axiom
71
+ that has only those is rejected.
72
+ """
73
+ if prems:
74
+ return "Ax takes no premises"
75
+ if len(concl.succedent) > 1:
76
+ return "an LJ axiom has at most one succedent formula"
77
+ if any(is_false_constant(f) for f in concl.antecedent):
78
+ return None # Γ, $false ⊢ C
79
+ if len(concl.succedent) != 1:
80
+ return "an LJ axiom has exactly one succedent formula"
81
+ if is_true_constant(concl.succedent[0]):
82
+ return None # Γ ⊢ $true
83
+ if concl.succedent[0] not in concl.antecedent:
84
+ return "Ax: the succedent formula must occur in the antecedent"
85
+ return None
86
+
87
+
88
+ def _r_weaken_l(concl, prems, extra):
89
+ """WL: from ``Γ ⊢ Δ`` infer ``Γ, A ⊢ Δ``."""
90
+ if len(prems) != 1:
91
+ return "WL has one premise"
92
+ for _, rest in _candidates(concl.antecedent, lambda f: True):
93
+ if _seq_eq(prems[0], rest, concl.succedent):
94
+ return None
95
+ return "WL: the premise must be the conclusion minus one antecedent formula"
96
+
97
+
98
+ def _r_weaken_r(concl, prems, extra):
99
+ """WR: from ``Γ ⊢`` (empty succedent) infer ``Γ ⊢ A``."""
100
+ if len(prems) != 1:
101
+ return "WR has one premise"
102
+ if len(concl.succedent) != 1:
103
+ return "WR concludes a single succedent formula"
104
+ if _seq_eq(prems[0], concl.antecedent, ()):
105
+ return None
106
+ return "WR: the premise must have an empty succedent (Γ ⊢)"
107
+
108
+
109
+ def _r_contract_l(concl, prems, extra):
110
+ """CL: from ``Γ, A, A ⊢ Δ`` infer ``Γ, A ⊢ Δ``."""
111
+ if len(prems) != 1:
112
+ return "CL has one premise"
113
+ for principal, _ in _candidates(concl.antecedent, lambda f: True):
114
+ if _seq_eq(prems[0], concl.antecedent + (principal,), concl.succedent):
115
+ return None
116
+ return "CL: the premise must duplicate one antecedent formula"
117
+
118
+
119
+ def _r_cut(concl, prems, extra):
120
+ """Cut: from ``Γ ⊢ A`` and ``Γ, A ⊢ Δ`` infer ``Γ ⊢ Δ``."""
121
+ if len(prems) != 2:
122
+ return "Cut has two premises"
123
+ g, d = concl.antecedent, concl.succedent
124
+ for left, right in ((prems[0], prems[1]), (prems[1], prems[0])):
125
+ if len(left.succedent) != 1 or not _ms_eq(left.antecedent, g):
126
+ continue
127
+ a = left.succedent[0]
128
+ if _seq_eq(right, g + (a,), d):
129
+ return None
130
+ return "Cut: premises must be Γ⊢A and Γ,A⊢Δ sharing the context"
131
+
132
+
133
+ def _r_not_l(concl, prems, extra):
134
+ """¬L: from ``Γ ⊢ A`` infer ``Γ, ¬A ⊢`` (empty succedent)."""
135
+ if len(prems) != 1:
136
+ return "¬L has one premise"
137
+ if len(concl.succedent) != 0:
138
+ return "¬L concludes an empty succedent"
139
+ for principal, rest in _candidates(concl.antecedent, lambda f: isinstance(f, Not)):
140
+ if _seq_eq(prems[0], rest, (principal.formula,)):
141
+ return None
142
+ return "¬L: needs ¬A on the left; premise Γ ⊢ A"
143
+
144
+
145
+ def _r_not_r(concl, prems, extra):
146
+ """¬R: from ``Γ, A ⊢`` (empty succedent) infer ``Γ ⊢ ¬A``."""
147
+ if len(prems) != 1:
148
+ return "¬R has one premise"
149
+ if len(concl.succedent) != 1 or not isinstance(concl.succedent[0], Not):
150
+ return "¬R must conclude a single negation"
151
+ a = concl.succedent[0].formula
152
+ if _seq_eq(prems[0], concl.antecedent + (a,), ()):
153
+ return None
154
+ return "¬R: the premise must be Γ, A ⊢ (empty succedent)"
155
+
156
+
157
+ def _r_and_l(concl, prems, extra):
158
+ """∧L: from ``Γ, A, B ⊢ Δ`` infer ``Γ, A∧B ⊢ Δ``."""
159
+ if len(prems) != 1:
160
+ return "∧L has one premise"
161
+ for principal, rest in _candidates(concl.antecedent, lambda f: isinstance(f, And)):
162
+ if _seq_eq(prems[0], rest + (principal.left, principal.right), concl.succedent):
163
+ return None
164
+ return "∧L: needs A∧B on the left; premise Γ, A, B ⊢ Δ"
165
+
166
+
167
+ def _r_and_r(concl, prems, extra):
168
+ """∧R: from ``Γ ⊢ A`` and ``Γ ⊢ B`` infer ``Γ ⊢ A∧B``."""
169
+ if len(prems) != 2:
170
+ return "∧R has two premises"
171
+ if len(concl.succedent) != 1 or not isinstance(concl.succedent[0], And):
172
+ return "∧R must conclude a single conjunction"
173
+ p = concl.succedent[0]
174
+ if _two_match(prems, (concl.antecedent, (p.left,)), (concl.antecedent, (p.right,))):
175
+ return None
176
+ return "∧R: premises Γ⊢A and Γ⊢B"
177
+
178
+
179
+ def _r_or_l(concl, prems, extra):
180
+ """∨L: from ``Γ, A ⊢ Δ`` and ``Γ, B ⊢ Δ`` infer ``Γ, A∨B ⊢ Δ``."""
181
+ if len(prems) != 2:
182
+ return "∨L has two premises"
183
+ for principal, rest in _candidates(concl.antecedent, lambda f: isinstance(f, Or)):
184
+ w1 = (rest + (principal.left,), concl.succedent)
185
+ w2 = (rest + (principal.right,), concl.succedent)
186
+ if _two_match(prems, w1, w2):
187
+ return None
188
+ return "∨L: needs A∨B on the left; premises Γ,A⊢Δ and Γ,B⊢Δ"
189
+
190
+
191
+ def _r_or_r1(concl, prems, extra):
192
+ """∨R1: from ``Γ ⊢ A`` infer ``Γ ⊢ A∨B``."""
193
+ if len(prems) != 1:
194
+ return "∨R1 has one premise"
195
+ if len(concl.succedent) != 1 or not isinstance(concl.succedent[0], Or):
196
+ return "∨R1 must conclude a single disjunction"
197
+ if _seq_eq(prems[0], concl.antecedent, (concl.succedent[0].left,)):
198
+ return None
199
+ return "∨R1: the premise must be Γ ⊢ A (the left disjunct)"
200
+
201
+
202
+ def _r_or_r2(concl, prems, extra):
203
+ """∨R2: from ``Γ ⊢ B`` infer ``Γ ⊢ A∨B``."""
204
+ if len(prems) != 1:
205
+ return "∨R2 has one premise"
206
+ if len(concl.succedent) != 1 or not isinstance(concl.succedent[0], Or):
207
+ return "∨R2 must conclude a single disjunction"
208
+ if _seq_eq(prems[0], concl.antecedent, (concl.succedent[0].right,)):
209
+ return None
210
+ return "∨R2: the premise must be Γ ⊢ B (the right disjunct)"
211
+
212
+
213
+ def _r_imp_l(concl, prems, extra):
214
+ """→L: from ``Γ ⊢ A`` and ``Γ, B ⊢ Δ`` infer ``Γ, A→B ⊢ Δ`` (the LJ restriction)."""
215
+ if len(prems) != 2:
216
+ return "→L has two premises"
217
+ for principal, rest in _candidates(concl.antecedent, lambda f: isinstance(f, Implies)):
218
+ w1 = (rest, (principal.left,)) # Γ ⊢ A — succedent is just A
219
+ w2 = (rest + (principal.right,), concl.succedent)
220
+ if _two_match(prems, w1, w2):
221
+ return None
222
+ return "→L: needs A→B on the left; premises Γ⊢A and Γ,B⊢Δ"
223
+
224
+
225
+ def _r_imp_r(concl, prems, extra):
226
+ """→R: from ``Γ, A ⊢ B`` infer ``Γ ⊢ A→B``."""
227
+ if len(prems) != 1:
228
+ return "→R has one premise"
229
+ if len(concl.succedent) != 1 or not isinstance(concl.succedent[0], Implies):
230
+ return "→R must conclude a single implication"
231
+ p = concl.succedent[0]
232
+ if _seq_eq(prems[0], concl.antecedent + (p.left,), (p.right,)):
233
+ return None
234
+ return "→R: the premise must be Γ, A ⊢ B"
235
+
236
+
237
+ def _r_iff_l(concl, prems, extra):
238
+ """↔L: from ``Γ, A→B, B→A ⊢ Δ`` infer ``Γ, A↔B ⊢ Δ``."""
239
+ if len(prems) != 1:
240
+ return "↔L has one premise"
241
+ for principal, rest in _candidates(concl.antecedent, lambda f: isinstance(f, Iff)):
242
+ a, b = principal.left, principal.right
243
+ if _seq_eq(prems[0], rest + (Implies(a, b), Implies(b, a)), concl.succedent):
244
+ return None
245
+ return "↔L: needs A↔B on the left; premise Γ, A→B, B→A ⊢ Δ"
246
+
247
+
248
+ def _r_iff_r(concl, prems, extra):
249
+ """↔R: from ``Γ ⊢ A→B`` and ``Γ ⊢ B→A`` infer ``Γ ⊢ A↔B``."""
250
+ if len(prems) != 2:
251
+ return "↔R has two premises"
252
+ if len(concl.succedent) != 1 or not isinstance(concl.succedent[0], Iff):
253
+ return "↔R must conclude a single biconditional"
254
+ p = concl.succedent[0]
255
+ a, b = p.left, p.right
256
+ if _two_match(prems, (concl.antecedent, (Implies(a, b),)),
257
+ (concl.antecedent, (Implies(b, a),))):
258
+ return None
259
+ return "↔R: premises Γ⊢A→B and Γ⊢B→A"
260
+
261
+
262
+ def _r_forall_l(concl, prems, extra):
263
+ """∀L: from ``Γ, A[x:=t] ⊢ Δ`` infer ``Γ, ∀x A ⊢ Δ`` (t any term, in extra)."""
264
+ if len(prems) != 1:
265
+ return "∀L has one premise"
266
+ if len(extra) != 1 or not _is_term(extra[0]):
267
+ return "∀L needs the instantiation term in extra"
268
+ t = extra[0]
269
+ for principal, rest in _candidates(concl.antecedent, lambda f: _q_kind(f) == "∀"):
270
+ inst = _subst_var(principal.formula, principal.variable, t)
271
+ if _seq_eq(prems[0], rest + (inst,), concl.succedent):
272
+ return None
273
+ return "needs ∀x A on the left and premise Γ, A[x:=t] ⊢ Δ"
274
+
275
+
276
+ def _r_exists_r(concl, prems, extra):
277
+ """∃R: from ``Γ ⊢ A[x:=t]`` infer ``Γ ⊢ ∃x A`` (t any term, in extra)."""
278
+ if len(prems) != 1:
279
+ return "∃R has one premise"
280
+ if len(concl.succedent) != 1 or _q_kind(concl.succedent[0]) != "∃":
281
+ return "∃R must conclude a single existential"
282
+ if len(extra) != 1 or not _is_term(extra[0]):
283
+ return "∃R needs the witness term in extra"
284
+ p = concl.succedent[0]
285
+ inst = _subst_var(p.formula, p.variable, extra[0])
286
+ if _seq_eq(prems[0], concl.antecedent, (inst,)):
287
+ return None
288
+ return "∃R: the premise must be Γ ⊢ A[x:=t]"
289
+
290
+
291
+ def _r_forall_r(concl, prems, extra):
292
+ """∀R: from ``Γ ⊢ A[x:=a]`` infer ``Γ ⊢ ∀x A`` (eigenvariable a, in extra)."""
293
+ if len(prems) != 1:
294
+ return "∀R has one premise"
295
+ if len(concl.succedent) != 1 or _q_kind(concl.succedent[0]) != "∀":
296
+ return "∀R must conclude a single universal"
297
+ if len(extra) != 1 or not isinstance(extra[0], Variable):
298
+ return "∀R needs the eigenvariable in extra"
299
+ a = extra[0]
300
+ p = concl.succedent[0]
301
+ inst = _subst_var(p.formula, p.variable, a)
302
+ if _seq_eq(prems[0], concl.antecedent, (inst,)):
303
+ err = _eigenvar_not_free(a, concl)
304
+ return err if err else None
305
+ return "∀R: the premise must be Γ ⊢ A[x:=a]"
306
+
307
+
308
+ def _r_exists_l(concl, prems, extra):
309
+ """∃L: from ``Γ, A[x:=a] ⊢ Δ`` infer ``Γ, ∃x A ⊢ Δ`` (eigenvariable a, in extra)."""
310
+ if len(prems) != 1:
311
+ return "∃L has one premise"
312
+ if len(extra) != 1 or not isinstance(extra[0], Variable):
313
+ return "∃L needs the eigenvariable in extra"
314
+ a = extra[0]
315
+ for principal, rest in _candidates(concl.antecedent, lambda f: _q_kind(f) == "∃"):
316
+ inst = _subst_var(principal.formula, principal.variable, a)
317
+ if _seq_eq(prems[0], rest + (inst,), concl.succedent):
318
+ err = _eigenvar_not_free(a, concl)
319
+ return err if err else None
320
+ return "needs ∃x A on the left and premise Γ, A[x:=a] ⊢ Δ"
321
+
322
+
323
+ #: One checker per rule name: ``(conclusion, premise conclusions, extra)`` -> an error
324
+ #: message, or ``None`` when the step is a correct instance of the rule.
325
+ _LJ_RULES: Dict[str, Callable[[Sequent, List[Sequent], Tuple], Optional[str]]] = {
326
+ "Ax": _r_axiom,
327
+ "WL": _r_weaken_l, "WR": _r_weaken_r, "CL": _r_contract_l, "Cut": _r_cut,
328
+ "¬L": _r_not_l, "¬R": _r_not_r,
329
+ "∧L": _r_and_l, "∧R": _r_and_r,
330
+ "∨L": _r_or_l, "∨R1": _r_or_r1, "∨R2": _r_or_r2,
331
+ "→L": _r_imp_l, "→R": _r_imp_r,
332
+ "↔L": _r_iff_l, "↔R": _r_iff_r,
333
+ "∀L": _r_forall_l, "∀R": _r_forall_r,
334
+ "∃L": _r_exists_l, "∃R": _r_exists_r,
335
+ }
336
+
337
+
338
+ # ---------------------------------------------------------------------------
339
+ # The checker
340
+ # ---------------------------------------------------------------------------
341
+
342
+ def _multi_succedent(deriv: "Derivation") -> Optional["Sequent"]:
343
+ """Return the first sequent with more than one succedent formula, or None."""
344
+ if len(deriv.conclusion.succedent) > 1:
345
+ return deriv.conclusion
346
+ for child in deriv.premises:
347
+ bad = _multi_succedent(child)
348
+ if bad is not None:
349
+ return bad
350
+ return None
351
+
352
+
353
+ def _verify(deriv: "Derivation"):
354
+ """Recursive worker: return ``(rule, error)`` or ``(None, None)`` on success."""
355
+ if not isinstance(deriv, Derivation):
356
+ return ("?", f"expected a Derivation, got {type(deriv).__name__}")
357
+ for child in deriv.premises:
358
+ rule, err = _verify(child)
359
+ if err is not None:
360
+ return rule, err
361
+ fn = _LJ_RULES.get(deriv.rule)
362
+ if fn is None:
363
+ return deriv.rule, f"unknown LJ rule {deriv.rule!r}"
364
+ err = fn(deriv.conclusion, [c.conclusion for c in deriv.premises], deriv.extra)
365
+ if err is not None:
366
+ return deriv.rule, err if err.startswith(deriv.rule) else f"{deriv.rule}: {err}"
367
+ return None, None
368
+
369
+
370
+ def verify_lj_proof(derivation: "Derivation") -> SequentResult:
371
+ """Check an intuitionistic **LJ** derivation and return a :class:`SequentResult`.
372
+
373
+ Enforces the single-conclusion restriction (every succedent has at most one
374
+ formula) and then the LJ rule of each node, returning the end-sequent and, on
375
+ failure, the first offending rule and reason.
376
+
377
+ Malformed input (``None``, a bare :class:`~unicode_logic_kit.fol.nodes.Node`, a bare
378
+ :class:`Sequent` not wrapped in a :class:`Derivation`, …) is reported the same clean
379
+ way as :func:`~unicode_logic_kit.atp.sequent.verify_sequent_proof` does: ``ok=False``
380
+ with ``error_rule="?"`` and an ``error`` saying what type was expected — never a raw
381
+ ``AttributeError`` from probing a ``.conclusion`` that is not there.
382
+ """
383
+ if isinstance(derivation, Derivation):
384
+ derivation = _canon_derivation(derivation)
385
+ bad = _multi_succedent(derivation)
386
+ if bad is not None:
387
+ return SequentResult(
388
+ False, derivation.conclusion,
389
+ "LJ", "intuitionistic (LJ) sequents have at most one succedent formula; "
390
+ f"found {len(bad.succedent)} in '{bad}'")
391
+ # Non-Derivation input (None, a bare Node, a bare Sequent, ...) skips the
392
+ # multi-succedent probe above (it needs a real Derivation tree) and falls through
393
+ # to _verify, whose own isinstance check reports the same clean "expected a
394
+ # Derivation" shape that atp.sequent._verify uses -- never an AttributeError.
395
+ err_rule, err = _verify(derivation)
396
+ end = derivation.conclusion if isinstance(derivation, Derivation) else None
397
+ return SequentResult(err is None, end, err_rule, err)
398
+
399
+
400
+ def check_lj_proof(derivation: "Derivation") -> bool:
401
+ """Return True iff ``derivation`` is a valid intuitionistic LJ derivation (sound)."""
402
+ return verify_lj_proof(derivation).ok
403
+
404
+
405
+ # ---------------------------------------------------------------------------
406
+ # int_prove / int_decide: Dyckhoff's G4ip -- a terminating DECISION PROCEDURE
407
+ # ---------------------------------------------------------------------------
408
+ #
409
+ # Everything above this point CHECKS a given derivation. G4ip (Dyckhoff, "Contraction-
410
+ # free sequent calculi for intuitionistic logic", J. Symbolic Logic 57(3), 1992 — also
411
+ # called LJT) is a different, contraction-free presentation of LJ whose left rules are
412
+ # keyed on the SHAPE of the principal formula rather than being freely applicable, which
413
+ # is exactly what makes naive backward proof search in ordinary LJ loop (``A→B`` on the
414
+ # left can always fire again on the same formula). Dyckhoff's four ``→L`` variants each
415
+ # strictly shrink a well-founded complexity measure, so backward search terminates —
416
+ # this is a genuine decision procedure, not a bounded search.
417
+ #
418
+ # Internal formula representation. Proof search works over a small IR, deliberately
419
+ # NOT unicode_logic_kit.fol.nodes.Node, so that the ex-falso sentinel used to encode ``¬A``
420
+ # (see below) can never be confused with a Node the caller wrote:
421
+ # ('atom', key) -- key = the atom's written form (fol._atom_keys.atom_key); opaque to G4ip
422
+ # ('and', L, R) / ('or', L, R) / ('imp', L, R)
423
+ # _BOT -- the internal "always false" ex-falso sentinel (see _desugar)
424
+ # A sequent is (ctx: FrozenSet[IR], goal: IR); ctx is a SET, not a multiset — G4ip is
425
+ # "contraction-free" precisely because none of its rules ever need two copies of a
426
+ # hypothesis for completeness (unlike naive LJ, which needs an explicit CL rule).
427
+
428
+ _BOT: Tuple[str] = ('bot',) # the ex-falso sentinel; never equal to any ('atom', ...)
429
+ _TRUE = ('imp', _BOT, _BOT) # `$true`: provable from nothing (→R then the sentinel), inert on the left
430
+
431
+
432
+ def _desugar(f: Node):
433
+ """Rewrite ``¬`` / ``↔`` / ``⊕`` away into the ``('and'|'or'|'imp'|'atom', ...)`` IR
434
+ plus the internal ``_BOT`` sentinel, matching :meth:`IntKripkeModel.forces`'s clauses
435
+ exactly (:mod:`unicode_logic_kit.semantics.intuitionistic`), so ``int_prove`` decides
436
+ exactly the semantics ``int_valid`` checks.
437
+
438
+ ``¬A`` is encoded the standard way, ``A→⊥`` (Dyckhoff's own presentation), where ``⊥``
439
+ is ``_BOT`` — an internal sentinel object, DISTINCT from every ``('atom', ...)`` — and
440
+ the surface falsity constants ``⊥`` / ``$false`` desugar to that very sentinel (see
441
+ the module docstring's FALSUM paragraph). This is what makes ``¬A`` behave like the
442
+ *primitive* Kripke clause "no reachable world forces A" — which quantifies over
443
+ worlds, not over any particular atom's valuation — rather than like material
444
+ implication into a fellow proposition that could itself be forced somewhere.
445
+ ``_BOT`` gets a genuine ex-falso rule inside the prover (:func:`_prove`), which no
446
+ letter has.
447
+ """
448
+ if isinstance(f, Atom):
449
+ constant = truth_value(f)
450
+ if constant is not None:
451
+ # The truth constants (`$true` / `⊤`, `$false` / `⊥`): falsity IS the ex-falso
452
+ # sentinel (it is forced at no world, which is what _BOT means), and truth is
453
+ # `_BOT→_BOT` (forced at every world); neither is a letter.
454
+ return _TRUE if constant else _BOT
455
+ return ('atom', atom_key(f))
456
+ if isinstance(f, Not):
457
+ # ¬A := A→_BOT ("no future world forces A"); _BOT is the ex-falso sentinel the
458
+ # surface constant "⊥" desugars to (see the docstring above).
459
+ return ('imp', _desugar(f.formula), _BOT)
460
+ if isinstance(f, And):
461
+ return ('and', _desugar(f.left), _desugar(f.right))
462
+ if isinstance(f, Or):
463
+ return ('or', _desugar(f.left), _desugar(f.right))
464
+ if isinstance(f, Implies):
465
+ return ('imp', _desugar(f.left), _desugar(f.right))
466
+ if isinstance(f, Iff):
467
+ # A↔B ≡ (A→B)∧(B→A) -- matches IntKripkeModel.forces' ↔ clause.
468
+ left, right = _desugar(f.left), _desugar(f.right)
469
+ return ('and', ('imp', left, right), ('imp', right, left))
470
+ if isinstance(f, Xor):
471
+ # A⊕B ≡ (A∨B)∧¬(A∧B) -- matches IntKripkeModel.forces' ⊕ clause exactly.
472
+ left, right = _desugar(f.left), _desugar(f.right)
473
+ return ('and', ('or', left, right), ('imp', ('and', left, right), _BOT))
474
+ raise NotImplementedError(
475
+ f"int_prove: unsupported node {type(f).__name__} "
476
+ "(propositional ∧/∨/→/¬/↔/⊕ and atoms only; see int_valid/int_countermodel "
477
+ "for first-order input)."
478
+ )
479
+
480
+
481
+ #: G4ip reads an atom as an opaque letter, so it can give identity no meaning:
482
+ #: ``int_prove([], a = a)`` was False (measured on 0.28.1). Refused by name, with
483
+ #: the same wording the Kripke search this calculus is cross-checked against uses
484
+ #: — the two decide the same fragment and must refuse the same input.
485
+ _EQUALITY_ROUTE = "Dyckhoff's G4ip calculus"
486
+ _EQUALITY_ATOM_READING = ("an atom is an opaque letter in a sequent, and no term "
487
+ "is interpreted")
488
+ _EQUALITY_INSTEAD = (
489
+ "Decide identity classically with unicode_logic_kit.api.prove, or on the "
490
+ "quantified modal route with unicode_logic_kit.fol.qml.qml_is_valid (rigid "
491
+ "identity over the object domain); intuitionistic logic with equality is not "
492
+ "what this calculus decides."
493
+ )
494
+
495
+
496
+ def _reject_equality_everywhere(formula: Node, caller: str) -> None:
497
+ """Refuse an identity atom anywhere in ``formula``, before the search.
498
+
499
+ Up front, not inside a rule: G4ip closes a branch on an axiom match, so a
500
+ sequent that closes without ever decomposing the identity atom would return a
501
+ verdict that never read it.
502
+ """
503
+ reject_equality_in(formula, caller, _EQUALITY_ROUTE,
504
+ atom_reading=_EQUALITY_ATOM_READING,
505
+ instead=_EQUALITY_INSTEAD)
506
+
507
+
508
+ def _reject_quantified(formula: Node) -> None:
509
+ """Raise if ``formula`` contains a quantifier -- ``int_prove`` decides the
510
+ PROPOSITIONAL fragment only."""
511
+ for node in formula.walk():
512
+ if isinstance(node, (Quantifier, SortedQuantifier, SecondOrderQuantifier)):
513
+ raise NotImplementedError(
514
+ "int_prove: quantified input is out of scope. Dyckhoff's G4ip decides "
515
+ "PROPOSITIONAL intuitionistic logic only. For a first-order formula, use "
516
+ "the bounded Kripke search "
517
+ "unicode_logic_kit.semantics.intuitionistic.int_valid / int_countermodel "
518
+ "(a returned counter-model genuinely refutes validity; a clean search "
519
+ "does NOT prove it, since first-order intuitionistic logic is "
520
+ "undecidable), or, for the propositional fragment specifically, emit a "
521
+ "HOL problem via unicode_logic_kit.hol.intuitionistic (the "
522
+ "Gödel–McKinsey–Tarski box-translation into modal S4)."
523
+ )
524
+
525
+
526
+ def _forget_constant_sorts(node: Node, membership: List[Node]) -> Node:
527
+ """``node`` with every sorted constant ``c:S`` replaced by the plain constant ``c``.
528
+
529
+ The atom ``S(c)`` of each one is appended to ``membership`` (a constant is in
530
+ its sort), which is what makes ``Mortal(c:S)`` and ``Mortal(c)`` one letter of
531
+ the sequent and the annotation a hypothesis instead of a second symbol. This is
532
+ the sorted-constant half of ``Node._relativize`` and nothing more: any other
533
+ many-sorted node is left for :func:`_desugar` to refuse by name.
534
+ """
535
+ if isinstance(node, SortedConstant):
536
+ membership.append(Atom(node.sort, (Constant(node.name),)))
537
+ return Constant(node.name)
538
+ return node.map_children(lambda child: _forget_constant_sorts(child, membership))
539
+
540
+
541
+ _MAX_STEPS = 200000 # the step budget of one search: spent, it ends the search with no answer -- see _prove
542
+
543
+
544
+ def _prove(ctx: FrozenSet, goal, cache: Dict, steps: List[int]) -> bool:
545
+ """Decide the G4ip sequent ``ctx ⊢ goal`` (both in the IR of :func:`_desugar`).
546
+
547
+ ``cache`` memoizes by ``(ctx, goal)`` (both hashable: ``ctx`` a frozenset, ``goal`` a
548
+ tuple), which keeps the differential test battery fast; it does not change the
549
+ result (this is a pure function of ``ctx``/``goal``). ``steps`` is a one-element
550
+ mutable counter: Dyckhoff's calculus is *provably* terminating on its own (every
551
+ rule strictly decreases a well-founded complexity measure), but the number of steps
552
+ is exponential in the nesting of implications, so a sequent can need more than the
553
+ counter allows (Peirce's law nested in itself five times does). The counter then ends
554
+ the search with a ``RuntimeError`` that says so, instead of running for hours: no
555
+ answer is given, so it bounds what is decided and never what an answer means
556
+ (``int_countermodel``'s ``max_steps`` bounds an incomplete first-order search in the
557
+ same way).
558
+ """
559
+ key = (ctx, goal)
560
+ cached = cache.get(key)
561
+ if cached is not None:
562
+ return cached
563
+ steps[0] += 1
564
+ if steps[0] > _MAX_STEPS:
565
+ raise RuntimeError(
566
+ f"int_prove: internal step budget exceeded ({_MAX_STEPS} steps). Dyckhoff's "
567
+ "G4ip terminates on every sequent, but the number of steps it takes is "
568
+ "exponential in the nesting of implications, and this sequent needs more than "
569
+ "the budget: nothing was decided. (The backend 'intuitionistic' answers "
570
+ "unknown / bound_hit in this case.)"
571
+ )
572
+ result = _prove_uncached(ctx, goal, cache, steps)
573
+ cache[key] = result
574
+ return result
575
+
576
+
577
+ def _prove_uncached(ctx: FrozenSet, goal, cache: Dict, steps: List[int]) -> bool:
578
+ """One step of :func:`_prove`, assuming ``(ctx, goal)`` was not already cached."""
579
+ kind = goal[0]
580
+
581
+ # --- right rules on the goal: invertible, so always safe to apply first. ---
582
+ if kind == 'and':
583
+ # ∧R: Γ⊢A∧B iff Γ⊢A and Γ⊢B.
584
+ return (_prove(ctx, goal[1], cache, steps)
585
+ and _prove(ctx, goal[2], cache, steps))
586
+ if kind == 'imp':
587
+ # →R: Γ⊢A→B iff Γ,A⊢B.
588
+ return _prove(ctx | {goal[1]}, goal[2], cache, steps)
589
+
590
+ # goal is now 'atom', 'bot', or 'or' -- consult ctx.
591
+ if _BOT in ctx:
592
+ return True # ex falso quodlibet -- ONLY for the internal sentinel.
593
+ if kind == 'atom' and goal in ctx:
594
+ return True # Ax: Γ,P⊢P for atomic P.
595
+
596
+ # --- cheap left rules: each is an unconditional equivalence (holds regardless of
597
+ # the rest of ctx), so firing the FIRST one found and recursing is complete; the
598
+ # other hypotheses remain in ctx for later rounds (or the L⊃4 pass below). ---
599
+ for h in ctx:
600
+ if h[0] == 'and':
601
+ # ∧L: Γ,A∧B⊢C iff Γ,A,B⊢C.
602
+ return _prove((ctx - {h}) | {h[1], h[2]}, goal, cache, steps)
603
+ if h[0] == 'or':
604
+ # ∨L: Γ,A∨B⊢C iff Γ,A⊢C and Γ,B⊢C.
605
+ rest = ctx - {h}
606
+ return (_prove(rest | {h[1]}, goal, cache, steps)
607
+ and _prove(rest | {h[2]}, goal, cache, steps))
608
+ if h[0] == 'imp':
609
+ a, b = h[1], h[2]
610
+ if a[0] == 'atom' and a in ctx:
611
+ # atom→L: Γ,P,P→B⊢C iff Γ,P,B⊢C (P atomic, present separately).
612
+ return _prove((ctx - {h}) | {b}, goal, cache, steps)
613
+ if a[0] == 'and':
614
+ # ∧→L (currying): Γ,(A∧B)→C⊢D iff Γ,A→(B→C)⊢D.
615
+ x, y = a[1], a[2]
616
+ return _prove((ctx - {h}) | {('imp', x, ('imp', y, b))}, goal, cache, steps)
617
+ if a[0] == 'or':
618
+ # ∨→L: Γ,(A∨B)→C⊢D iff Γ,A→C,B→C⊢D.
619
+ x, y = a[1], a[2]
620
+ return _prove((ctx - {h}) | {('imp', x, b), ('imp', y, b)}, goal, cache, steps)
621
+ # a[0] == 'imp' (an L⊃4 candidate) or an atom not (yet) present: no cheap
622
+ # move on this h; leave it in ctx and keep scanning.
623
+
624
+ # ctx is now saturated w.r.t. the cheap rules. Two backtracking choice points remain.
625
+ if kind == 'or':
626
+ # ∨R: try each disjunct (not invertible -- the only genuine "which one" choice
627
+ # besides L⊃4). Both sub-calls reuse the same saturated ctx.
628
+ return (_prove(ctx, goal[1], cache, steps)
629
+ or _prove(ctx, goal[2], cache, steps))
630
+
631
+ # →→L (L⊃4), the one genuinely two-premise rule: for a hypothesis (A→B)→C, decide
632
+ # Γ,(A→B)→C⊢D iff (Γ,B→C⊢A→B) and (Γ,C⊢D).
633
+ # This is an EQUIVALENCE over the full (h-including) Γ (Dyckhoff 1992), so trying
634
+ # each L⊃4-shaped hypothesis in turn -- and, for a given one, requiring BOTH
635
+ # premises -- is complete; a failed candidate is simply skipped in favour of the
636
+ # next (the rest of ctx, including any other hypothesis, is retained in "rest" and
637
+ # tried by both premises, so nothing here is lost by moving on).
638
+ for h in ctx:
639
+ if h[0] == 'imp' and h[1][0] == 'imp':
640
+ x, y = h[1][1], h[1][2]
641
+ b = h[2]
642
+ rest = ctx - {h}
643
+ if _prove(rest | {('imp', y, b)}, ('imp', x, y), cache, steps):
644
+ if _prove(rest | {b}, goal, cache, steps):
645
+ return True
646
+ return False
647
+
648
+
649
+ def int_prove(premises: List[Node], conclusion: Node) -> bool:
650
+ """Decide ``premises ⊢ conclusion`` in **propositional intuitionistic logic** —
651
+ a genuine, terminating decision procedure via Dyckhoff's contraction-free **G4ip**
652
+ calculus (unlike :func:`verify_lj_proof`, which only checks a *given* derivation).
653
+
654
+ Connectives: ``∧`` ``∨`` ``→`` ``↔`` ``⊕`` ``¬`` and atoms — ``↔`` is expanded to two
655
+ implications and ``⊕`` to ``(A∨B)∧¬(A∧B)``, exactly the clauses
656
+ :meth:`~unicode_logic_kit.semantics.intuitionistic.IntKripkeModel.forces` uses, so
657
+ ``int_prove`` decides exactly what
658
+ :func:`~unicode_logic_kit.semantics.intuitionistic.int_valid` checks (the differential
659
+ test battery in ``tests/test_lj_search.py`` cross-checks this). The nullary atoms
660
+ ``⊥`` / ``$false`` and ``⊤`` / ``$true`` are the falsity and truth constants here
661
+ too (see the module docstring's FALSUM paragraph): ``int_prove([], Implies(BOT, p))``
662
+ and ``int_prove([], Not(BOT))`` are both True, exactly like ``int_valid``.
663
+
664
+ Quantified input raises ``NotImplementedError`` — see :func:`_reject_quantified` for
665
+ where to go instead (the bounded first-order Kripke search, or the propositional
666
+ GMT/S4 HOL route).
667
+
668
+ A SORTED constant (``socrates:Human``) is read the way
669
+ :func:`~unicode_logic_kit.semantics.intuitionistic.int_valid` reads it: the
670
+ annotation does not make a second symbol, so ``Mortal(socrates:Human)`` and
671
+ ``Mortal(socrates)`` are ONE letter, and the constant being in its sort — the atom
672
+ ``Human(socrates)`` — is a HYPOTHESIS of the sequent, exactly as
673
+ :func:`~unicode_logic_kit.fol._msfl_nodes.sort_membership_axioms` asserts it on every
674
+ other many-sorted route. So ``int_prove([], Mortal(socrates:Human) →
675
+ Mortal(socrates))`` is True and ``int_prove([], Human(socrates:Human))`` is True,
676
+ while ``int_prove([], Mortal(socrates:Human))`` is False, as it is of the plain
677
+ ``Mortal(socrates)``. A sorted QUANTIFIER is refused, like every quantifier.
678
+
679
+ A letter is named by the text its atom prints as, so two different atoms that print
680
+ alike would be one letter and the sequent another problem. Such a pair is refused by name
681
+ (``NotImplementedError``): the numeral ``1`` and a constant named ``1``
682
+ (``Number(1)`` and ``Constant('1')``, which the TPTP reader reads from ``p(1)`` and
683
+ ``p('1')``), and a free variable ``x`` and a constant named ``x`` (a free variable is a
684
+ parameter, not the constant of its name).
685
+ """
686
+ for p in premises:
687
+ _reject_quantified(p)
688
+ _reject_equality_everywhere(p, "int_prove")
689
+ _reject_quantified(conclusion)
690
+ _reject_equality_everywhere(conclusion, "int_prove")
691
+ membership: List[Node] = []
692
+ premises = [_forget_constant_sorts(p, membership) for p in premises]
693
+ conclusion = _forget_constant_sorts(conclusion, membership)
694
+ AtomKeys("int_prove").letters([*premises, *membership, conclusion])
695
+ ctx = frozenset(_desugar(p) for p in list(premises) + membership)
696
+ goal = _desugar(conclusion)
697
+ return _prove(ctx, goal, {}, [0])
698
+
699
+
700
+ def int_decide(formula: Node) -> bool:
701
+ """Return True iff ``formula`` is a propositional intuitionistic tautology.
702
+
703
+ ``int_decide(f)`` is exactly ``int_prove([], f)``; see :func:`int_prove`.
704
+ """
705
+ return int_prove([], formula)