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,1050 @@
1
+ """Gentzen sequent calculus (LK) for second-order logic: a derivation *checker*.
2
+
3
+ Where :mod:`unicode_logic_kit.atp.fitch` checks a Fitch-style natural-deduction
4
+ proof, this module checks a **two-sided sequent-calculus derivation** — a tree of
5
+ sequents ``Γ ⊢ Δ`` (multisets of formulas on each side, read as
6
+ ``⋀Γ → ⋁Δ``), each obtained from its premises by a named inference rule. It is the
7
+ classical Gentzen system **LK** extended with the first-order quantifier rules and
8
+ the **second-order** quantifier rules (∀²/∃² over predicate variables), so it
9
+ covers the propositional, first-order, and second-order fragments of the kit.
10
+
11
+ ``check_sequent_proof`` is *sound*: it returns ``True`` only when every node of the
12
+ derivation genuinely follows from its premises by the stated rule. It is NOT a
13
+ prover, and for full second-order logic it *cannot* be completed into one — by
14
+ Gödel, standard second-order validity is not recursively enumerable, so every
15
+ sound second-order calculus is incomplete. The checker therefore verifies a
16
+ *given* derivation; soundness of the rules is by the LK metatheory, cross-checked
17
+ in the test-suite against Z3 (for the first-order-expressible sequents) and the
18
+ finite-model evaluator ``satisfies_so`` (for the genuinely second-order ones).
19
+
20
+ Design notes:
21
+
22
+ - **Additive (context-sharing) LK.** The side contexts ``Γ`` / ``Δ`` are shared
23
+ between a rule's premises and conclusion; weakening and contraction are explicit
24
+ structural rules. Contexts are compared as **multisets** (formula order does not
25
+ matter; multiplicity does).
26
+ - **Eigenvariable conditions.** ``∀R`` / ``∃L`` carry an object eigenvariable that
27
+ must not occur free in the lower sequent; ``∀²R`` / ``∃²L`` carry a *predicate*
28
+ eigenvariable with the analogous condition. Both are enforced.
29
+ - **Comprehension.** ``∀²L`` / ``∃²R`` instantiate the bound predicate variable
30
+ ``X`` with a comprehension term ``λx̄.ψ`` (arity = ``X``'s arity), replacing each
31
+ ``X(t̄)`` by ``ψ[x̄ := t̄]`` via a fresh, capture-avoiding predicate substitution.
32
+
33
+ Public API: :class:`Sequent`, :class:`Derivation`, :class:`SequentResult`, the
34
+ helpers :func:`sequent`, :func:`derive`, :func:`axiom`, the comprehension wrapper
35
+ :class:`Comprehension`, and the checkers :func:`check_sequent_proof` /
36
+ :func:`verify_sequent_proof`, plus :func:`render_sequent_proof` /
37
+ :meth:`Derivation.to_html`.
38
+ """
39
+
40
+ from collections import Counter
41
+ from dataclasses import dataclass, replace
42
+ from typing import Callable, Dict, List, Optional, Tuple
43
+
44
+ from ..fol.nodes import (
45
+ Node, Atom, Not, And, Or, Xor, Implies, Iff, Quantifier,
46
+ Variable,
47
+ SortedQuantifier, SecondOrderQuantifier, SlashedExists,
48
+ )
49
+ from ..fol._identifiers import fresh_variables, variable_names
50
+ from ..fol._msfl_nodes import _rename, _fresh_name, _names_in
51
+ from ..fol._truth_constants import is_true_constant, is_false_constant
52
+ from .fitch import _subst_var, _free_vars, _is_term, _q_kind, _VAR_BINDERS
53
+ from ._html import esc_html, html_page
54
+
55
+
56
+ # ---------------------------------------------------------------------------
57
+ # Sequents and derivations
58
+ # ---------------------------------------------------------------------------
59
+
60
+ @dataclass(frozen=True)
61
+ class Sequent:
62
+ """A two-sided sequent ``antecedent ⊢ succedent`` (each a multiset of formulas).
63
+
64
+ Read as ``⋀antecedent → ⋁succedent``. Both sides are tuples of :class:`Node`
65
+ formulas; their *order* is irrelevant (compared as multisets) but their
66
+ *multiplicity* matters (contraction/weakening are explicit rules). Frozen and
67
+ hashable.
68
+ """
69
+
70
+ antecedent: Tuple[Node, ...] = ()
71
+ succedent: Tuple[Node, ...] = ()
72
+
73
+ def __post_init__(self):
74
+ """Coerce both sides to tuples so the sequent stays hashable."""
75
+ object.__setattr__(self, "antecedent", tuple(self.antecedent))
76
+ object.__setattr__(self, "succedent", tuple(self.succedent))
77
+
78
+ def to_dict(self) -> dict:
79
+ """Serialise to a JSON-compatible dict."""
80
+ return {
81
+ "antecedent": [f.to_dict() for f in self.antecedent],
82
+ "succedent": [f.to_dict() for f in self.succedent],
83
+ }
84
+
85
+ @staticmethod
86
+ def from_dict(d: dict) -> "Sequent":
87
+ """Deserialise a Sequent produced by :meth:`to_dict`."""
88
+ return Sequent(
89
+ tuple(Node.from_dict(f) for f in d.get("antecedent", ())),
90
+ tuple(Node.from_dict(f) for f in d.get("succedent", ())),
91
+ )
92
+
93
+ def __str__(self) -> str:
94
+ """Render as ``A, B ⊢ C`` using each formula's Unicode form."""
95
+ left = ", ".join(f.to_unicode_str() for f in self.antecedent)
96
+ right = ", ".join(f.to_unicode_str() for f in self.succedent)
97
+ return f"{left} ⊢ {right}".strip()
98
+
99
+
100
+ @dataclass(frozen=True)
101
+ class Comprehension:
102
+ """A second-order comprehension term ``λx̄.ψ`` for ∀²L / ∃²R instantiation.
103
+
104
+ ``params`` is the tuple of object Variables ``x̄`` abstracted; ``body`` is the
105
+ formula ``ψ``. Instantiating a predicate variable ``X`` of arity ``len(params)``
106
+ with this replaces each ``X(t̄)`` by ``ψ[x̄ := t̄]``.
107
+ """
108
+
109
+ params: Tuple[Variable, ...]
110
+ body: Node
111
+
112
+ def __post_init__(self):
113
+ """Coerce ``params`` to a tuple so the comprehension stays hashable."""
114
+ object.__setattr__(self, "params", tuple(self.params))
115
+
116
+ @property
117
+ def arity(self) -> int:
118
+ """The arity of the abstracted relation (the number of parameters)."""
119
+ return len(self.params)
120
+
121
+ def to_dict(self) -> dict:
122
+ """Serialise to a JSON-compatible dict."""
123
+ return {
124
+ "params": [p.to_dict() for p in self.params],
125
+ "body": self.body.to_dict(),
126
+ }
127
+
128
+ @staticmethod
129
+ def from_dict(d: dict) -> "Comprehension":
130
+ """Deserialise a Comprehension produced by :meth:`to_dict`."""
131
+ return Comprehension(
132
+ tuple(Node.from_dict(p) for p in d["params"]),
133
+ Node.from_dict(d["body"]),
134
+ )
135
+
136
+
137
+ @dataclass(frozen=True)
138
+ class Derivation:
139
+ """A node of a sequent-calculus derivation tree.
140
+
141
+ ``conclusion`` is the sequent this node derives; ``rule`` is the inference rule
142
+ name (``"Ax"``, ``"∧R"``, ``"∀L"``, ``"∀²R"``, …); ``premises`` are the
143
+ sub-derivations whose conclusions are this rule's premises (empty for an
144
+ axiom). ``extra`` carries rule-specific data: the instantiation term of
145
+ ``∀L``/``∃R``, the eigenvariable of ``∀R``/``∃L``, the predicate eigenvariable
146
+ of ``∀²R``/``∃²L``, or the :class:`Comprehension` of ``∀²L``/``∃²R``.
147
+ """
148
+
149
+ conclusion: Sequent
150
+ rule: str
151
+ premises: Tuple["Derivation", ...] = ()
152
+ extra: Tuple = ()
153
+
154
+ def __post_init__(self):
155
+ """Coerce ``premises``/``extra`` to tuples so the derivation stays hashable."""
156
+ object.__setattr__(self, "premises", tuple(self.premises))
157
+ object.__setattr__(self, "extra", tuple(self.extra))
158
+
159
+ def to_dict(self) -> dict:
160
+ """Serialise the derivation tree to a JSON-compatible dict."""
161
+ return {
162
+ "conclusion": self.conclusion.to_dict(),
163
+ "rule": self.rule,
164
+ "premises": [p.to_dict() for p in self.premises],
165
+ "extra": [_extra_to_dict(e) for e in self.extra],
166
+ }
167
+
168
+ @staticmethod
169
+ def from_dict(d: dict) -> "Derivation":
170
+ """Deserialise a Derivation produced by :meth:`to_dict`."""
171
+ return Derivation(
172
+ Sequent.from_dict(d["conclusion"]),
173
+ d["rule"],
174
+ tuple(Derivation.from_dict(p) for p in d.get("premises", ())),
175
+ tuple(_extra_from_dict(e) for e in d.get("extra", ())),
176
+ )
177
+
178
+ def render(self) -> str:
179
+ """Render this derivation as an indented proof tree."""
180
+ return render_sequent_proof(self)
181
+
182
+ def to_html(self, title: str = "Sequent derivation") -> str:
183
+ """Render as a self-contained, theme-aware HTML page.
184
+
185
+ Same idiom as :meth:`unicode_logic_kit.fol.derivation.CCGDerivation.to_html`
186
+ (and the same tree-of-blocks layout): each node's premises sit above an
187
+ inference bar with the rule name — and, when present, the instantiation
188
+ term / eigenvariable / comprehension — to its right, exactly as
189
+ :func:`render_sequent_proof` surfaces ``extra`` in its bracketed label.
190
+ """
191
+ return html_page(title, _html_sequent_page(self), _SEQUENT_CSS)
192
+
193
+
194
+ def _extra_to_dict(e):
195
+ """Serialise one ``extra`` item (a Node, a Comprehension, or a scalar)."""
196
+ if isinstance(e, Comprehension):
197
+ return {"_extra": "comprehension", **e.to_dict()}
198
+ if isinstance(e, Node):
199
+ return {"_extra": "node", **e.to_dict()}
200
+ return {"_extra": "scalar", "value": e}
201
+
202
+
203
+ def _extra_from_dict(d):
204
+ """Deserialise one ``extra`` item produced by :func:`_extra_to_dict`."""
205
+ kind = d.get("_extra")
206
+ if kind == "comprehension":
207
+ return Comprehension.from_dict(d)
208
+ if kind == "node":
209
+ return Node.from_dict(d)
210
+ return d["value"]
211
+
212
+
213
+ @dataclass(frozen=True)
214
+ class SequentResult:
215
+ """The outcome of checking a derivation: ``ok`` plus the first failure."""
216
+
217
+ ok: bool
218
+ endsequent: Optional[Sequent]
219
+ error_rule: Optional[str]
220
+ error: Optional[str]
221
+
222
+ def __bool__(self) -> bool:
223
+ """A SequentResult is truthy iff the derivation checked out."""
224
+ return self.ok
225
+
226
+
227
+ # ---------------------------------------------------------------------------
228
+ # Authoring helpers
229
+ # ---------------------------------------------------------------------------
230
+
231
+ def sequent(antecedent, succedent) -> Sequent:
232
+ """Build a :class:`Sequent` from two iterables of formulas."""
233
+ return Sequent(tuple(antecedent), tuple(succedent))
234
+
235
+
236
+ def derive(conclusion: Sequent, rule: str, *premises, extra=()) -> Derivation:
237
+ """Build a :class:`Derivation` node with the given rule and sub-derivations."""
238
+ return Derivation(conclusion, rule, tuple(premises), tuple(extra))
239
+
240
+
241
+ def axiom(conclusion: Sequent) -> Derivation:
242
+ """Build an axiom leaf ``Γ, A ⊢ A, Δ`` (rule ``"Ax"``, no premises)."""
243
+ return Derivation(conclusion, "Ax", (), ())
244
+
245
+
246
+ # ---------------------------------------------------------------------------
247
+ # Multiset helpers
248
+ # ---------------------------------------------------------------------------
249
+
250
+ def _ms(formulas: Tuple[Node, ...]) -> Counter:
251
+ """Return the multiset (Counter) of a tuple of formulas."""
252
+ return Counter(formulas)
253
+
254
+
255
+ def _ms_eq(a: Tuple[Node, ...], b: Tuple[Node, ...]) -> bool:
256
+ """Return True iff two formula tuples are equal as multisets."""
257
+ return _ms(a) == _ms(b)
258
+
259
+
260
+ def _seq_eq(s: Sequent, antecedent: Tuple[Node, ...], succedent: Tuple[Node, ...]) -> bool:
261
+ """Return True iff sequent ``s`` equals ``antecedent ⊢ succedent`` (multiset)."""
262
+ return _ms_eq(s.antecedent, antecedent) and _ms_eq(s.succedent, succedent)
263
+
264
+
265
+ def _candidates(formulas: Tuple[Node, ...], pred: Callable[[Node], bool]):
266
+ """Yield ``(principal, rest)`` for each occurrence satisfying ``pred``.
267
+
268
+ ``rest`` is ``formulas`` with that one occurrence removed (order preserved for
269
+ the others; multiplicity respected). Distinct positions holding equal formulas
270
+ each yield once, which is harmless — the caller accepts on the first match.
271
+ """
272
+ seen_positions = set()
273
+ for i, f in enumerate(formulas):
274
+ if i in seen_positions or not pred(f):
275
+ continue
276
+ rest = formulas[:i] + formulas[i + 1:]
277
+ yield f, rest
278
+
279
+
280
+ # ---------------------------------------------------------------------------
281
+ # Rule checkers — propositional LK (additive / context-sharing)
282
+ # ---------------------------------------------------------------------------
283
+ #
284
+ # Each checker has signature (conclusion: Sequent, premises: List[Sequent],
285
+ # extra: tuple) and returns None when the step is licensed, or an error string.
286
+ # A rule picks its principal formula from the conclusion (a connective on the
287
+ # left for an L-rule, on the right for an R-rule), and the premises must equal the
288
+ # shared context plus the introduced sub-formulas (compared as multisets). Several
289
+ # occurrences of a matching principal are tried; the rule succeeds if ANY works.
290
+
291
+ RuleFn = Callable[[Sequent, List[Sequent], tuple], Optional[str]]
292
+
293
+
294
+ def _r_axiom(concl, prems, extra):
295
+ """Ax: ``Γ, A ⊢ A, Δ`` — some formula occurs on both sides.
296
+
297
+ Also the two axioms of the truth constants: ``Γ ⊢ Δ, $true`` (the constant
298
+ ``$true`` on the right) and ``Γ, $false ⊢ Δ`` (the constant ``$false`` on the
299
+ left). Nothing else is licensed: ``$true`` on the LEFT and ``$false`` on the RIGHT
300
+ say nothing, so a sequent that has only those is no axiom.
301
+ """
302
+ if prems:
303
+ return "Ax takes no premises"
304
+ shared = set(concl.antecedent) & set(concl.succedent)
305
+ if shared:
306
+ return None
307
+ if any(is_true_constant(f) for f in concl.succedent):
308
+ return None
309
+ if any(is_false_constant(f) for f in concl.antecedent):
310
+ return None
311
+ return ("Ax: no formula occurs on both sides of the sequent, and neither $true "
312
+ "on the right nor $false on the left")
313
+
314
+
315
+ def _r_weaken_l(concl, prems, extra):
316
+ """WL: from ``Γ ⊢ Δ`` infer ``Γ, A ⊢ Δ`` (A arbitrary)."""
317
+ if len(prems) != 1:
318
+ return "WL has one premise"
319
+ for _, rest in _candidates(concl.antecedent, lambda f: True):
320
+ if _seq_eq(prems[0], rest, concl.succedent):
321
+ return None
322
+ return "WL: the premise must be the conclusion minus one antecedent formula"
323
+
324
+
325
+ def _r_weaken_r(concl, prems, extra):
326
+ """WR: from ``Γ ⊢ Δ`` infer ``Γ ⊢ Δ, A`` (A arbitrary)."""
327
+ if len(prems) != 1:
328
+ return "WR has one premise"
329
+ for _, rest in _candidates(concl.succedent, lambda f: True):
330
+ if _seq_eq(prems[0], concl.antecedent, rest):
331
+ return None
332
+ return "WR: the premise must be the conclusion minus one succedent formula"
333
+
334
+
335
+ def _r_contract_l(concl, prems, extra):
336
+ """CL: from ``Γ, A, A ⊢ Δ`` infer ``Γ, A ⊢ Δ``."""
337
+ if len(prems) != 1:
338
+ return "CL has one premise"
339
+ for principal, _ in _candidates(concl.antecedent, lambda f: True):
340
+ if _seq_eq(prems[0], concl.antecedent + (principal,), concl.succedent):
341
+ return None
342
+ return "CL: the premise must duplicate one antecedent formula of the conclusion"
343
+
344
+
345
+ def _r_contract_r(concl, prems, extra):
346
+ """CR: from ``Γ ⊢ Δ, A, A`` infer ``Γ ⊢ Δ, A``."""
347
+ if len(prems) != 1:
348
+ return "CR has one premise"
349
+ for principal, _ in _candidates(concl.succedent, lambda f: True):
350
+ if _seq_eq(prems[0], concl.antecedent, concl.succedent + (principal,)):
351
+ return None
352
+ return "CR: the premise must duplicate one succedent formula of the conclusion"
353
+
354
+
355
+ def _r_cut(concl, prems, extra):
356
+ """Cut: from ``Γ ⊢ Δ, A`` and ``Γ, A ⊢ Δ`` infer ``Γ ⊢ Δ``."""
357
+ if len(prems) != 2:
358
+ return "Cut has two premises"
359
+ g, d = concl.antecedent, concl.succedent
360
+ for left, right in ((prems[0], prems[1]), (prems[1], prems[0])):
361
+ # left = Γ ⊢ Δ, A ; right = Γ, A ⊢ Δ
362
+ for a, rest_succ in _candidates(left.succedent, lambda f: True):
363
+ if not _ms_eq(rest_succ, d):
364
+ continue
365
+ if not _ms_eq(left.antecedent, g):
366
+ continue
367
+ if _seq_eq(right, g + (a,), d):
368
+ return None
369
+ return "Cut: premises must be Γ⊢Δ,A and Γ,A⊢Δ sharing the conclusion's context"
370
+
371
+
372
+ def _r_not_l(concl, prems, extra):
373
+ """¬L: from ``Γ ⊢ Δ, A`` infer ``Γ, ¬A ⊢ Δ``."""
374
+ if len(prems) != 1:
375
+ return "¬L has one premise"
376
+ for principal, rest in _candidates(concl.antecedent, lambda f: isinstance(f, Not)):
377
+ if _seq_eq(prems[0], rest, concl.succedent + (principal.formula,)):
378
+ return None
379
+ return "¬L: needs ¬A on the left; premise Γ ⊢ Δ, A"
380
+
381
+
382
+ def _r_not_r(concl, prems, extra):
383
+ """¬R: from ``Γ, A ⊢ Δ`` infer ``Γ ⊢ Δ, ¬A``."""
384
+ if len(prems) != 1:
385
+ return "¬R has one premise"
386
+ for principal, rest in _candidates(concl.succedent, lambda f: isinstance(f, Not)):
387
+ if _seq_eq(prems[0], concl.antecedent + (principal.formula,), rest):
388
+ return None
389
+ return "¬R: needs ¬A on the right; premise Γ, A ⊢ Δ"
390
+
391
+
392
+ def _r_and_l(concl, prems, extra):
393
+ """∧L: from ``Γ, A, B ⊢ Δ`` infer ``Γ, A∧B ⊢ Δ``."""
394
+ if len(prems) != 1:
395
+ return "∧L has one premise"
396
+ for principal, rest in _candidates(concl.antecedent, lambda f: isinstance(f, And)):
397
+ if _seq_eq(prems[0], rest + (principal.left, principal.right), concl.succedent):
398
+ return None
399
+ return "∧L: needs A∧B on the left; premise Γ, A, B ⊢ Δ"
400
+
401
+
402
+ def _r_and_r(concl, prems, extra):
403
+ """∧R: from ``Γ ⊢ Δ, A`` and ``Γ ⊢ Δ, B`` infer ``Γ ⊢ Δ, A∧B``."""
404
+ if len(prems) != 2:
405
+ return "∧R has two premises"
406
+ for principal, rest in _candidates(concl.succedent, lambda f: isinstance(f, And)):
407
+ w1 = (concl.antecedent, rest + (principal.left,))
408
+ w2 = (concl.antecedent, rest + (principal.right,))
409
+ if _two_match(prems, w1, w2):
410
+ return None
411
+ return "∧R: needs A∧B on the right; premises Γ⊢Δ,A and Γ⊢Δ,B"
412
+
413
+
414
+ def _r_or_l(concl, prems, extra):
415
+ """∨L: from ``Γ, A ⊢ Δ`` and ``Γ, B ⊢ Δ`` infer ``Γ, A∨B ⊢ Δ``."""
416
+ if len(prems) != 2:
417
+ return "∨L has two premises"
418
+ for principal, rest in _candidates(concl.antecedent, lambda f: isinstance(f, Or)):
419
+ w1 = (rest + (principal.left,), concl.succedent)
420
+ w2 = (rest + (principal.right,), concl.succedent)
421
+ if _two_match(prems, w1, w2):
422
+ return None
423
+ return "∨L: needs A∨B on the left; premises Γ,A⊢Δ and Γ,B⊢Δ"
424
+
425
+
426
+ def _r_or_r(concl, prems, extra):
427
+ """∨R: from ``Γ ⊢ Δ, A, B`` infer ``Γ ⊢ Δ, A∨B``."""
428
+ if len(prems) != 1:
429
+ return "∨R has one premise"
430
+ for principal, rest in _candidates(concl.succedent, lambda f: isinstance(f, Or)):
431
+ if _seq_eq(prems[0], concl.antecedent, rest + (principal.left, principal.right)):
432
+ return None
433
+ return "∨R: needs A∨B on the right; premise Γ ⊢ Δ, A, B"
434
+
435
+
436
+ def _r_imp_l(concl, prems, extra):
437
+ """→L: from ``Γ ⊢ Δ, A`` and ``Γ, B ⊢ Δ`` infer ``Γ, A→B ⊢ Δ``."""
438
+ if len(prems) != 2:
439
+ return "→L has two premises"
440
+ for principal, rest in _candidates(concl.antecedent, lambda f: isinstance(f, Implies)):
441
+ w1 = (rest, concl.succedent + (principal.left,))
442
+ w2 = (rest + (principal.right,), concl.succedent)
443
+ if _two_match(prems, w1, w2):
444
+ return None
445
+ return "→L: needs A→B on the left; premises Γ⊢Δ,A and Γ,B⊢Δ"
446
+
447
+
448
+ def _r_imp_r(concl, prems, extra):
449
+ """→R: from ``Γ, A ⊢ Δ, B`` infer ``Γ ⊢ Δ, A→B``."""
450
+ if len(prems) != 1:
451
+ return "→R has one premise"
452
+ for principal, rest in _candidates(concl.succedent, lambda f: isinstance(f, Implies)):
453
+ if _seq_eq(prems[0], concl.antecedent + (principal.left,), rest + (principal.right,)):
454
+ return None
455
+ return "→R: needs A→B on the right; premise Γ, A ⊢ Δ, B"
456
+
457
+
458
+ def _r_iff_l(concl, prems, extra):
459
+ """↔L: from ``Γ, A, B ⊢ Δ`` and ``Γ ⊢ Δ, A, B`` infer ``Γ, A↔B ⊢ Δ``."""
460
+ if len(prems) != 2:
461
+ return "↔L has two premises"
462
+ for principal, rest in _candidates(concl.antecedent, lambda f: isinstance(f, Iff)):
463
+ a, b = principal.left, principal.right
464
+ w1 = (rest + (a, b), concl.succedent)
465
+ w2 = (rest, concl.succedent + (a, b))
466
+ if _two_match(prems, w1, w2):
467
+ return None
468
+ return "↔L: needs A↔B on the left; premises Γ,A,B⊢Δ and Γ⊢Δ,A,B"
469
+
470
+
471
+ def _r_iff_r(concl, prems, extra):
472
+ """↔R: from ``Γ, A ⊢ Δ, B`` and ``Γ, B ⊢ Δ, A`` infer ``Γ ⊢ Δ, A↔B``."""
473
+ if len(prems) != 2:
474
+ return "↔R has two premises"
475
+ for principal, rest in _candidates(concl.succedent, lambda f: isinstance(f, Iff)):
476
+ a, b = principal.left, principal.right
477
+ w1 = (concl.antecedent + (a,), rest + (b,))
478
+ w2 = (concl.antecedent + (b,), rest + (a,))
479
+ if _two_match(prems, w1, w2):
480
+ return None
481
+ return "↔R: needs A↔B on the right; premises Γ,A⊢Δ,B and Γ,B⊢Δ,A"
482
+
483
+
484
+ def _r_xor_l(concl, prems, extra):
485
+ """⊕L: from ``Γ, A ⊢ Δ, B`` and ``Γ, B ⊢ Δ, A`` infer ``Γ, A⊕B ⊢ Δ``.
486
+
487
+ ``A⊕B ≡ ¬(A↔B)``, so ⊕L mirrors ↔R (one side negated).
488
+ """
489
+ if len(prems) != 2:
490
+ return "⊕L has two premises"
491
+ for principal, rest in _candidates(concl.antecedent, lambda f: isinstance(f, Xor)):
492
+ a, b = principal.left, principal.right
493
+ w1 = (rest + (a,), concl.succedent + (b,))
494
+ w2 = (rest + (b,), concl.succedent + (a,))
495
+ if _two_match(prems, w1, w2):
496
+ return None
497
+ return "⊕L: needs A⊕B on the left; premises Γ,A⊢Δ,B and Γ,B⊢Δ,A"
498
+
499
+
500
+ def _r_xor_r(concl, prems, extra):
501
+ """⊕R: from ``Γ, A, B ⊢ Δ`` and ``Γ ⊢ Δ, A, B`` infer ``Γ ⊢ Δ, A⊕B``.
502
+
503
+ ``A⊕B ≡ ¬(A↔B)``, so ⊕R mirrors ↔L.
504
+ """
505
+ if len(prems) != 2:
506
+ return "⊕R has two premises"
507
+ for principal, rest in _candidates(concl.succedent, lambda f: isinstance(f, Xor)):
508
+ a, b = principal.left, principal.right
509
+ w1 = (concl.antecedent + (a, b), rest)
510
+ w2 = (concl.antecedent, rest + (a, b))
511
+ if _two_match(prems, w1, w2):
512
+ return None
513
+ return "⊕R: needs A⊕B on the right; premises Γ,A,B⊢Δ and Γ⊢Δ,A,B"
514
+
515
+
516
+ def _two_match(prems: List[Sequent], w1, w2) -> bool:
517
+ """Return True iff the two premises equal sequents w1 and w2 in either order."""
518
+ a, b = prems
519
+ return ((_seq_eq(a, *w1) and _seq_eq(b, *w2))
520
+ or (_seq_eq(a, *w2) and _seq_eq(b, *w1)))
521
+
522
+
523
+ _PROP_RULES: Dict[str, RuleFn] = {
524
+ "Ax": _r_axiom,
525
+ "WL": _r_weaken_l, "WR": _r_weaken_r,
526
+ "CL": _r_contract_l, "CR": _r_contract_r,
527
+ "Cut": _r_cut,
528
+ "¬L": _r_not_l, "¬R": _r_not_r,
529
+ "∧L": _r_and_l, "∧R": _r_and_r,
530
+ "∨L": _r_or_l, "∨R": _r_or_r,
531
+ "→L": _r_imp_l, "→R": _r_imp_r,
532
+ "↔L": _r_iff_l, "↔R": _r_iff_r,
533
+ "⊕L": _r_xor_l, "⊕R": _r_xor_r,
534
+ }
535
+
536
+
537
+ # ---------------------------------------------------------------------------
538
+ # Rule checkers — first-order quantifiers
539
+ # ---------------------------------------------------------------------------
540
+
541
+ def _eigenvar_not_free(a: Variable, seq: Sequent) -> Optional[str]:
542
+ """Return an error if object eigenvariable ``a`` occurs free in the lower sequent."""
543
+ for f in seq.antecedent + seq.succedent:
544
+ if a in _free_vars(f):
545
+ return (f"eigenvariable {a.name} occurs free in the lower sequent "
546
+ f"({f.to_unicode_str()})")
547
+ return None
548
+
549
+
550
+ def _r_forall_l(concl, prems, extra):
551
+ """∀L: from ``Γ, A[x:=t] ⊢ Δ`` infer ``Γ, ∀x A ⊢ Δ`` (t any term, in extra)."""
552
+ if len(prems) != 1:
553
+ return "∀L has one premise"
554
+ if len(extra) != 1 or not _is_term(extra[0]):
555
+ return "∀L needs the instantiation term in extra"
556
+ t = extra[0]
557
+ for principal, rest in _candidates(concl.antecedent, lambda f: _q_kind(f) == "∀"):
558
+ inst = _subst_var(principal.formula, principal.variable, t)
559
+ if _seq_eq(prems[0], rest + (inst,), concl.succedent):
560
+ return None
561
+ return "needs ∀x A on the left and premise Γ, A[x:=t] ⊢ Δ"
562
+
563
+
564
+ def _r_exists_r(concl, prems, extra):
565
+ """∃R: from ``Γ ⊢ Δ, A[x:=t]`` infer ``Γ ⊢ Δ, ∃x A`` (t any term, in extra)."""
566
+ if len(prems) != 1:
567
+ return "∃R has one premise"
568
+ if len(extra) != 1 or not _is_term(extra[0]):
569
+ return "∃R needs the witness term in extra"
570
+ t = extra[0]
571
+ for principal, rest in _candidates(concl.succedent, lambda f: _q_kind(f) == "∃"):
572
+ inst = _subst_var(principal.formula, principal.variable, t)
573
+ if _seq_eq(prems[0], concl.antecedent, rest + (inst,)):
574
+ return None
575
+ return "needs ∃x A on the right and premise Γ ⊢ Δ, A[x:=t]"
576
+
577
+
578
+ def _r_forall_r(concl, prems, extra):
579
+ """∀R: from ``Γ ⊢ Δ, A[x:=a]`` infer ``Γ ⊢ Δ, ∀x A`` (eigenvariable a, in extra)."""
580
+ if len(prems) != 1:
581
+ return "∀R has one premise"
582
+ if len(extra) != 1 or not isinstance(extra[0], Variable):
583
+ return "∀R needs the eigenvariable in extra"
584
+ a = extra[0]
585
+ for principal, rest in _candidates(concl.succedent, lambda f: _q_kind(f) == "∀"):
586
+ inst = _subst_var(principal.formula, principal.variable, a)
587
+ if _seq_eq(prems[0], concl.antecedent, rest + (inst,)):
588
+ err = _eigenvar_not_free(a, concl)
589
+ return err if err else None
590
+ return "needs ∀x A on the right and premise Γ ⊢ Δ, A[x:=a]"
591
+
592
+
593
+ def _r_exists_l(concl, prems, extra):
594
+ """∃L: from ``Γ, A[x:=a] ⊢ Δ`` infer ``Γ, ∃x A ⊢ Δ`` (eigenvariable a, in extra)."""
595
+ if len(prems) != 1:
596
+ return "∃L has one premise"
597
+ if len(extra) != 1 or not isinstance(extra[0], Variable):
598
+ return "∃L needs the eigenvariable in extra"
599
+ a = extra[0]
600
+ for principal, rest in _candidates(concl.antecedent, lambda f: _q_kind(f) == "∃"):
601
+ inst = _subst_var(principal.formula, principal.variable, a)
602
+ if _seq_eq(prems[0], rest + (inst,), concl.succedent):
603
+ err = _eigenvar_not_free(a, concl)
604
+ return err if err else None
605
+ return "needs ∃x A on the left and premise Γ, A[x:=a] ⊢ Δ"
606
+
607
+
608
+ _FO_RULES: Dict[str, RuleFn] = {
609
+ "∀L": _r_forall_l, "∀R": _r_forall_r,
610
+ "∃L": _r_exists_l, "∃R": _r_exists_r,
611
+ }
612
+
613
+
614
+ # ---------------------------------------------------------------------------
615
+ # Second-order machinery: predicate substitution + free predicate variables
616
+ # ---------------------------------------------------------------------------
617
+
618
+ def _so_kind(node: Node) -> Optional[str]:
619
+ """Normalise a SecondOrderQuantifier to '∀' / '∃', or None for other nodes."""
620
+ if isinstance(node, SecondOrderQuantifier):
621
+ if node.type in ("∀", "forall"):
622
+ return "∀"
623
+ if node.type in ("∃", "exists"):
624
+ return "∃"
625
+ return None
626
+
627
+
628
+ def _free_pred_vars(node: Node, bound: frozenset = frozenset()) -> set:
629
+ """Return predicate names applied free in ``node`` (not bound by an enclosing ∀²/∃²).
630
+
631
+ ``=`` / ``≠`` are excluded (built-in identity, never a predicate variable). Used
632
+ for the second-order eigenvariable freshness condition.
633
+ """
634
+ if isinstance(node, SecondOrderQuantifier):
635
+ return _free_pred_vars(node.formula, bound | {node.predicate})
636
+ result: set = set()
637
+ if isinstance(node, Atom):
638
+ if node.predicate not in ("=", "≠") and node.predicate not in bound:
639
+ result.add(node.predicate)
640
+ for child in node._child_nodes():
641
+ result |= _free_pred_vars(child, bound)
642
+ return result
643
+
644
+
645
+ def _all_pred_names(node: Node) -> set:
646
+ """Return every predicate name occurring in ``node`` (applied or as an ∀²/∃² binder).
647
+
648
+ Includes bound predicate names (needed for freshness), excludes ``=`` / ``≠``.
649
+ """
650
+ names: set = set()
651
+ for n in node.walk():
652
+ if isinstance(n, Atom) and n.predicate not in ("=", "≠"):
653
+ names.add(n.predicate)
654
+ elif isinstance(n, SecondOrderQuantifier):
655
+ names.add(n.predicate)
656
+ return names
657
+
658
+
659
+ def _fresh_pred_name(base: str, avoid: set) -> str:
660
+ """Return the first ``base_N`` (N = 0, 1, …) not in ``avoid``.
661
+
662
+ Legal as it stands, unlike an object variable's ``y_0``: a second-order variable
663
+ is a PREDICATE, and PREDICATE takes underscores and digits after its uppercase
664
+ first letter, so ``W`` becomes ``W_0`` and the text still parses (checked in
665
+ ``tests/test_alpha_renaming_names.py``). ``avoid`` is every predicate name in
666
+ the scope, bound ones included.
667
+ """
668
+ i = 0
669
+ while True:
670
+ candidate = f"{base}_{i}"
671
+ if candidate not in avoid:
672
+ return candidate
673
+ i += 1
674
+
675
+
676
+ def _rename_pred(node: Node, x_name: str, y_name: str) -> Node:
677
+ """Rename the (free) predicate variable ``x_name`` to ``y_name`` throughout ``node``.
678
+
679
+ Capture-avoiding in BOTH directions: it stops at an inner ∀²/∃² that rebinds the
680
+ SOURCE ``x_name`` (shadowing), and α-renames an inner ∀²/∃² that binds the
681
+ TARGET ``y_name`` to a fresh predicate name first, so the introduced ``Y(…)``
682
+ atoms are never captured by an inner binder of the same name. Used for the
683
+ predicate-eigenvariable instantiation A[X:=Y] of ∀²R / ∃²L.
684
+ """
685
+ if isinstance(node, SecondOrderQuantifier):
686
+ if node.predicate == x_name:
687
+ return node # source rebound here: occurrences below are shadowed
688
+ if node.predicate == y_name and x_name != y_name:
689
+ # The inner binder would capture the introduced target name; α-rename it.
690
+ avoid = _all_pred_names(node.formula) | {x_name, y_name}
691
+ fresh = _fresh_pred_name(y_name, avoid)
692
+ renamed_body = _rename_pred(node.formula, y_name, fresh)
693
+ return SecondOrderQuantifier(node.type, fresh, node.arity,
694
+ _rename_pred(renamed_body, x_name, y_name))
695
+ return SecondOrderQuantifier(node.type, node.predicate, node.arity,
696
+ _rename_pred(node.formula, x_name, y_name))
697
+ if isinstance(node, Atom) and node.predicate == x_name and x_name not in ("=", "≠"):
698
+ return Atom(y_name, node.args)
699
+ return node.map_children(lambda c: _rename_pred(c, x_name, y_name))
700
+
701
+
702
+ def _subst_simultaneous(psi: Node, params: Tuple[Variable, ...], args) -> Node:
703
+ """Return ``ψ[x̄ := t̄]`` — simultaneous capture-avoiding object substitution.
704
+
705
+ Each parameter is first routed through a fresh temporary so that a later
706
+ argument mentioning an earlier parameter's name cannot interfere.
707
+ """
708
+ if not params:
709
+ return psi
710
+ # A temporary is an ordinary variable (it goes through _subst_var, which treats it
711
+ # as a free variable of the replacement), so it takes a name the parser reads back
712
+ # -- letter ``c`` and digits -- and it avoids EVERY name in play, bound ones too:
713
+ # a temporary called like a binder of psi would make _subst_var rename that
714
+ # binder, and the result would come back with bound variables nobody asked to
715
+ # rename.
716
+ taken = set(variable_names(psi, *args)) | {p.name for p in params}
717
+ result = psi
718
+ temps: List[Variable] = []
719
+ for p in params:
720
+ tmp = Variable(fresh_variables(1, letter="c", avoid=taken)[0])
721
+ taken.add(tmp.name)
722
+ result = _subst_var(result, p, tmp)
723
+ temps.append(tmp)
724
+ for tmp, arg in zip(temps, args):
725
+ result = _subst_var(result, tmp, arg)
726
+ return result
727
+
728
+
729
+ def _subst_pred(node: Node, x_name: str, params: Tuple[Variable, ...], psi: Node) -> Node:
730
+ """Return ``A[X := λx̄.ψ]`` — capture-avoiding comprehension instantiation.
731
+
732
+ Replaces each free application ``X(t̄)`` by ``ψ[x̄ := t̄]``. Stops at an inner
733
+ ∀²/∃² that rebinds ``X``; α-renames an object binder of ``A`` that would capture
734
+ a free OBJECT variable of ``ψ``, and an inner ∀²/∃² binder of ``A`` that would
735
+ capture a free PREDICATE variable of ``ψ``. Raises ValueError on an application
736
+ of ``X`` whose arity differs from ``len(params)``.
737
+ """
738
+ psi_fv = _free_vars(psi) - set(params)
739
+ psi_pred_fv = _free_pred_vars(psi)
740
+ return _subst_pred_inner(node, x_name, params, psi, psi_fv, psi_pred_fv)
741
+
742
+
743
+ def _subst_pred_inner(node, x_name, params, psi, psi_fv, psi_pred_fv):
744
+ """Recursive worker for :func:`_subst_pred`."""
745
+ if isinstance(node, SecondOrderQuantifier) and node.predicate == x_name:
746
+ return node # inner binder shadows X
747
+ if isinstance(node, Atom) and node.predicate == x_name and x_name not in ("=", "≠"):
748
+ if len(node.args) != len(params):
749
+ raise ValueError(
750
+ f"comprehension arity {len(params)} does not match an application "
751
+ f"of {x_name} at arity {len(node.args)}"
752
+ )
753
+ return _subst_simultaneous(psi, params, node.args)
754
+ if isinstance(node, _VAR_BINDERS):
755
+ y = node.variable
756
+ body = node.formula
757
+ if y in psi_fv:
758
+ # An object binder of A would capture a free object variable of ψ. The
759
+ # target here is a PREDICATE, so a slash set is never itself substituted —
760
+ # but its names must still be avoided when minting the fresh binder.
761
+ # The fresh name must also differ from every BOUND name inside the scope
762
+ # (``_names_in``, not ``_free_vars``): _rename moves y's occurrences
763
+ # onto it, and an inner binder that already uses the name would capture
764
+ # them.
765
+ avoid = psi_fv | _names_in(body) | {y}
766
+ if isinstance(node, SlashedExists):
767
+ avoid = avoid | {Variable(n) for n in node.slashed}
768
+ fresh = Variable(_fresh_name(y.name, avoid))
769
+ body = _rename(body, y, fresh)
770
+ y = fresh
771
+ inner = _subst_pred_inner(body, x_name, params, psi, psi_fv, psi_pred_fv)
772
+ # Counting quantifiers and cardinality terms bind an object variable too;
773
+ # replace rebuilds each binder shape from its ``variable``/``formula`` pair.
774
+ return replace(node, variable=y, formula=inner)
775
+ if isinstance(node, SecondOrderQuantifier):
776
+ pred = node.predicate
777
+ body = node.formula
778
+ if pred in psi_pred_fv:
779
+ # A second-order binder of A would capture a free predicate variable of ψ.
780
+ avoid = psi_pred_fv | _all_pred_names(body) | {x_name, pred}
781
+ fresh = _fresh_pred_name(pred, avoid)
782
+ body = _rename_pred(body, pred, fresh)
783
+ pred = fresh
784
+ return SecondOrderQuantifier(
785
+ node.type, pred, node.arity,
786
+ _subst_pred_inner(body, x_name, params, psi, psi_fv, psi_pred_fv))
787
+ return node.map_children(
788
+ lambda c: _subst_pred_inner(c, x_name, params, psi, psi_fv, psi_pred_fv))
789
+
790
+
791
+ def _pred_eigenvar_not_free(y_name: str, seq: Sequent) -> Optional[str]:
792
+ """Return an error if predicate eigenvariable ``y_name`` occurs free in the sequent."""
793
+ for f in seq.antecedent + seq.succedent:
794
+ if y_name in _free_pred_vars(f):
795
+ return (f"predicate eigenvariable {y_name} occurs free in the lower "
796
+ f"sequent ({f.to_unicode_str()})")
797
+ return None
798
+
799
+
800
+ # ---------------------------------------------------------------------------
801
+ # Rule checkers — second-order quantifiers
802
+ # ---------------------------------------------------------------------------
803
+
804
+ def _r_so_forall_l(concl, prems, extra):
805
+ """∀²L: from ``Γ, A[X:=λx̄.ψ] ⊢ Δ`` infer ``Γ, ∀X A ⊢ Δ`` (Comprehension in extra)."""
806
+ if len(prems) != 1:
807
+ return "∀²L has one premise"
808
+ if len(extra) != 1 or not isinstance(extra[0], Comprehension):
809
+ return "∀²L needs a Comprehension λx̄.ψ in extra"
810
+ comp = extra[0]
811
+ for principal, rest in _candidates(concl.antecedent, lambda f: _so_kind(f) == "∀"):
812
+ if comp.arity != principal.arity:
813
+ continue
814
+ try:
815
+ inst = _subst_pred(principal.formula, principal.predicate, comp.params, comp.body)
816
+ except ValueError:
817
+ continue
818
+ if _seq_eq(prems[0], rest + (inst,), concl.succedent):
819
+ return None
820
+ return "needs ∀X A on the left and premise Γ, A[X:=λx̄.ψ] ⊢ Δ (arity must match)"
821
+
822
+
823
+ def _r_so_exists_r(concl, prems, extra):
824
+ """∃²R: from ``Γ ⊢ Δ, A[X:=λx̄.ψ]`` infer ``Γ ⊢ Δ, ∃X A`` (Comprehension in extra)."""
825
+ if len(prems) != 1:
826
+ return "∃²R has one premise"
827
+ if len(extra) != 1 or not isinstance(extra[0], Comprehension):
828
+ return "∃²R needs a Comprehension λx̄.ψ in extra"
829
+ comp = extra[0]
830
+ for principal, rest in _candidates(concl.succedent, lambda f: _so_kind(f) == "∃"):
831
+ if comp.arity != principal.arity:
832
+ continue
833
+ try:
834
+ inst = _subst_pred(principal.formula, principal.predicate, comp.params, comp.body)
835
+ except ValueError:
836
+ continue
837
+ if _seq_eq(prems[0], concl.antecedent, rest + (inst,)):
838
+ return None
839
+ return "needs ∃X A on the right and premise Γ ⊢ Δ, A[X:=λx̄.ψ] (arity must match)"
840
+
841
+
842
+ def _r_so_forall_r(concl, prems, extra):
843
+ """∀²R: from ``Γ ⊢ Δ, A[X:=Y]`` infer ``Γ ⊢ Δ, ∀X A`` (predicate eigenvariable Y)."""
844
+ if len(prems) != 1:
845
+ return "∀²R has one premise"
846
+ if len(extra) != 1 or not isinstance(extra[0], str):
847
+ return "∀²R needs the predicate-eigenvariable name (a str) in extra"
848
+ y_name = extra[0]
849
+ for principal, rest in _candidates(concl.succedent, lambda f: _so_kind(f) == "∀"):
850
+ inst = _rename_pred(principal.formula, principal.predicate, y_name)
851
+ if _seq_eq(prems[0], concl.antecedent, rest + (inst,)):
852
+ err = _pred_eigenvar_not_free(y_name, concl)
853
+ return err if err else None
854
+ return "needs ∀X A on the right and premise Γ ⊢ Δ, A[X:=Y]"
855
+
856
+
857
+ def _r_so_exists_l(concl, prems, extra):
858
+ """∃²L: from ``Γ, A[X:=Y] ⊢ Δ`` infer ``Γ, ∃X A ⊢ Δ`` (predicate eigenvariable Y)."""
859
+ if len(prems) != 1:
860
+ return "∃²L has one premise"
861
+ if len(extra) != 1 or not isinstance(extra[0], str):
862
+ return "∃²L needs the predicate-eigenvariable name (a str) in extra"
863
+ y_name = extra[0]
864
+ for principal, rest in _candidates(concl.antecedent, lambda f: _so_kind(f) == "∃"):
865
+ inst = _rename_pred(principal.formula, principal.predicate, y_name)
866
+ if _seq_eq(prems[0], rest + (inst,), concl.succedent):
867
+ err = _pred_eigenvar_not_free(y_name, concl)
868
+ return err if err else None
869
+ return "needs ∃X A on the left and premise Γ, A[X:=Y] ⊢ Δ"
870
+
871
+
872
+ _SO_RULES: Dict[str, RuleFn] = {
873
+ "∀²L": _r_so_forall_l, "∀²R": _r_so_forall_r,
874
+ "∃²L": _r_so_exists_l, "∃²R": _r_so_exists_r,
875
+ }
876
+
877
+
878
+ _RULES: Dict[str, RuleFn] = {**_PROP_RULES, **_FO_RULES, **_SO_RULES}
879
+
880
+
881
+ # ---------------------------------------------------------------------------
882
+ # The checker
883
+ # ---------------------------------------------------------------------------
884
+
885
+ def _canon(node: Node) -> Node:
886
+ """Canonicalise quantifier spellings ('forall'/'exists' → '∀'/'∃') throughout.
887
+
888
+ Quantifier nodes are keyed on the raw ``type`` string, so the multiset ``==``
889
+ the sequent rules rely on would falsely reject a derivation that mixed the glyph
890
+ and word spellings. Normalising every formula entering the checker prevents that.
891
+ """
892
+ node = node.map_children(_canon)
893
+ if isinstance(node, Quantifier) and node.type in ("forall", "exists"):
894
+ return Quantifier("∀" if node.type == "forall" else "∃", node.variable, node.formula)
895
+ if isinstance(node, SortedQuantifier) and node.type in ("forall", "exists"):
896
+ return SortedQuantifier("∀" if node.type == "forall" else "∃",
897
+ node.variable, node.sort, node.formula)
898
+ if isinstance(node, SecondOrderQuantifier) and node.type in ("forall", "exists"):
899
+ return SecondOrderQuantifier("∀" if node.type == "forall" else "∃",
900
+ node.predicate, node.arity, node.formula)
901
+ return node
902
+
903
+
904
+ def _canon_extra(item):
905
+ """Canonicalise one ``extra`` payload (Comprehension body / Node), else pass through."""
906
+ if isinstance(item, Comprehension):
907
+ return Comprehension(item.params, _canon(item.body))
908
+ if isinstance(item, Node):
909
+ return _canon(item)
910
+ return item
911
+
912
+
913
+ def _canon_derivation(d: "Derivation") -> "Derivation":
914
+ """Rebuild a derivation tree with every sequent formula quantifier-canonicalised."""
915
+ conclusion = Sequent(tuple(_canon(f) for f in d.conclusion.antecedent),
916
+ tuple(_canon(f) for f in d.conclusion.succedent))
917
+ premises = tuple(_canon_derivation(p) for p in d.premises)
918
+ extra = tuple(_canon_extra(e) for e in d.extra)
919
+ return Derivation(conclusion, d.rule, premises, extra)
920
+
921
+
922
+ def verify_sequent_proof(derivation: "Derivation") -> SequentResult:
923
+ """Check ``derivation`` and return a :class:`SequentResult`.
924
+
925
+ Recursively verifies that every node's conclusion follows from its premises'
926
+ conclusions by the node's rule, returning the end-sequent and, on failure, the
927
+ first offending rule and reason.
928
+ """
929
+ if isinstance(derivation, Derivation):
930
+ derivation = _canon_derivation(derivation)
931
+ err_rule, err = _verify(derivation)
932
+ end = derivation.conclusion if derivation is not None else None
933
+ return SequentResult(err is None, end, err_rule, err)
934
+
935
+
936
+ def _verify(deriv: "Derivation"):
937
+ """Recursive worker: return ``(rule, error)`` or ``(None, None)`` on success."""
938
+ if not isinstance(deriv, Derivation):
939
+ return ("?", f"expected a Derivation, got {type(deriv).__name__}")
940
+ # Children first, so the deepest error surfaces.
941
+ for child in deriv.premises:
942
+ rule, err = _verify(child)
943
+ if err is not None:
944
+ return rule, err
945
+ fn = _RULES.get(deriv.rule)
946
+ if fn is None:
947
+ return deriv.rule, f"unknown rule {deriv.rule!r}"
948
+ premise_sequents = [c.conclusion for c in deriv.premises]
949
+ err = fn(deriv.conclusion, premise_sequents, deriv.extra)
950
+ if err is not None:
951
+ return deriv.rule, f"{deriv.rule}: {err}" if not err.startswith(deriv.rule) else err
952
+ return None, None
953
+
954
+
955
+ def check_sequent_proof(derivation: "Derivation") -> bool:
956
+ """Return True iff ``derivation`` is a valid sequent-calculus derivation (sound)."""
957
+ return verify_sequent_proof(derivation).ok
958
+
959
+
960
+ # ---------------------------------------------------------------------------
961
+ # Rendering
962
+ # ---------------------------------------------------------------------------
963
+
964
+ def _fmt_extra(extra: tuple) -> str:
965
+ """Render a rule's ``extra`` payload as a short annotation."""
966
+ if not extra:
967
+ return ""
968
+ parts = []
969
+ for e in extra:
970
+ if isinstance(e, Comprehension):
971
+ params = ",".join(p.name for p in e.params)
972
+ parts.append(f"λ{params}.{e.body.to_unicode_str()}")
973
+ elif isinstance(e, Node):
974
+ parts.append(e.to_unicode_str())
975
+ else:
976
+ parts.append(str(e))
977
+ return " " + ", ".join(parts)
978
+
979
+
980
+ def render_sequent_proof(derivation: "Derivation", indent: int = 0) -> str:
981
+ """Render a derivation as an indented proof tree (conclusion first, premises below).
982
+
983
+ Each line is ``Γ ⊢ Δ [rule extra]``; a rule's premises are nested one level
984
+ deeper. (The visual sequent-calculus convention draws premises *above* the
985
+ conclusion; this indented form is the same tree, easier to read in a terminal.)
986
+ """
987
+ pad = " " * indent
988
+ label = f" [{derivation.rule}{_fmt_extra(derivation.extra)}]"
989
+ lines = [f"{pad}{derivation.conclusion}{label}"]
990
+ for premise in derivation.premises:
991
+ lines.append(render_sequent_proof(premise, indent + 1))
992
+ return "\n".join(lines)
993
+
994
+
995
+ # ---------------------------------------------------------------------------
996
+ # Rendering: self-contained HTML page (mirrors CCGDerivation.to_html's idiom)
997
+ # ---------------------------------------------------------------------------
998
+
999
+ _SEQUENT_CSS = """
1000
+ .scroll{overflow-x:auto;padding:26px 8px}
1001
+ .fig{width:max-content;min-width:100%;padding:0 28px;
1002
+ font-family:"Cambria Math","Times New Roman",Times,serif;font-size:14px}
1003
+ .nd{display:inline-flex;flex-direction:column;align-items:center;vertical-align:bottom}
1004
+ .pr{display:flex;align-items:flex-end;justify-content:center;gap:26px}
1005
+ .bar{position:relative;align-self:stretch;border-top:1.3px solid var(--bar);margin-top:4px}
1006
+ .lbl{position:absolute;left:100%;top:-3px;padding-left:6px;white-space:nowrap;text-align:left}
1007
+ .r{font-size:11px;font-style:italic;color:var(--accent)}
1008
+ .ex{font-size:9.5px;color:var(--muted)}
1009
+ .cn{padding-top:4px;text-align:center;white-space:nowrap;color:var(--ink)}
1010
+ .leaf{padding:0 4px}
1011
+ .leaflbl{margin-top:2px;text-align:center;white-space:nowrap}
1012
+ """
1013
+
1014
+
1015
+ def _rule_label_html(d: "Derivation") -> str:
1016
+ """The ``.r``/``.ex`` pair for one node: the rule name, plus its ``extra``
1017
+ annotation (instantiation term / eigenvariable / comprehension) beneath it,
1018
+ exactly as :func:`render_sequent_proof`'s bracketed ``[rule extra]`` label."""
1019
+ extra = _fmt_extra(d.extra).strip()
1020
+ ex_html = ('<span class="ex">%s</span>' % esc_html(extra)) if extra else ""
1021
+ return '<span class="r">%s</span>%s' % (esc_html(d.rule), ex_html)
1022
+
1023
+
1024
+ def _html_sequent(d: "Derivation") -> str:
1025
+ """Render one derivation node, recursively, mirroring
1026
+ ``CCGDerivation._node_html``'s ``.nd``/``.pr``/``.bar``/``.cn`` tree layout —
1027
+ generalised from CCG's binary ``forward``/``backward`` to ``d.premises``'
1028
+ arbitrary arity, which the flex-row-of-blocks pattern already tolerates.
1029
+
1030
+ A leaf (no premises, e.g. an ``Ax`` axiom) has no inference bar to attach
1031
+ the rule label to, so it is shown as the sequent with the label stacked
1032
+ below it instead of to a bar's right.
1033
+ """
1034
+ if not d.premises:
1035
+ return ('<div class="nd leaf"><div class="cn">%s</div>'
1036
+ '<div class="leaflbl">%s</div></div>'
1037
+ % (esc_html(str(d.conclusion)), _rule_label_html(d)))
1038
+ prem = "".join(_html_sequent(p) for p in d.premises)
1039
+ return (
1040
+ '<div class="nd"><div class="pr">%s</div>'
1041
+ '<div class="bar"><span class="lbl">%s</span></div>'
1042
+ '<div class="cn">%s</div></div>'
1043
+ % (prem, _rule_label_html(d), esc_html(str(d.conclusion)))
1044
+ )
1045
+
1046
+
1047
+ def _html_sequent_page(d: "Derivation") -> str:
1048
+ """The ``<body>`` content for :meth:`Derivation.to_html`: the recursive tree
1049
+ wrapped in the same ``scroll``/``fig`` scaffolding CCG's page uses."""
1050
+ return '<div class="scroll"><div class="fig">%s</div></div>' % _html_sequent(d)