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,543 @@
1
+ """Independent classical-tableau proof CHECKER (not a searcher).
2
+
3
+ Where :mod:`unicode_logic_kit.atp.tableau` SEARCHES for a closed tableau
4
+ (``prove_tableau`` / ``prove_tableau_detailed``), this module CERTIFIES one that
5
+ was produced elsewhere: given a :class:`~unicode_logic_kit.atp.tableau.TableauProof`
6
+ (the flat, node-ID-addressed rule-application tree ``prove_tableau_detailed``
7
+ records), :func:`check_tableau_proof` decides, independently, whether it
8
+ genuinely refutes ``premises ∪ {¬conclusion}`` — the same division of labour as
9
+ :mod:`atp.resolution_check` (resolution refutations) and :mod:`atp.twee_check`
10
+ (Twee proofs): a proof produced by a search procedure is only as trustworthy as
11
+ an INDEPENDENT re-check of it, so the tableau *search* is trusted only to
12
+ search, never for soundness.
13
+
14
+ Four things are checked, none of them by trusting a flag the search itself set:
15
+
16
+ 1. **Root** — ``proof.root_formulas`` must be EXACTLY ``premises`` followed by
17
+ ``¬conclusion``, structurally, in that order. This is the boundary that
18
+ catches a proof that is internally flawless but answers a different
19
+ question (e.g. the conclusion left un-negated). For MANY-SORTED input the
20
+ expected root is the one :func:`_expected_roots` derives: the guard image of
21
+ each of those formulas followed by the background facts of
22
+ :func:`~unicode_logic_kit.fol.nodes.sort_axioms` — computed here from the
23
+ premises and the conclusion, never read off the proof.
24
+ 2. **Every step is a genuine rule instance, RE-DERIVED from scratch** —
25
+ first, the step's ``principal_formula`` must GENUINELY BE ON THE BRANCH it
26
+ extends (a root formula, or produced by an ancestor step — for a β split,
27
+ on the branch down to the split node): without this membership check a
28
+ fabricated proof could "decompose" an invented contradiction unrelated to
29
+ the premises/conclusion, close on its components, and pass every shape
30
+ check (found by the Tier-3 adversarial review). Then
31
+ :func:`_expected_shape` reclassifies ``step.principal_formula`` by the same
32
+ α/β/γ/δ case split :mod:`atp.tableau`'s own ``_rule`` makes, but as an
33
+ independent re-implementation (never imported — see below): α must
34
+ decompose into exactly its declared components; β's two sibling steps (same
35
+ ``parent_id``, same ``principal_formula``, both ``branch_split=True``) must
36
+ be exactly its two alternatives, in either order; γ's ``produced``/``terms``
37
+ must be the matrix substituted by each declared term — the substitution is
38
+ RECOMPUTED here, not trusted; δ's ``produced`` must be the matrix
39
+ substituted by the declared ``fresh_constant``, AND no node earlier on the
40
+ same branch (nor of the root formulas) may carry that constant's NAME, as a
41
+ constant or as any other kind of symbol —
42
+ re-checked independently, since a reused "fresh" constant is the classical
43
+ soundness hole in this rule: it lets a witness for one existential silently
44
+ stand for an already-constrained individual, licensing an unsound proof.
45
+ 3. **Every closed branch's pair is really on that branch** — a
46
+ :class:`~atp.tableau.TableauClosure` names the two branch nodes where its
47
+ literal and complement supposedly occur; the checker walks the ACTUAL
48
+ parent-chain from the closure's leaf back to the root and verifies both
49
+ cited nodes are genuinely on it, and that the named formula really is in
50
+ that node's own ``produced`` (or, at the root, ``root_formulas``) — trusting
51
+ neither the node-ID citation nor the formula label without this.
52
+ Complementarity itself (``φ`` vs ``¬φ``, or a bare ⊥) is re-derived
53
+ structurally, not read off any flag on the closure.
54
+ 4. **Completeness** — every LEAF of the recorded tree (a node no step cites as
55
+ its ``parent_id``) must carry a closure. One open leaf means the tableau
56
+ never actually closed, no matter how many other branches did.
57
+
58
+ Independence, concretely (mirrors :mod:`atp.resolution_check`'s module
59
+ docstring): the α/β/γ/δ case split is RE-IMPLEMENTED from scratch here
60
+ (:func:`_expected_shape`) rather than importing :mod:`atp.tableau`'s ``_rule``
61
+ — a bug shared between the search's own rule dispatch and the checker's would
62
+ be invisible to both. Substitution reuses
63
+ :func:`unicode_logic_kit.fol.nodes.substitute` — the generic, capture-avoiding
64
+ MSFL-level substitution already used elsewhere in the kit for beta-reduction
65
+ and quantifier grounding — rather than :mod:`atp.tableau`'s own ``_subst_var``:
66
+ a DIFFERENT traversal of the same specification, which is exactly the
67
+ independence a checker needs (the same principle behind ``atp.twee_check``
68
+ reusing only the generic, direction-agnostic ``apply_subst`` while
69
+ re-implementing the direction-SENSITIVE rewrite matching itself). The one thing the
70
+ two share is the rule that names a binder renamed to avoid capture: the recorded
71
+ instance is compared with the recomputed one formula for formula, not up to the
72
+ spelling of bound variables, so the two have to spell a renamed binder alike. The rule
73
+ itself is the capture-avoidance both need, and the substitution lemma
74
+ (``tests/test_substitution_lemma.py``) is checked against a Tarski evaluator that
75
+ shares nothing with either.
76
+ Complementarity (``_is_complement``) and falsum recognition (``_is_falsum``)
77
+ are likewise re-implemented locally here, not imported from
78
+ ``atp.fitch``/``atp.tableau``.
79
+
80
+ Public API: :class:`TableauCheckError`, :func:`check_tableau_proof`,
81
+ :func:`check_entailment_tableau_detailed`.
82
+ """
83
+
84
+ import sys
85
+ from typing import Dict, List, Optional, Sequence, Tuple
86
+
87
+ from ..fol._identifiers import symbol_names
88
+ from ..fol.nodes import (
89
+ Node, Atom, Not, And, Or, Xor, Implies, Iff, Quantifier, Variable, Constant,
90
+ Contrast, Count, substitute,
91
+ SortedQuantifier, SortedConstant, SortedCount, SortedCardinality, to_fol, sort_axioms,
92
+ )
93
+ from .tableau import TableauProof, TableauStep, TableauClosure, prove_tableau_detailed
94
+
95
+ __all__ = ["TableauCheckError", "check_tableau_proof", "check_entailment_tableau_detailed"]
96
+
97
+
98
+ class TableauCheckError(ValueError):
99
+ """Raised by :func:`check_tableau_proof` when a proof does not check out."""
100
+
101
+
102
+ # ---------------------------------------------------------------------------
103
+ # Independent structural predicates — NOT imported from atp.tableau/atp.fitch
104
+ # (see module docstring: falsum recognition and complementarity are the
105
+ # kind of "small vocabulary" a shared bug could hide in, so they are
106
+ # re-implemented here from the reserved-symbol/shape definitions directly).
107
+ # ---------------------------------------------------------------------------
108
+
109
+ def _is_falsum(f: Node) -> bool:
110
+ """True iff ``f`` closes a branch on its own: the nullary falsum atom ``⊥`` or
111
+ ``$false``, or the negation of the nullary truth atom ``$true`` or ``⊤``
112
+ (re-derived here from the names, like the rest of this vocabulary).
113
+
114
+ The converse literals — ``$true`` / ``⊤`` and ``¬$false`` / ``¬⊥`` — close
115
+ nothing: they hold in every interpretation, so a closure that cites one of them
116
+ with no complement is rejected."""
117
+ if isinstance(f, Not):
118
+ inner = f.formula
119
+ return isinstance(inner, Atom) and inner.predicate in ("$true", "⊤") and not inner.args
120
+ return isinstance(f, Atom) and not f.args and f.predicate in ("⊥", "$false")
121
+
122
+
123
+ def _is_complement(a: Node, b: Node) -> bool:
124
+ """True iff ``a`` and ``b`` are ``φ`` / ``¬φ`` for some ``φ``, in either order."""
125
+ if isinstance(a, Not) and a.formula == b:
126
+ return True
127
+ if isinstance(b, Not) and b.formula == a:
128
+ return True
129
+ return False
130
+
131
+
132
+ def _is_forall(f: Node) -> bool:
133
+ """True iff ``f`` is an (unsorted) universal quantifier."""
134
+ return isinstance(f, Quantifier) and f.type in ("forall", "∀")
135
+
136
+
137
+ def _is_exists(f: Node) -> bool:
138
+ """True iff ``f`` is an (unsorted) existential quantifier."""
139
+ return isinstance(f, Quantifier) and f.type in ("exists", "∃")
140
+
141
+
142
+ # ---------------------------------------------------------------------------
143
+ # Independent rule re-derivation — see module docstring, independence
144
+ # requirement 1. A structural twin of atp.tableau._rule, written from
145
+ # scratch here rather than imported.
146
+ # ---------------------------------------------------------------------------
147
+
148
+ def _expected_shape(f: Node):
149
+ """Re-derive which tableau rule applies to ``f``, and its expansion.
150
+
151
+ Returns ``("alpha", [comp, ...])``, ``("beta", [[...], [...]])``,
152
+ ``("gamma", var, body, neg)``, or ``("delta", var, body, neg)`` — the same
153
+ shapes :mod:`atp.tableau`'s own (unimported) ``_rule`` computes, for the
154
+ same node families it handles. Raises :class:`TableauCheckError` if no
155
+ tableau rule applies to ``f`` (e.g. it is already a literal, or an
156
+ exotic/second-order node family a genuine proof could never have
157
+ decomposed this way).
158
+ """
159
+ if isinstance(f, And):
160
+ return ("alpha", [f.left, f.right])
161
+ if isinstance(f, Contrast):
162
+ return ("alpha", [f.left, f.right])
163
+ if isinstance(f, Or):
164
+ return ("beta", [[f.left], [f.right]])
165
+ if isinstance(f, Implies):
166
+ return ("beta", [[Not(f.left)], [f.right]])
167
+ if isinstance(f, Iff):
168
+ return ("beta", [[f.left, f.right], [Not(f.left), Not(f.right)]])
169
+ if isinstance(f, Xor):
170
+ return ("beta", [[f.left, Not(f.right)], [Not(f.left), f.right]])
171
+ if isinstance(f, Count):
172
+ return ("alpha", [f._expand()])
173
+ if _is_exists(f):
174
+ return ("delta", (f.variable, f.formula, False))
175
+ if _is_forall(f):
176
+ return ("gamma", (f.variable, f.formula, False))
177
+ if isinstance(f, Not):
178
+ g = f.formula
179
+ if isinstance(g, Not):
180
+ return ("alpha", [g.formula])
181
+ if isinstance(g, And):
182
+ return ("beta", [[Not(g.left)], [Not(g.right)]])
183
+ if isinstance(g, Contrast):
184
+ return ("beta", [[Not(g.left)], [Not(g.right)]])
185
+ if isinstance(g, Or):
186
+ return ("alpha", [Not(g.left), Not(g.right)])
187
+ if isinstance(g, Implies):
188
+ return ("alpha", [g.left, Not(g.right)])
189
+ if isinstance(g, Iff):
190
+ return ("beta", [[g.left, Not(g.right)], [Not(g.left), g.right]])
191
+ if isinstance(g, Xor):
192
+ return ("beta", [[g.left, g.right], [Not(g.left), Not(g.right)]])
193
+ if isinstance(g, Count):
194
+ return ("alpha", [Not(g._expand())])
195
+ if _is_forall(g):
196
+ return ("delta", (g.variable, g.formula, True))
197
+ if _is_exists(g):
198
+ return ("gamma", (g.variable, g.formula, True))
199
+ raise TableauCheckError(
200
+ f"no tableau rule applies to principal formula {f.to_unicode_str()!r} "
201
+ "(it is already a literal, or an exotic node family)")
202
+
203
+
204
+ def _expected_instance(var: Variable, body: Node, neg: bool, term: Node) -> Node:
205
+ """Independently recompute ``body[var := term]`` (negated when ``neg``).
206
+
207
+ Uses :func:`unicode_logic_kit.fol.nodes.substitute` — capture-avoiding, and a
208
+ DIFFERENT traversal from :mod:`atp.tableau`'s own ``_subst_var`` (see module
209
+ docstring, independence requirement 2), with which it shares only the name of a
210
+ renamed binder, so that an instance at a term whose free variable a binder of the
211
+ matrix would capture (``∀x1 P(x1)`` at ``x0 := x1``) is spelled the same way by both.
212
+ """
213
+ inst = substitute(body, var, term)
214
+ return Not(inst) if neg else inst
215
+
216
+
217
+ def _same_formula_list(a: Sequence[Node], b: Sequence[Node]) -> bool:
218
+ """True iff the two SEQUENCES are pairwise equal, position by position."""
219
+ return len(a) == len(b) and all(x == y for x, y in zip(a, b))
220
+
221
+
222
+ def _expected_roots(premises, conclusion: Node) -> Tuple[Node, ...]:
223
+ """The root formulas a proof of ``premises ⊨ conclusion`` must start from.
224
+
225
+ The premises followed by ``¬conclusion``. When any of them contains a many-sorted
226
+ node, the guard image (:func:`~unicode_logic_kit.fol.nodes.to_fol`) of each, followed
227
+ by :func:`~unicode_logic_kit.fol.nodes.sort_axioms` of all of them: every sort is
228
+ non-empty and a sorted constant lies in its sort. The axioms are roots of their own,
229
+ never part of the negated conclusion.
230
+ """
231
+ formulas = tuple(premises) + (Not(conclusion),)
232
+ sorted_nodes = (SortedQuantifier, SortedConstant, SortedCount, SortedCardinality)
233
+ if not any(isinstance(n, sorted_nodes) for f in formulas for n in f.walk()):
234
+ return formulas
235
+ return tuple(to_fol(f) for f in formulas) + tuple(sort_axioms(*formulas))
236
+
237
+
238
+ # ---------------------------------------------------------------------------
239
+ # Branch bookkeeping: a step's ancestor chain via its own recorded
240
+ # parent_id, and the formulas actually visible on a branch up to a node.
241
+ # ---------------------------------------------------------------------------
242
+
243
+ def _ancestor_chain(node_id: int, steps_by_id: Dict[int, TableauStep]) -> List[int]:
244
+ """Step IDs from ``node_id`` (inclusive, if nonzero) up to (excluding) the root."""
245
+ chain = []
246
+ cur = node_id
247
+ while cur != 0:
248
+ chain.append(cur)
249
+ cur = steps_by_id[cur].parent_id
250
+ return chain
251
+
252
+
253
+ def _formulas_up_to(node_id: int, steps_by_id: Dict[int, TableauStep],
254
+ root_formulas: Tuple[Node, ...]) -> List[Node]:
255
+ """All formulas on the branch from the root down to and including ``node_id``."""
256
+ chain = _ancestor_chain(node_id, steps_by_id)
257
+ formulas = list(root_formulas)
258
+ for sid in reversed(chain):
259
+ formulas.extend(steps_by_id[sid].produced)
260
+ return formulas
261
+
262
+
263
+ def _occurs_anywhere(term: Constant, formulas: Sequence[Node]) -> bool:
264
+ """True iff the name of the constant ``term`` is carried by any node of ``formulas``.
265
+
266
+ A witness is fresh only when nothing else of the branch has its spelling. That is
267
+ more than "the constant itself does not occur": a function symbol, a predicate, a
268
+ sort or a variable of the same name is a symbol the witness would be confused with
269
+ in any target that keeps one namespace, and a zero-argument function is the
270
+ constant of its name in every writer of the kit. So every name any node holds is
271
+ compared, whatever the node.
272
+ """
273
+ return term.name in symbol_names(*formulas)
274
+
275
+
276
+ # ---------------------------------------------------------------------------
277
+ # Per-step / per-split verification. Each returns None (licensed) or an
278
+ # error string (WITHOUT any "step N"/"node N" prefix — added by the caller).
279
+ # ---------------------------------------------------------------------------
280
+
281
+ def _verify_alpha_gamma_delta(step: TableauStep, expected_kind: str, payload,
282
+ steps_by_id: Dict[int, TableauStep],
283
+ root_formulas: Tuple[Node, ...]) -> Optional[str]:
284
+ """Check a non-branching step's ``produced``/``terms``/``fresh_constant``."""
285
+ if expected_kind == "alpha":
286
+ if not _same_formula_list(step.produced, payload):
287
+ return ("'alpha' step's produced formulas are not the principal "
288
+ "formula's own components")
289
+ return None
290
+ if expected_kind == "gamma":
291
+ var, body, neg = payload
292
+ if not step.terms or len(step.terms) != len(step.produced):
293
+ return "'gamma' step must record exactly one term per produced formula"
294
+ for p, t in zip(step.produced, step.terms):
295
+ if p != _expected_instance(var, body, neg, t):
296
+ return (f"'gamma' step's substitution does not check out: "
297
+ f"instantiating at {t.to_unicode_str()!r} does not "
298
+ f"produce {p.to_unicode_str()!r}")
299
+ return None
300
+ if expected_kind == "delta":
301
+ var, body, neg = payload
302
+ if len(step.produced) != 1:
303
+ return "'delta' step must produce exactly one formula"
304
+ c = step.fresh_constant
305
+ if not isinstance(c, Constant):
306
+ return "'delta' step must record its witnessing constant"
307
+ if step.produced[0] != _expected_instance(var, body, neg, c):
308
+ return (f"'delta' step's substitution does not check out: witnessing "
309
+ f"with {c.to_unicode_str()!r} does not produce "
310
+ f"{step.produced[0].to_unicode_str()!r}")
311
+ prior = _formulas_up_to(step.parent_id, steps_by_id, root_formulas)
312
+ if _occurs_anywhere(c, prior):
313
+ return (f"'delta' step's witnessing constant {c.to_unicode_str()!r} "
314
+ "already occurs earlier on this branch (or in the root "
315
+ "formulas) — it is not fresh")
316
+ return None
317
+ raise AssertionError(expected_kind)
318
+
319
+
320
+ def _verify_beta_pair(parent_id: int, children: List[TableauStep],
321
+ expected_kind: str, payload) -> Optional[str]:
322
+ """Check a beta split's two sibling steps against the rule's two alternatives."""
323
+ if expected_kind != "beta":
324
+ return (f"node {parent_id} has two children sharing a principal formula, "
325
+ f"but that formula is a {expected_kind!r}-rule formula, not a beta")
326
+ if len(children) != 2:
327
+ return f"node {parent_id}: a 'beta' split must have exactly 2 children, got {len(children)}"
328
+ a, b = children
329
+ if not (a.branch_split and b.branch_split):
330
+ return f"node {parent_id}: both children of a 'beta' split must have branch_split=True"
331
+ if a.principal_formula != b.principal_formula:
332
+ return f"node {parent_id}: the two 'beta' children cite different principal formulas"
333
+ alt1, alt2 = payload
334
+ if ((_same_formula_list(a.produced, alt1) and _same_formula_list(b.produced, alt2)) or
335
+ (_same_formula_list(a.produced, alt2) and _same_formula_list(b.produced, alt1))):
336
+ return None
337
+ return (f"node {parent_id}: the 'beta' children's produced formulas are not "
338
+ "the principal formula's two alternatives")
339
+
340
+
341
+ # ---------------------------------------------------------------------------
342
+ # The checker
343
+ # ---------------------------------------------------------------------------
344
+
345
+ def check_tableau_proof(proof: TableauProof, premises, conclusion: Node) -> None:
346
+ """Independently verify ``proof`` refutes ``premises ∪ {¬ conclusion}``.
347
+
348
+ Raises :class:`TableauCheckError` naming the first problem found (see the
349
+ module docstring for the four things checked). Returns ``None`` (no
350
+ exception) iff every check passes.
351
+
352
+ A proof of a formula nested deeper than the checker's recursive substitution can
353
+ follow within the interpreter's recursion limit is not certified: the call raises
354
+ :class:`TableauCheckError` that says so, never :class:`RecursionError`.
355
+ """
356
+ try:
357
+ _check_tableau_proof(proof, premises, conclusion)
358
+ except RecursionError:
359
+ raise TableauCheckError(
360
+ "the proof could not be checked: a formula of it is nested deeper than the "
361
+ "checker's recursive helpers can walk within the interpreter's recursion "
362
+ f"limit ({sys.getrecursionlimit()}), so it is not certified") from None
363
+
364
+
365
+ def _check_tableau_proof(proof: TableauProof, premises, conclusion: Node) -> None:
366
+ """The checks of :func:`check_tableau_proof`, without its guard against a deep formula."""
367
+ if not isinstance(proof, TableauProof):
368
+ raise TableauCheckError(f"expected a TableauProof, got {type(proof).__name__}")
369
+
370
+ expected_root = _expected_roots(premises, conclusion)
371
+ if not _same_formula_list(proof.root_formulas, expected_root):
372
+ raise TableauCheckError(
373
+ "root formulas are not exactly premises followed by the negated "
374
+ "conclusion (structurally; for many-sorted input their guard images "
375
+ "followed by the sort axioms) — got "
376
+ f"{[f.to_unicode_str() for f in proof.root_formulas]}, expected "
377
+ f"{[f.to_unicode_str() for f in expected_root]}")
378
+
379
+ # --- structural validity of every step, and the parent/child skeleton ---
380
+ steps_by_id: Dict[int, TableauStep] = {}
381
+ for pos, step in enumerate(proof.steps):
382
+ expected_id = pos + 1
383
+ if not isinstance(step, TableauStep):
384
+ raise TableauCheckError(f"step {expected_id}: expected a TableauStep, got {type(step).__name__}")
385
+ if step.step_id != expected_id:
386
+ raise TableauCheckError(
387
+ f"step {step.step_id}: step_id must equal its 1-based position "
388
+ f"in the derivation (expected {expected_id})")
389
+ if step.rule not in ("alpha", "beta", "gamma", "delta"):
390
+ raise TableauCheckError(f"step {step.step_id}: unknown rule {step.rule!r}")
391
+ if not (0 <= step.parent_id < step.step_id):
392
+ raise TableauCheckError(
393
+ f"step {step.step_id}: parent_id {step.parent_id} must be 0 (root) "
394
+ "or a strictly earlier step")
395
+ if step.rule == "beta" and not step.branch_split:
396
+ raise TableauCheckError(f"step {step.step_id}: a 'beta' step must have branch_split=True")
397
+ if step.rule != "beta" and step.branch_split:
398
+ raise TableauCheckError(f"step {step.step_id}: only a 'beta' step may have branch_split=True")
399
+ steps_by_id[step.step_id] = step
400
+
401
+ children_of: Dict[int, List[TableauStep]] = {}
402
+ for step in proof.steps:
403
+ children_of.setdefault(step.parent_id, []).append(step)
404
+
405
+ # --- re-derive and check every non-beta step's rule instance ---
406
+ for step in proof.steps:
407
+ if step.rule == "beta":
408
+ continue # verified once per SPLIT below, together with its sibling
409
+ # The principal formula must GENUINELY be on the branch being
410
+ # extended (root formulas + every ancestor's produced formulas) —
411
+ # without this, a fabricated proof could "decompose" an invented
412
+ # formula unrelated to the premises/conclusion, manufacture a
413
+ # trivial closure from its components, and pass every shape check
414
+ # (adversarial review, Tier 3: the checker certified exactly such
415
+ # a fabrication before this membership check existed).
416
+ on_branch = _formulas_up_to(step.parent_id, steps_by_id,
417
+ proof.root_formulas)
418
+ if not any(step.principal_formula == f for f in on_branch):
419
+ raise TableauCheckError(
420
+ f"step {step.step_id}: principal formula "
421
+ f"{step.principal_formula.to_unicode_str()!r} is not on the "
422
+ "branch it extends (neither a root formula nor produced by "
423
+ "an ancestor step)")
424
+ try:
425
+ expected_kind, payload = _expected_shape(step.principal_formula)
426
+ except TableauCheckError as exc:
427
+ raise TableauCheckError(f"step {step.step_id}: {exc}") from exc
428
+ if expected_kind != step.rule:
429
+ raise TableauCheckError(
430
+ f"step {step.step_id}: declared rule {step.rule!r} does not match "
431
+ f"the principal formula's actual shape ({expected_kind!r} expected)")
432
+ err = _verify_alpha_gamma_delta(step, expected_kind, payload, steps_by_id, proof.root_formulas)
433
+ if err is not None:
434
+ raise TableauCheckError(f"step {step.step_id}: {err}")
435
+
436
+ # --- check the branching shape at every node (0, 1, or exactly-2-as-beta children) ---
437
+ all_ids = {0} | set(steps_by_id)
438
+ for node_id in all_ids:
439
+ kids = children_of.get(node_id, [])
440
+ if len(kids) == 0:
441
+ continue
442
+ if len(kids) == 1:
443
+ child = kids[0]
444
+ if child.branch_split:
445
+ raise TableauCheckError(
446
+ f"node {node_id}: has a single child but that child has branch_split=True")
447
+ continue
448
+ principal = kids[0].principal_formula
449
+ # Same branch-membership requirement as for non-beta steps: the
450
+ # split's shared principal formula must live on the branch down to
451
+ # the common parent (the split node itself).
452
+ on_branch = _formulas_up_to(node_id, steps_by_id, proof.root_formulas)
453
+ if not any(principal == f for f in on_branch):
454
+ raise TableauCheckError(
455
+ f"node {node_id}: the 'beta' split's principal formula "
456
+ f"{principal.to_unicode_str()!r} is not on the branch it "
457
+ "splits (neither a root formula nor produced by an ancestor "
458
+ "step)")
459
+ try:
460
+ expected_kind, payload = _expected_shape(principal)
461
+ except TableauCheckError as exc:
462
+ raise TableauCheckError(f"node {node_id}: {exc}") from exc
463
+ err = _verify_beta_pair(node_id, kids, expected_kind, payload)
464
+ if err is not None:
465
+ raise TableauCheckError(err)
466
+
467
+ # --- closures: every cited node/formula pair must genuinely be on the branch ---
468
+ closures_by_leaf: Dict[int, List[TableauClosure]] = {}
469
+ for closure in proof.closures:
470
+ if closure.leaf_id not in all_ids:
471
+ raise TableauCheckError(f"closure cites unknown node {closure.leaf_id}")
472
+ if children_of.get(closure.leaf_id):
473
+ raise TableauCheckError(
474
+ f"closure cites node {closure.leaf_id}, which is not a leaf "
475
+ "(it has further steps)")
476
+ eligible = {0} | set(_ancestor_chain(closure.leaf_id, steps_by_id))
477
+
478
+ def _on_branch(formula: Node, step_id: int, label: str) -> Optional[str]:
479
+ if step_id not in eligible:
480
+ return (f"{label} cites node {step_id}, which is not on the "
481
+ f"branch ending at {closure.leaf_id}")
482
+ source = proof.root_formulas if step_id == 0 else steps_by_id[step_id].produced
483
+ if not any(formula == f for f in source):
484
+ return f"{label} formula does not actually occur at cited node {step_id}"
485
+ return None
486
+
487
+ err = _on_branch(closure.literal, closure.literal_step_id, "closure's literal")
488
+ if err is not None:
489
+ raise TableauCheckError(err)
490
+
491
+ if closure.complement is None:
492
+ if not _is_falsum(closure.literal):
493
+ raise TableauCheckError(
494
+ f"closure at node {closure.leaf_id} has no complement but its "
495
+ "literal is not ⊥, $false or ¬$true")
496
+ else:
497
+ if closure.complement_step_id is None:
498
+ raise TableauCheckError(
499
+ f"closure at node {closure.leaf_id} has a complement but no complement_step_id")
500
+ err = _on_branch(closure.complement, closure.complement_step_id, "closure's complement")
501
+ if err is not None:
502
+ raise TableauCheckError(err)
503
+ if not _is_complement(closure.literal, closure.complement):
504
+ raise TableauCheckError(
505
+ f"closure at node {closure.leaf_id}: "
506
+ f"{closure.literal.to_unicode_str()!r} and "
507
+ f"{closure.complement.to_unicode_str()!r} are not complementary")
508
+ closures_by_leaf.setdefault(closure.leaf_id, []).append(closure)
509
+
510
+ # --- completeness: every leaf must be closed ---
511
+ for node_id in all_ids:
512
+ if children_of.get(node_id):
513
+ continue # not a leaf
514
+ if not closures_by_leaf.get(node_id):
515
+ raise TableauCheckError(f"branch ending at node {node_id} has no recorded closure (open branch)")
516
+
517
+
518
+ def check_entailment_tableau_detailed(premises, conclusion: Node, max_steps: int = 20000,
519
+ max_terms: int = 8, timeout: Optional[int] = None) -> dict:
520
+ """Search AND independently check a tableau proof of ``premises ⊨ conclusion``.
521
+
522
+ Runs :func:`unicode_logic_kit.atp.tableau.prove_tableau_detailed` (``timeout`` is its
523
+ wall-clock bound in milliseconds, default none), then — iff
524
+ it found a closed tableau — :func:`check_tableau_proof`. Returns a dict:
525
+
526
+ * ``proved``: True iff a closed tableau was found within the budget (the
527
+ raw search verdict — mirrors ``prove_tableau``'s own bool, so this is
528
+ never a claim of invalidity when False; see that function's docstring).
529
+ * ``proof``: the :class:`~atp.tableau.TableauProof`, or ``None``.
530
+ * ``check_passed``: True/False iff a proof was found and independently
531
+ (re-)verified; ``None`` if no proof was found (nothing to check).
532
+ * ``check_error``: the :class:`TableauCheckError` message when
533
+ ``check_passed`` is False, else ``None``.
534
+ """
535
+ proof = prove_tableau_detailed(premises, conclusion, max_steps=max_steps, max_terms=max_terms,
536
+ timeout=timeout)
537
+ if proof is None:
538
+ return {"proved": False, "proof": None, "check_passed": None, "check_error": None}
539
+ try:
540
+ check_tableau_proof(proof, premises, conclusion)
541
+ except TableauCheckError as exc:
542
+ return {"proved": True, "proof": proof, "check_passed": False, "check_error": str(exc)}
543
+ return {"proved": True, "proof": proof, "check_passed": True, "check_error": None}