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,738 @@
1
+ """Intuitionistic linear logic (ILL) — cut-free backward proof search + a checker.
2
+
3
+ The single-succedent two-sided sequent calculus for propositional ILL: a sequent is
4
+ ``Γ ⊢ C`` with ``Γ`` a **multiset** of formulas (exchange is implicit; weakening and
5
+ contraction are NOT available on plain formulas — that is the whole point) and ``C``
6
+ a single formula. The connectives are the ``linear`` parser mode's
7
+ (:mod:`unicode_logic_kit.fol._linear_nodes`): ``⊗`` / ``&`` / ``⊕`` / ``⊸`` / ``!`` / ``𝟙``
8
+ plus the additive units ``⊤`` (top) and ``𝟘`` (zero).
9
+
10
+ Rules (Girard's ILL, cut-free)::
11
+
12
+ Ax: A ⊢ A
13
+ 1R: ⊢ 𝟙 1L: Γ ⊢ C ⇒ Γ, 𝟙 ⊢ C
14
+ ⊗L: Γ, A, B ⊢ C ⇒ Γ, A⊗B ⊢ C
15
+ ⊗R: Γ ⊢ A and Δ ⊢ B ⇒ Γ, Δ ⊢ A⊗B (the context SPLITS)
16
+ ⊸L: Γ ⊢ A and Δ, B ⊢ C ⇒ Γ, Δ, A⊸B ⊢ C (the context SPLITS)
17
+ ⊸R: Γ, A ⊢ B ⇒ Γ ⊢ A⊸B
18
+ &L1: Γ, A ⊢ C ⇒ Γ, A&B ⊢ C (&L2 likewise with B)
19
+ &R: Γ ⊢ A and Γ ⊢ B ⇒ Γ ⊢ A&B (the context is SHARED)
20
+ ⊕L: Γ, A ⊢ C and Γ, B ⊢ C ⇒ Γ, A⊕B ⊢ C
21
+ ⊕R1: Γ ⊢ A ⇒ Γ ⊢ A⊕B (⊕R2 likewise with B)
22
+ !W: Γ ⊢ C ⇒ Γ, !A ⊢ C (weakening — only under !)
23
+ !C: Γ, !A, !A ⊢ C ⇒ Γ, !A ⊢ C (contraction — only under !)
24
+ !D: Γ, A ⊢ C ⇒ Γ, !A ⊢ C (dereliction)
25
+ !P: !Γ ⊢ A ⇒ !Γ ⊢ !A (promotion: every antecedent !-prefixed)
26
+ ⊤R: Γ ⊢ ⊤ always (no premise, no ⊤L — the additive
27
+ unit of &; ⊤ carries no information,
28
+ so e.g. ⊤⊸A is NOT in general derivable)
29
+ 0L: Γ, 𝟘 ⊢ C always (no premise, no 0R — the additive
30
+ unit of ⊕; ILL's ex falso)
31
+
32
+ **Decidability status.** For the **!-free fragment** every rule's premises have
33
+ strictly smaller total size (node count) than its conclusion, so the backward search
34
+ below — exhaustive over rule instances, memoised on ``(antecedent multiset, goal)`` —
35
+ is a **complete, terminating decision procedure**: ``ill_prove`` returning ``None``
36
+ *proves* underivability, and the default depth bound (the sequent's total size, which
37
+ no cut-free proof of it can exceed) is never the reason a proof is missed. When ``!``
38
+ occurs, the ``!C`` rule *grows* sequents and the naive search space is infinite; the
39
+ search is then depth- and step-bounded and ``None`` means only "no proof found within
40
+ the bound" — honest, but not a refutation.
41
+
42
+ Deliberately, there is **no classical export**: the collapse ⊗,& → ∧, ⊕ → ∨, ⊸ → →,
43
+ drop ! erases exactly the resource distinctions ILL draws (it is *sound* — every ILL
44
+ theorem collapses to a classical one, which the test-suite checks against Z3 — but
45
+ wildly incomplete in reverse).
46
+
47
+ **What is read.** The calculus reads exactly the node classes it has rules for: ``⊗``
48
+ (``Tensor``), ``&`` (``With``), ``⊕`` (``OPlus``), ``⊸`` (``LinearImplies``), ``!``
49
+ (``OfCourse``), ``𝟙`` (``One``), ``⊤`` (``Top``) and ``𝟘`` (``Zero``), over atoms. The atoms
50
+ are the propositional letters of the ``linear`` grammar, and an atom over terms (``P(alpha)``,
51
+ ``Loves(john, mary)``, a free variable included) is ONE category, identified by its predicate
52
+ and its terms as written; that reading is sound in both directions, because a quantifier-free,
53
+ equality-free sequent has no rule that substitutes one term for another. Every other node is
54
+ refused by name with ``NotImplementedError``: a quantifier (sorted or not), a counting or
55
+ cardinality node, a sorted constant, an equality atom, and a node of another logic (``And``,
56
+ ``Or``, ``Not``, ``Box``, a connective of the Lambek calculus, ...). Read as one more category
57
+ they would answer about another formula (``∀x P(x) ⊢ P(alpha)`` holds in first-order linear
58
+ logic and has no derivation between the two categories; ``And(A, B) ⊢ A`` holds classically and
59
+ has none between ``And(A, B)`` and ``A``). Decide such input with a route of its own logic.
60
+
61
+ Public API: :class:`ILLSequent`, :class:`ILLDerivation`, :func:`ill_prove`,
62
+ :func:`ill_derivable`, :func:`check_ill_proof`, :func:`verify_ill_proof`,
63
+ :func:`render_ill_proof`.
64
+ """
65
+
66
+ from collections import Counter
67
+ from dataclasses import dataclass
68
+ from itertools import product as _iproduct
69
+ from typing import Callable, Dict, Iterable, List, Optional, Tuple
70
+
71
+ from ..fol.nodes import Node, Tensor, With, OPlus, LinearImplies, OfCourse, One
72
+ # Top/Zero ARE re-exported by fol.nodes (and by the package __init__) —
73
+ # fol.nodes.Top and fol._linear_nodes.Top are the same class object, verified
74
+ # live. This import just goes straight to their defining module instead,
75
+ # which is equally valid; it is grouped with render_ill_formula and
76
+ # _ill_sort_key, which _linear_nodes defines and fol.nodes does not
77
+ # re-export. render_ill_formula is _linear_nodes' fully-parenthesised
78
+ # renderer, kept for the MSFLParser round-trip guarantee that buys it (see
79
+ # that module's comment above its definition) — not to route around a
80
+ # rendering gap: to_unicode_str()/to_latex() already render Top/Zero
81
+ # correctly.
82
+ from ..fol._linear_nodes import Top, Zero, render_ill_formula, _ill_sort_key
83
+ from ..fol._truth_constants import refuse_truth_constants
84
+ from ._substructural_input import ILL, refuse_unreadable_input, unreadable_reason
85
+ from .sequent import SequentResult
86
+
87
+ #: Why ILL has no reading of the TPTP constants ``$true`` / ``$false`` (the reason
88
+ #: :func:`ill_prove` and the Isabelle export name when they refuse one).
89
+ _NO_TRUTH_CONSTANT_WHY = (
90
+ "intuitionistic linear logic has two truths, the additive ⊤ and the "
91
+ "multiplicative 1, which are not interderivable, and a falsity 0 of its own, "
92
+ "and TPTP's $true / $false name none of them; write ⊤, 1 or 0 in the linear "
93
+ "grammar for the unit meant")
94
+
95
+
96
+ # ---------------------------------------------------------------------------
97
+ # Sequents and derivations
98
+ # ---------------------------------------------------------------------------
99
+
100
+ @dataclass(frozen=True)
101
+ class ILLSequent:
102
+ """A single-succedent linear sequent ``antecedent ⊢ succedent``.
103
+
104
+ ``antecedent`` is a tuple of formulas read as a **multiset** (order never
105
+ matters, multiplicity always does — there is no weakening or contraction on
106
+ plain formulas); ``succedent`` is one formula. Frozen and hashable.
107
+ """
108
+
109
+ antecedent: Tuple[Node, ...]
110
+ succedent: Node
111
+
112
+ def __post_init__(self):
113
+ """Coerce the antecedent to a tuple so the sequent stays hashable."""
114
+ object.__setattr__(self, "antecedent", tuple(self.antecedent))
115
+
116
+ def __str__(self) -> str:
117
+ """Render as ``A, B ⊢ C`` using each formula's Unicode form.
118
+
119
+ Uses :func:`~unicode_logic_kit.fol._linear_nodes.render_ill_formula`
120
+ rather than ``Node.to_unicode_str()`` directly: the two always agree
121
+ on the rendered symbols (Top/Zero included), but render_ill_formula
122
+ fully parenthesises every compound subformula, which guarantees a
123
+ sequent's text re-parses to a structurally equal AST — a guarantee
124
+ to_unicode_str()'s precedence-based minimal parenthesisation does not
125
+ make in general (see that module's comment for a worked example).
126
+ """
127
+ left = ", ".join(render_ill_formula(f) for f in self.antecedent)
128
+ return f"{left} ⊢ {render_ill_formula(self.succedent)}".strip()
129
+
130
+
131
+ @dataclass(frozen=True)
132
+ class ILLDerivation:
133
+ """A node of an ILL derivation tree.
134
+
135
+ ``conclusion`` is the sequent this node derives; ``rule`` is the rule name
136
+ (``"Ax"``, ``"⊗R"``, ``"!C"``, …); ``premises`` are the sub-derivations whose
137
+ conclusions are this rule's premises (empty for ``Ax`` / ``1R``).
138
+ """
139
+
140
+ conclusion: ILLSequent
141
+ rule: str
142
+ premises: Tuple["ILLDerivation", ...] = ()
143
+
144
+ def __post_init__(self):
145
+ """Coerce ``premises`` to a tuple so the derivation stays hashable."""
146
+ object.__setattr__(self, "premises", tuple(self.premises))
147
+
148
+ def render(self) -> str:
149
+ """Render this derivation as an indented proof tree."""
150
+ return render_ill_proof(self)
151
+
152
+
153
+ def render_ill_proof(derivation: "ILLDerivation", indent: int = 0) -> str:
154
+ """Render a derivation as an indented tree (conclusion first, premises below)."""
155
+ pad = " " * indent
156
+ lines = [f"{pad}{derivation.conclusion} [{derivation.rule}]"]
157
+ for premise in derivation.premises:
158
+ lines.append(render_ill_proof(premise, indent + 1))
159
+ return "\n".join(lines)
160
+
161
+
162
+ # ---------------------------------------------------------------------------
163
+ # Multiset helpers
164
+ # ---------------------------------------------------------------------------
165
+
166
+ def _norm(cnt: Counter) -> Counter:
167
+ """Return ``cnt`` with non-positive entries stripped (Counter == is dict ==)."""
168
+ return Counter({k: v for k, v in cnt.items() if v > 0})
169
+
170
+
171
+ def _cnt_of(formulas: Iterable[Node]) -> Counter:
172
+ """Return the multiset (Counter) of an iterable of formulas."""
173
+ return Counter(formulas)
174
+
175
+
176
+ def _minus(cnt: Counter, f: Node) -> Counter:
177
+ """Return ``cnt`` with one occurrence of ``f`` removed (caller ensures presence)."""
178
+ out = Counter(cnt)
179
+ out[f] -= 1
180
+ return _norm(out)
181
+
182
+
183
+ def _plus(cnt: Counter, *fs: Node) -> Counter:
184
+ """Return ``cnt`` with one occurrence of each of ``fs`` added."""
185
+ out = Counter(cnt)
186
+ for f in fs:
187
+ out[f] += 1
188
+ return out
189
+
190
+
191
+ def _tuple_of(cnt: Counter) -> Tuple[Node, ...]:
192
+ """Expand a multiset to a deterministic tuple (sorted by Unicode rendering)."""
193
+ out: List[Node] = []
194
+ for f in sorted(cnt.keys(), key=_ill_sort_key):
195
+ out.extend([f] * cnt[f])
196
+ return tuple(out)
197
+
198
+
199
+ def _key(cnt: Counter, goal: Node):
200
+ """Return a hashable memo key for the sequent ``cnt ⊢ goal``."""
201
+ return (frozenset(_norm(cnt).items()), goal)
202
+
203
+
204
+ def _splits(cnt: Counter):
205
+ """Yield every split ``(Γ, Δ)`` of a multiset with ``Γ ⊎ Δ = cnt`` (both may be empty)."""
206
+ items = sorted(cnt.items(), key=lambda kv: _ill_sort_key(kv[0]))
207
+ ranges = [range(c + 1) for _, c in items]
208
+ for take in _iproduct(*ranges):
209
+ left = Counter({f: k for (f, _), k in zip(items, take) if k > 0})
210
+ right = Counter({f: c - k for (f, c), k in zip(items, take) if c - k > 0})
211
+ yield left, right
212
+
213
+
214
+ def _size(f: Node) -> int:
215
+ """Return the node count of a formula (atoms and 𝟙 count 1)."""
216
+ return 1 + sum(_size(c) for c in f._child_nodes())
217
+
218
+
219
+ def _has_bang(f: Node) -> bool:
220
+ """Return True iff ``!`` occurs anywhere in the formula."""
221
+ return any(isinstance(n, OfCourse) for n in f.walk())
222
+
223
+
224
+ # ---------------------------------------------------------------------------
225
+ # Backward proof search
226
+ # ---------------------------------------------------------------------------
227
+
228
+ class _IllSearch:
229
+ """Holds the memo tables and the (optional) step budget of one search run."""
230
+
231
+ def __init__(self, budget: Optional[int]):
232
+ self.proven: Dict = {} # key -> ILLDerivation
233
+ self.failed: Dict = {} # key -> largest depth budget it failed with
234
+ self.steps = budget # None = unbounded (the !-free decision procedure)
235
+
236
+ def spend(self) -> bool:
237
+ """Consume one step of the budget; return False when it is exhausted."""
238
+ if self.steps is None:
239
+ return True
240
+ if self.steps <= 0:
241
+ return False
242
+ self.steps -= 1
243
+ return True
244
+
245
+
246
+ def _attempts(cnt: Counter, goal: Node):
247
+ """Yield ``(rule, [premise (multiset, goal) pairs])`` for every rule instance.
248
+
249
+ Backward reading of every ILL rule whose conclusion matches ``cnt ⊢ goal``.
250
+ Ordered cheapest-first (non-branching size reducers before context splits;
251
+ the sequent-growing ``!C`` last), which is a heuristic only — the search
252
+ tries them all.
253
+ """
254
+ distinct = sorted(cnt.keys(), key=_ill_sort_key)
255
+
256
+ # Non-branching left rules that strictly shrink the sequent.
257
+ for f in distinct:
258
+ if isinstance(f, One):
259
+ yield "1L", [(_minus(cnt, f), goal)]
260
+ elif isinstance(f, Tensor):
261
+ yield "⊗L", [(_plus(_minus(cnt, f), f.left, f.right), goal)]
262
+ elif isinstance(f, OfCourse):
263
+ yield "!D", [(_plus(_minus(cnt, f), f.formula), goal)]
264
+
265
+ # Right rules on the goal's main connective.
266
+ if isinstance(goal, LinearImplies):
267
+ yield "⊸R", [(_plus(cnt, goal.left), goal.right)]
268
+ elif isinstance(goal, With):
269
+ yield "&R", [(cnt, goal.left), (cnt, goal.right)]
270
+ elif isinstance(goal, OPlus):
271
+ yield "⊕R1", [(cnt, goal.left)]
272
+ yield "⊕R2", [(cnt, goal.right)]
273
+ elif isinstance(goal, Tensor):
274
+ for left, right in _splits(cnt):
275
+ yield "⊗R", [(left, goal.left), (right, goal.right)]
276
+ elif isinstance(goal, OfCourse):
277
+ if all(isinstance(f, OfCourse) for f in cnt):
278
+ yield "!P", [(cnt, goal.formula)]
279
+
280
+ # Branching / choice left rules.
281
+ for f in distinct:
282
+ if isinstance(f, With):
283
+ rest = _minus(cnt, f)
284
+ yield "&L1", [(_plus(rest, f.left), goal)]
285
+ yield "&L2", [(_plus(rest, f.right), goal)]
286
+ elif isinstance(f, OPlus):
287
+ rest = _minus(cnt, f)
288
+ yield "⊕L", [(_plus(rest, f.left), goal), (_plus(rest, f.right), goal)]
289
+ elif isinstance(f, LinearImplies):
290
+ rest = _minus(cnt, f)
291
+ for left, right in _splits(rest):
292
+ yield "⊸L", [(left, f.left), (_plus(right, f.right), goal)]
293
+
294
+ # The remaining exponential structure: weakening, then sequent-growing contraction.
295
+ for f in distinct:
296
+ if isinstance(f, OfCourse):
297
+ yield "!W", [(_minus(cnt, f), goal)]
298
+ for f in distinct:
299
+ if isinstance(f, OfCourse):
300
+ yield "!C", [(_plus(cnt, f), goal)]
301
+
302
+
303
+ def _prove(cnt: Counter, goal: Node, depth: int, search: _IllSearch) -> Optional[ILLDerivation]:
304
+ """Backward search for a derivation of ``cnt ⊢ goal`` within ``depth`` rule steps."""
305
+ cnt = _norm(cnt)
306
+ key = _key(cnt, goal)
307
+ hit = search.proven.get(key)
308
+ if hit is not None:
309
+ return hit
310
+ if search.failed.get(key, -1) >= depth:
311
+ return None
312
+ if not search.spend():
313
+ return None
314
+
315
+ # Leaves.
316
+ if sum(cnt.values()) == 1 and next(iter(cnt)) == goal:
317
+ d = ILLDerivation(ILLSequent(_tuple_of(cnt), goal), "Ax")
318
+ search.proven[key] = d
319
+ return d
320
+ if not cnt and isinstance(goal, One):
321
+ d = ILLDerivation(ILLSequent((), goal), "1R")
322
+ search.proven[key] = d
323
+ return d
324
+ # ⊤R and 0L are unconditional (no premise, any context/goal), exactly like
325
+ # Ax/1R above — checked here rather than via _attempts so they fire even
326
+ # at depth 0 (they can never be the reason a proof is missed).
327
+ if isinstance(goal, Top):
328
+ d = ILLDerivation(ILLSequent(_tuple_of(cnt), goal), "⊤R")
329
+ search.proven[key] = d
330
+ return d
331
+ if any(isinstance(f, Zero) for f in cnt):
332
+ d = ILLDerivation(ILLSequent(_tuple_of(cnt), goal), "0L")
333
+ search.proven[key] = d
334
+ return d
335
+
336
+ if depth > 0:
337
+ for rule, premises in _attempts(cnt, goal):
338
+ subs: List[ILLDerivation] = []
339
+ for (pc, pg) in premises:
340
+ sub = _prove(pc, pg, depth - 1, search)
341
+ if sub is None:
342
+ break
343
+ subs.append(sub)
344
+ else:
345
+ d = ILLDerivation(ILLSequent(_tuple_of(cnt), goal), rule, tuple(subs))
346
+ search.proven[key] = d
347
+ return d
348
+
349
+ if search.failed.get(key, -1) < depth:
350
+ search.failed[key] = depth
351
+ return None
352
+
353
+
354
+ def ill_prove(antecedents: Iterable[Node], goal: Node,
355
+ max_depth: Optional[int] = None,
356
+ max_steps: int = 200000) -> Optional[ILLDerivation]:
357
+ """Search for a cut-free ILL derivation of ``antecedents ⊢ goal``.
358
+
359
+ Returns an :class:`ILLDerivation` (which :func:`check_ill_proof` re-validates
360
+ before it is handed back, so a search bug can only lose proofs, never invent
361
+ them) or ``None``.
362
+
363
+ **What ``None`` means.** For a **!-free** sequent the search is a complete,
364
+ terminating decision procedure — every rule's premises are strictly smaller
365
+ than its conclusion, so with the default ``max_depth`` (the sequent's total
366
+ node count, which no cut-free derivation's height can exceed) and no step
367
+ budget, ``None`` *proves* the sequent underivable. When ``!`` occurs the
368
+ contraction rule ``!C`` grows sequents, so the search is bounded (default
369
+ depth ``2·size + 4`` and ``max_steps`` expansions) and ``None`` means only
370
+ "no derivation found within the bound".
371
+
372
+ ``max_depth`` counts rule applications along a branch; pass it explicitly to
373
+ search deeper !-sequents. ``max_steps`` bounds total node expansions and is
374
+ ignored for !-free sequents (where it could otherwise break completeness).
375
+
376
+ The sequent is read through the connectives ``⊗ & ⊕ ⊸ ! 𝟙 ⊤ 𝟘`` and atoms: an atom over
377
+ terms is one category, and every other node (a quantifier, a counting or cardinality node,
378
+ a sorted constant, an equality atom, a connective of another logic) is refused by name (see
379
+ the module docstring).
380
+
381
+ Raises:
382
+ NotImplementedError: a truth constant ``$true`` / ``$false``, or a node ILL has no
383
+ rule for (a quantifier, a counting or cardinality node, a sorted constant, an
384
+ equality atom, a connective of another logic) occurs in the sequent; the message
385
+ names the node and what to use instead.
386
+ """
387
+ ants = tuple(antecedents)
388
+ refuse_truth_constants([*ants, goal], "ill_prove", _NO_TRUTH_CONSTANT_WHY)
389
+ refuse_unreadable_input([*ants, goal], "ill_prove", ILL)
390
+ cnt = _cnt_of(ants)
391
+ total = sum(_size(f) for f in ants) + _size(goal)
392
+ bang = any(_has_bang(f) for f in ants) or _has_bang(goal)
393
+ if max_depth is None:
394
+ max_depth = 2 * total + 4 if bang else total
395
+ search = _IllSearch(max_steps if bang else None)
396
+ derivation = _prove(cnt, goal, max_depth, search)
397
+ if derivation is not None and not check_ill_proof(derivation):
398
+ raise RuntimeError(
399
+ "internal error: the ILL search assembled a derivation its own "
400
+ "checker rejects — please report this")
401
+ return derivation
402
+
403
+
404
+ def ill_derivable(antecedents: Iterable[Node], goal: Node,
405
+ max_depth: Optional[int] = None,
406
+ max_steps: int = 200000) -> bool:
407
+ """Return True iff :func:`ill_prove` finds a derivation of ``antecedents ⊢ goal``.
408
+
409
+ Decides derivability for !-free sequents; for sequents containing ``!`` a
410
+ ``False`` means only "no derivation within the bound" (see :func:`ill_prove`).
411
+ Refuses by name every node ILL has no rule for, as :func:`ill_prove` does.
412
+ """
413
+ return ill_prove(antecedents, goal, max_depth=max_depth, max_steps=max_steps) is not None
414
+
415
+
416
+ # ---------------------------------------------------------------------------
417
+ # The checker
418
+ # ---------------------------------------------------------------------------
419
+ #
420
+ # Each rule checker has signature (conclusion: ILLSequent, premises:
421
+ # List[ILLSequent]) and returns None when the step is licensed, or an error
422
+ # string. Antecedents are compared as multisets throughout.
423
+
424
+ def _c_eq(a: Counter, b: Counter) -> bool:
425
+ """Multiset equality with zero entries stripped."""
426
+ return _norm(a) == _norm(b)
427
+
428
+
429
+ def _r_ax(concl, prems):
430
+ """Ax: ``A ⊢ A`` — a single antecedent formula equal to the succedent."""
431
+ if prems:
432
+ return "Ax takes no premises"
433
+ if len(concl.antecedent) != 1 or concl.antecedent[0] != concl.succedent:
434
+ return "Ax: the antecedent must be exactly the succedent formula"
435
+ return None
436
+
437
+
438
+ def _r_one_r(concl, prems):
439
+ """1R: ``⊢ 𝟙`` — empty antecedent, unit succedent."""
440
+ if prems:
441
+ return "1R takes no premises"
442
+ if concl.antecedent or not isinstance(concl.succedent, One):
443
+ return "1R: the conclusion must be ⊢ 𝟙 with an empty antecedent"
444
+ return None
445
+
446
+
447
+ def _r_one_l(concl, prems):
448
+ """1L: from ``Γ ⊢ C`` infer ``Γ, 𝟙 ⊢ C``."""
449
+ if len(prems) != 1:
450
+ return "1L has one premise"
451
+ c = _cnt_of(concl.antecedent)
452
+ if not any(isinstance(f, One) for f in c):
453
+ return "1L: needs 𝟙 in the antecedent"
454
+ p = prems[0]
455
+ if p.succedent == concl.succedent and _c_eq(_cnt_of(p.antecedent), _minus(c, One())):
456
+ return None
457
+ return "1L: the premise must be the conclusion minus one 𝟙"
458
+
459
+
460
+ def _r_tensor_l(concl, prems):
461
+ """⊗L: from ``Γ, A, B ⊢ C`` infer ``Γ, A⊗B ⊢ C``."""
462
+ if len(prems) != 1:
463
+ return "⊗L has one premise"
464
+ c, p = _cnt_of(concl.antecedent), prems[0]
465
+ for f in c:
466
+ if isinstance(f, Tensor):
467
+ want = _plus(_minus(c, f), f.left, f.right)
468
+ if p.succedent == concl.succedent and _c_eq(_cnt_of(p.antecedent), want):
469
+ return None
470
+ return "⊗L: needs A⊗B on the left; premise Γ, A, B ⊢ C"
471
+
472
+
473
+ def _r_tensor_r(concl, prems):
474
+ """⊗R: from ``Γ ⊢ A`` and ``Δ ⊢ B`` infer ``Γ, Δ ⊢ A⊗B`` (context splits)."""
475
+ if len(prems) != 2:
476
+ return "⊗R has two premises"
477
+ g = concl.succedent
478
+ if not isinstance(g, Tensor):
479
+ return "⊗R must conclude a tensor A⊗B"
480
+ c = _cnt_of(concl.antecedent)
481
+ for p1, p2 in ((prems[0], prems[1]), (prems[1], prems[0])):
482
+ if (p1.succedent == g.left and p2.succedent == g.right
483
+ and _c_eq(_cnt_of(p1.antecedent) + _cnt_of(p2.antecedent), c)):
484
+ return None
485
+ return "⊗R: premises Γ⊢A and Δ⊢B with Γ⊎Δ the conclusion's antecedent"
486
+
487
+
488
+ def _r_limp_l(concl, prems):
489
+ """⊸L: from ``Γ ⊢ A`` and ``Δ, B ⊢ C`` infer ``Γ, Δ, A⊸B ⊢ C`` (context splits)."""
490
+ if len(prems) != 2:
491
+ return "⊸L has two premises"
492
+ c = _cnt_of(concl.antecedent)
493
+ for f in c:
494
+ if not isinstance(f, LinearImplies):
495
+ continue
496
+ rest = _minus(c, f)
497
+ for p1, p2 in ((prems[0], prems[1]), (prems[1], prems[0])):
498
+ if p1.succedent != f.left or p2.succedent != concl.succedent:
499
+ continue
500
+ p2c = _cnt_of(p2.antecedent)
501
+ if p2c[f.right] < 1:
502
+ continue
503
+ if _c_eq(_cnt_of(p1.antecedent) + _minus(p2c, f.right), rest):
504
+ return None
505
+ return "⊸L: needs A⊸B on the left; premises Γ⊢A and Δ,B⊢C with Γ⊎Δ the rest"
506
+
507
+
508
+ def _r_limp_r(concl, prems):
509
+ """⊸R: from ``Γ, A ⊢ B`` infer ``Γ ⊢ A⊸B``."""
510
+ if len(prems) != 1:
511
+ return "⊸R has one premise"
512
+ g = concl.succedent
513
+ if not isinstance(g, LinearImplies):
514
+ return "⊸R must conclude a linear implication A⊸B"
515
+ p = prems[0]
516
+ if (p.succedent == g.right
517
+ and _c_eq(_cnt_of(p.antecedent), _plus(_cnt_of(concl.antecedent), g.left))):
518
+ return None
519
+ return "⊸R: the premise must be Γ, A ⊢ B"
520
+
521
+
522
+ def _with_l(concl, prems, take_left: bool):
523
+ """Shared worker for &L1 / &L2."""
524
+ name = "&L1" if take_left else "&L2"
525
+ if len(prems) != 1:
526
+ return f"{name} has one premise"
527
+ c, p = _cnt_of(concl.antecedent), prems[0]
528
+ for f in c:
529
+ if isinstance(f, With):
530
+ kept = f.left if take_left else f.right
531
+ if (p.succedent == concl.succedent
532
+ and _c_eq(_cnt_of(p.antecedent), _plus(_minus(c, f), kept))):
533
+ return None
534
+ return f"{name}: needs A&B on the left; premise keeps the chosen component"
535
+
536
+
537
+ def _r_with_l1(concl, prems):
538
+ """&L1: from ``Γ, A ⊢ C`` infer ``Γ, A&B ⊢ C``."""
539
+ return _with_l(concl, prems, True)
540
+
541
+
542
+ def _r_with_l2(concl, prems):
543
+ """&L2: from ``Γ, B ⊢ C`` infer ``Γ, A&B ⊢ C``."""
544
+ return _with_l(concl, prems, False)
545
+
546
+
547
+ def _r_with_r(concl, prems):
548
+ """&R: from ``Γ ⊢ A`` and ``Γ ⊢ B`` infer ``Γ ⊢ A&B`` (context shared)."""
549
+ if len(prems) != 2:
550
+ return "&R has two premises"
551
+ g = concl.succedent
552
+ if not isinstance(g, With):
553
+ return "&R must conclude A&B"
554
+ c = _cnt_of(concl.antecedent)
555
+ for p1, p2 in ((prems[0], prems[1]), (prems[1], prems[0])):
556
+ if (p1.succedent == g.left and p2.succedent == g.right
557
+ and _c_eq(_cnt_of(p1.antecedent), c) and _c_eq(_cnt_of(p2.antecedent), c)):
558
+ return None
559
+ return "&R: premises Γ⊢A and Γ⊢B sharing the conclusion's antecedent"
560
+
561
+
562
+ def _r_oplus_l(concl, prems):
563
+ """⊕L: from ``Γ, A ⊢ C`` and ``Γ, B ⊢ C`` infer ``Γ, A⊕B ⊢ C``."""
564
+ if len(prems) != 2:
565
+ return "⊕L has two premises"
566
+ c = _cnt_of(concl.antecedent)
567
+ for f in c:
568
+ if not isinstance(f, OPlus):
569
+ continue
570
+ rest = _minus(c, f)
571
+ wa, wb = _plus(rest, f.left), _plus(rest, f.right)
572
+ for p1, p2 in ((prems[0], prems[1]), (prems[1], prems[0])):
573
+ if (p1.succedent == concl.succedent and p2.succedent == concl.succedent
574
+ and _c_eq(_cnt_of(p1.antecedent), wa)
575
+ and _c_eq(_cnt_of(p2.antecedent), wb)):
576
+ return None
577
+ return "⊕L: needs A⊕B on the left; premises Γ,A⊢C and Γ,B⊢C"
578
+
579
+
580
+ def _oplus_r(concl, prems, take_left: bool):
581
+ """Shared worker for ⊕R1 / ⊕R2."""
582
+ name = "⊕R1" if take_left else "⊕R2"
583
+ if len(prems) != 1:
584
+ return f"{name} has one premise"
585
+ g = concl.succedent
586
+ if not isinstance(g, OPlus):
587
+ return f"{name} must conclude A⊕B"
588
+ p = prems[0]
589
+ want = g.left if take_left else g.right
590
+ if p.succedent == want and _c_eq(_cnt_of(p.antecedent), _cnt_of(concl.antecedent)):
591
+ return None
592
+ return f"{name}: the premise must be Γ ⊢ {'A' if take_left else 'B'}"
593
+
594
+
595
+ def _r_oplus_r1(concl, prems):
596
+ """⊕R1: from ``Γ ⊢ A`` infer ``Γ ⊢ A⊕B``."""
597
+ return _oplus_r(concl, prems, True)
598
+
599
+
600
+ def _r_oplus_r2(concl, prems):
601
+ """⊕R2: from ``Γ ⊢ B`` infer ``Γ ⊢ A⊕B``."""
602
+ return _oplus_r(concl, prems, False)
603
+
604
+
605
+ def _r_bang_w(concl, prems):
606
+ """!W: from ``Γ ⊢ C`` infer ``Γ, !A ⊢ C``."""
607
+ if len(prems) != 1:
608
+ return "!W has one premise"
609
+ c, p = _cnt_of(concl.antecedent), prems[0]
610
+ for f in c:
611
+ if isinstance(f, OfCourse):
612
+ if (p.succedent == concl.succedent
613
+ and _c_eq(_cnt_of(p.antecedent), _minus(c, f))):
614
+ return None
615
+ return "!W: needs !A on the left; the premise drops it"
616
+
617
+
618
+ def _r_bang_c(concl, prems):
619
+ """!C: from ``Γ, !A, !A ⊢ C`` infer ``Γ, !A ⊢ C``."""
620
+ if len(prems) != 1:
621
+ return "!C has one premise"
622
+ c, p = _cnt_of(concl.antecedent), prems[0]
623
+ for f in c:
624
+ if isinstance(f, OfCourse):
625
+ if (p.succedent == concl.succedent
626
+ and _c_eq(_cnt_of(p.antecedent), _plus(c, f))):
627
+ return None
628
+ return "!C: needs !A on the left; the premise duplicates it"
629
+
630
+
631
+ def _r_bang_d(concl, prems):
632
+ """!D: from ``Γ, A ⊢ C`` infer ``Γ, !A ⊢ C``."""
633
+ if len(prems) != 1:
634
+ return "!D has one premise"
635
+ c, p = _cnt_of(concl.antecedent), prems[0]
636
+ for f in c:
637
+ if isinstance(f, OfCourse):
638
+ if (p.succedent == concl.succedent
639
+ and _c_eq(_cnt_of(p.antecedent), _plus(_minus(c, f), f.formula))):
640
+ return None
641
+ return "!D: needs !A on the left; the premise derelicts it to A"
642
+
643
+
644
+ def _r_bang_p(concl, prems):
645
+ """!P: from ``!Γ ⊢ A`` infer ``!Γ ⊢ !A`` (every antecedent formula !-prefixed)."""
646
+ if len(prems) != 1:
647
+ return "!P has one premise"
648
+ g = concl.succedent
649
+ if not isinstance(g, OfCourse):
650
+ return "!P must conclude !A"
651
+ if not all(isinstance(f, OfCourse) for f in concl.antecedent):
652
+ return "!P: every antecedent formula must be !-prefixed"
653
+ p = prems[0]
654
+ if (p.succedent == g.formula
655
+ and _c_eq(_cnt_of(p.antecedent), _cnt_of(concl.antecedent))):
656
+ return None
657
+ return "!P: the premise must be !Γ ⊢ A with the same antecedent"
658
+
659
+
660
+ def _r_top_r(concl, prems):
661
+ """⊤R: ``Γ ⊢ ⊤`` — no premises, any context, succedent must be ⊤."""
662
+ if prems:
663
+ return "⊤R takes no premises"
664
+ if not isinstance(concl.succedent, Top):
665
+ return "⊤R: the succedent must be ⊤"
666
+ return None
667
+
668
+
669
+ def _r_zero_l(concl, prems):
670
+ """0L: ``Γ, 𝟘 ⊢ C`` — no premises, needs 𝟘 in the antecedent, any succedent."""
671
+ if prems:
672
+ return "0L takes no premises"
673
+ if not any(isinstance(f, Zero) for f in concl.antecedent):
674
+ return "0L: needs 𝟘 in the antecedent"
675
+ return None
676
+
677
+
678
+ _ILL_RULES: Dict[str, Callable] = {
679
+ "Ax": _r_ax, "1R": _r_one_r, "1L": _r_one_l,
680
+ "⊗L": _r_tensor_l, "⊗R": _r_tensor_r,
681
+ "⊸L": _r_limp_l, "⊸R": _r_limp_r,
682
+ "&L1": _r_with_l1, "&L2": _r_with_l2, "&R": _r_with_r,
683
+ "⊕L": _r_oplus_l, "⊕R1": _r_oplus_r1, "⊕R2": _r_oplus_r2,
684
+ "!W": _r_bang_w, "!C": _r_bang_c, "!D": _r_bang_d, "!P": _r_bang_p,
685
+ "⊤R": _r_top_r, "0L": _r_zero_l,
686
+ }
687
+
688
+
689
+ def _verify(deriv: "ILLDerivation"):
690
+ """Recursive worker: return ``(rule, error)`` or ``(None, None)`` on success."""
691
+ if not isinstance(deriv, ILLDerivation):
692
+ return ("?", f"expected an ILLDerivation, got {type(deriv).__name__}")
693
+ for child in deriv.premises:
694
+ rule, err = _verify(child)
695
+ if err is not None:
696
+ return rule, err
697
+ fn = _ILL_RULES.get(deriv.rule)
698
+ if fn is None:
699
+ return deriv.rule, f"unknown ILL rule {deriv.rule!r}"
700
+ err = fn(deriv.conclusion, [c.conclusion for c in deriv.premises])
701
+ if err is not None:
702
+ return deriv.rule, err if err.startswith(deriv.rule) else f"{deriv.rule}: {err}"
703
+ return None, None
704
+
705
+
706
+ def _formulas_of(derivation: "ILLDerivation") -> List[Node]:
707
+ """Every formula of every sequent of a derivation tree."""
708
+ formulas: List[Node] = []
709
+ pending = [derivation]
710
+ while pending:
711
+ node = pending.pop()
712
+ if isinstance(node, ILLDerivation) and isinstance(node.conclusion, ILLSequent):
713
+ formulas.extend(node.conclusion.antecedent)
714
+ formulas.append(node.conclusion.succedent)
715
+ pending.extend(node.premises)
716
+ return formulas
717
+
718
+
719
+ def verify_ill_proof(derivation: "ILLDerivation") -> SequentResult:
720
+ """Check an ILL derivation and return a
721
+ :class:`~unicode_logic_kit.atp.sequent.SequentResult`.
722
+
723
+ Recursively re-validates that every node's conclusion follows from its
724
+ premises' conclusions by the node's rule (antecedents compared as multisets),
725
+ returning the end-sequent and, on failure, the first offending rule and reason.
726
+ A derivation whose sequents hold a node ILL has no rule for (a quantifier, an
727
+ equality atom, a connective of another logic, ...) is not a derivation of ILL:
728
+ it does not check, and the reason names the node (``error_rule`` is ``"formula"``).
729
+ """
730
+ unreadable = unreadable_reason(_formulas_of(derivation), "verify_ill_proof", ILL)
731
+ err_rule, err = ("formula", unreadable) if unreadable is not None else _verify(derivation)
732
+ end = derivation.conclusion if isinstance(derivation, ILLDerivation) else None
733
+ return SequentResult(err is None, end, err_rule, err)
734
+
735
+
736
+ def check_ill_proof(derivation: "ILLDerivation") -> bool:
737
+ """Return True iff ``derivation`` is a valid cut-free ILL derivation (sound)."""
738
+ return verify_ill_proof(derivation).ok