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.
- unicode_logic_kit/__init__.py +385 -0
- unicode_logic_kit/__main__.py +520 -0
- unicode_logic_kit/_deadline.py +219 -0
- unicode_logic_kit/ace/__init__.py +126 -0
- unicode_logic_kit/ace/_align.py +135 -0
- unicode_logic_kit/ace/chem_lexicon.py +128 -0
- unicode_logic_kit/ace/drs_reader.py +570 -0
- unicode_logic_kit/ace/mapping.py +666 -0
- unicode_logic_kit/ace/reverse_modal.py +138 -0
- unicode_logic_kit/ace/runner.py +551 -0
- unicode_logic_kit/ace/translate.py +452 -0
- unicode_logic_kit/ace/verbalize.py +1070 -0
- unicode_logic_kit/api.py +1284 -0
- unicode_logic_kit/atp/__init__.py +177 -0
- unicode_logic_kit/atp/_ascii_names.py +113 -0
- unicode_logic_kit/atp/_html.py +72 -0
- unicode_logic_kit/atp/_substructural_input.py +228 -0
- unicode_logic_kit/atp/_tff_problem.py +715 -0
- unicode_logic_kit/atp/_tptp_problem.py +1111 -0
- unicode_logic_kit/atp/_writer_support.py +289 -0
- unicode_logic_kit/atp/clingo_backend.py +1180 -0
- unicode_logic_kit/atp/cvc5_backend.py +1385 -0
- unicode_logic_kit/atp/eprover_backend.py +732 -0
- unicode_logic_kit/atp/finite_domain.py +1055 -0
- unicode_logic_kit/atp/fitch.py +1547 -0
- unicode_logic_kit/atp/fitch_search.py +551 -0
- unicode_logic_kit/atp/hets_backend.py +339 -0
- unicode_logic_kit/atp/hybrid_down.py +120 -0
- unicode_logic_kit/atp/incremental.py +250 -0
- unicode_logic_kit/atp/kripke_enum.py +741 -0
- unicode_logic_kit/atp/lambek.py +436 -0
- unicode_logic_kit/atp/leo3_backend.py +332 -0
- unicode_logic_kit/atp/linear.py +738 -0
- unicode_logic_kit/atp/lj.py +705 -0
- unicode_logic_kit/atp/logic_backends.py +566 -0
- unicode_logic_kit/atp/ltl_tableau.py +1084 -0
- unicode_logic_kit/atp/minizinc_backend.py +1402 -0
- unicode_logic_kit/atp/modal_tableau.py +1382 -0
- unicode_logic_kit/atp/nanocop_backend.py +410 -0
- unicode_logic_kit/atp/portfolio.py +489 -0
- unicode_logic_kit/atp/protocol.py +1803 -0
- unicode_logic_kit/atp/prover9_entailment.py +1153 -0
- unicode_logic_kit/atp/resolution.py +1376 -0
- unicode_logic_kit/atp/resolution_check.py +1114 -0
- unicode_logic_kit/atp/sequent.py +1050 -0
- unicode_logic_kit/atp/tableau.py +921 -0
- unicode_logic_kit/atp/tableau_check.py +543 -0
- unicode_logic_kit/atp/tptp_ncl.py +811 -0
- unicode_logic_kit/atp/tptp_tff.py +1546 -0
- unicode_logic_kit/atp/tstp.py +1333 -0
- unicode_logic_kit/atp/tstp_check.py +1096 -0
- unicode_logic_kit/atp/twee_backend.py +236 -0
- unicode_logic_kit/atp/twee_check.py +711 -0
- unicode_logic_kit/atp/twee_entailment.py +953 -0
- unicode_logic_kit/atp/vampire_entailment.py +540 -0
- unicode_logic_kit/atp/z3_arith.py +470 -0
- unicode_logic_kit/atp/z3_equivalence.py +36 -0
- unicode_logic_kit/atp/z3_fuzzy.py +362 -0
- unicode_logic_kit/atp/z3_input.py +500 -0
- unicode_logic_kit/atp/z3_models.py +208 -0
- unicode_logic_kit/chem/__init__.py +88 -0
- unicode_logic_kit/chem/_naming.py +284 -0
- unicode_logic_kit/chem/cache.py +185 -0
- unicode_logic_kit/chem/interop.py +244 -0
- unicode_logic_kit/chem/mol.py +525 -0
- unicode_logic_kit/chem/signature.py +112 -0
- unicode_logic_kit/comorphism.py +497 -0
- unicode_logic_kit/dl/__init__.py +384 -0
- unicode_logic_kit/dl/classification.py +227 -0
- unicode_logic_kit/dl/concepts.py +632 -0
- unicode_logic_kit/dl/datatypes.py +818 -0
- unicode_logic_kit/dl/owl_functional.py +2433 -0
- unicode_logic_kit/dl/owl_manchester.py +1637 -0
- unicode_logic_kit/dl/owl_reasoner.py +790 -0
- unicode_logic_kit/dl/parser.py +391 -0
- unicode_logic_kit/dl/tableau.py +4048 -0
- unicode_logic_kit/dl/translate.py +2704 -0
- unicode_logic_kit/drt/__init__.py +94 -0
- unicode_logic_kit/drt/export.py +179 -0
- unicode_logic_kit/drt/nodes.py +506 -0
- unicode_logic_kit/drt/parser.py +965 -0
- unicode_logic_kit/drt/resolve.py +195 -0
- unicode_logic_kit/drt/reverse.py +175 -0
- unicode_logic_kit/eval/__init__.py +106 -0
- unicode_logic_kit/eval/batch.py +382 -0
- unicode_logic_kit/eval/canonical.py +663 -0
- unicode_logic_kit/eval/chem_batch.py +606 -0
- unicode_logic_kit/eval/converses.py +200 -0
- unicode_logic_kit/eval/datasets/__init__.py +136 -0
- unicode_logic_kit/eval/datasets/_base.py +263 -0
- unicode_logic_kit/eval/datasets/_proofwriter_proof.py +422 -0
- unicode_logic_kit/eval/datasets/c3po.py +678 -0
- unicode_logic_kit/eval/datasets/folio.py +158 -0
- unicode_logic_kit/eval/datasets/fracas.py +418 -0
- unicode_logic_kit/eval/datasets/groves.py +191 -0
- unicode_logic_kit/eval/datasets/logicbench.py +467 -0
- unicode_logic_kit/eval/datasets/logicnli.py +303 -0
- unicode_logic_kit/eval/datasets/malls.py +133 -0
- unicode_logic_kit/eval/datasets/pfolio.py +594 -0
- unicode_logic_kit/eval/datasets/pmb.py +242 -0
- unicode_logic_kit/eval/datasets/prontoqa.py +611 -0
- unicode_logic_kit/eval/datasets/proofwriter.py +1431 -0
- unicode_logic_kit/eval/datasets/proverqa.py +674 -0
- unicode_logic_kit/eval/datasets/willow.py +478 -0
- unicode_logic_kit/eval/equivalence.py +466 -0
- unicode_logic_kit/eval/exercise_gen.py +533 -0
- unicode_logic_kit/eval/explain.py +791 -0
- unicode_logic_kit/eval/generality.py +750 -0
- unicode_logic_kit/eval/metric_hf.py +458 -0
- unicode_logic_kit/eval/predicate_match.py +343 -0
- unicode_logic_kit/eval/theory_check.py +1170 -0
- unicode_logic_kit/eval/validate.py +306 -0
- unicode_logic_kit/fol/__init__.py +177 -0
- unicode_logic_kit/fol/_atom_keys.py +510 -0
- unicode_logic_kit/fol/_fol_nodes.py +3586 -0
- unicode_logic_kit/fol/_free_parameters.py +105 -0
- unicode_logic_kit/fol/_ho_nodes.py +448 -0
- unicode_logic_kit/fol/_hybrid_nodes.py +308 -0
- unicode_logic_kit/fol/_identifiers.py +1091 -0
- unicode_logic_kit/fol/_lambek_nodes.py +112 -0
- unicode_logic_kit/fol/_linear_nodes.py +352 -0
- unicode_logic_kit/fol/_modal_nodes.py +1467 -0
- unicode_logic_kit/fol/_msfl_nodes.py +2196 -0
- unicode_logic_kit/fol/_numeral_symbols.py +231 -0
- unicode_logic_kit/fol/_so_nodes.py +200 -0
- unicode_logic_kit/fol/_symbol_names.py +81 -0
- unicode_logic_kit/fol/_team_nodes.py +181 -0
- unicode_logic_kit/fol/_tptp_symbols.py +551 -0
- unicode_logic_kit/fol/_truth_constants.py +117 -0
- unicode_logic_kit/fol/casl_export.py +1135 -0
- unicode_logic_kit/fol/casl_import.py +929 -0
- unicode_logic_kit/fol/derivation.py +367 -0
- unicode_logic_kit/fol/dialect_detect.py +70 -0
- unicode_logic_kit/fol/dialect_repair.py +537 -0
- unicode_logic_kit/fol/frames.py +637 -0
- unicode_logic_kit/fol/grammars/terminals.lark +31 -0
- unicode_logic_kit/fol/lambda_tools.py +297 -0
- unicode_logic_kit/fol/latex_input.py +429 -0
- unicode_logic_kit/fol/modal_translation.py +944 -0
- unicode_logic_kit/fol/msflparser.py +1033 -0
- unicode_logic_kit/fol/naming.py +422 -0
- unicode_logic_kit/fol/nodes.py +241 -0
- unicode_logic_kit/fol/normalforms.py +492 -0
- unicode_logic_kit/fol/pal.py +287 -0
- unicode_logic_kit/fol/prolog_export.py +566 -0
- unicode_logic_kit/fol/prolog_input.py +505 -0
- unicode_logic_kit/fol/prover9_input.py +1325 -0
- unicode_logic_kit/fol/qml.py +1760 -0
- unicode_logic_kit/fol/qmltp_input.py +525 -0
- unicode_logic_kit/fol/sanitize.py +221 -0
- unicode_logic_kit/fol/serialize.py +79 -0
- unicode_logic_kit/fol/signature.py +1290 -0
- unicode_logic_kit/fol/simplify_check.py +544 -0
- unicode_logic_kit/fol/spans.py +594 -0
- unicode_logic_kit/fol/tptp_input.py +1503 -0
- unicode_logic_kit/fol/tptp_repair.py +941 -0
- unicode_logic_kit/fol/unification.py +157 -0
- unicode_logic_kit/fol/verbalize.py +263 -0
- unicode_logic_kit/hets/__init__.py +163 -0
- unicode_logic_kit/hets/bridge.py +142 -0
- unicode_logic_kit/hets/client.py +748 -0
- unicode_logic_kit/hets/docker.py +420 -0
- unicode_logic_kit/hets/dol.py +712 -0
- unicode_logic_kit/hets/haskell_json.py +355 -0
- unicode_logic_kit/hets/owl_backend.py +794 -0
- unicode_logic_kit/hets/owl_cli.py +598 -0
- unicode_logic_kit/hets/symbols.py +512 -0
- unicode_logic_kit/hol/__init__.py +140 -0
- unicode_logic_kit/hol/_ho_common.py +323 -0
- unicode_logic_kit/hol/_isabelle_binders.py +125 -0
- unicode_logic_kit/hol/classical.py +812 -0
- unicode_logic_kit/hol/deepshallow/__init__.py +45 -0
- unicode_logic_kit/hol/deepshallow/_common.py +177 -0
- unicode_logic_kit/hol/deepshallow/conditional.py +225 -0
- unicode_logic_kit/hol/deepshallow/intuitionistic.py +181 -0
- unicode_logic_kit/hol/deepshallow/modal.py +217 -0
- unicode_logic_kit/hol/deepshallow/qml.py +406 -0
- unicode_logic_kit/hol/deepshallow/relevant.py +206 -0
- unicode_logic_kit/hol/free.py +753 -0
- unicode_logic_kit/hol/goedel.py +336 -0
- unicode_logic_kit/hol/ho_modal.py +1743 -0
- unicode_logic_kit/hol/intuitionistic.py +403 -0
- unicode_logic_kit/hol/isabelle_conditional.py +593 -0
- unicode_logic_kit/hol/isabelle_modal.py +1908 -0
- unicode_logic_kit/hol/isabelle_relevant.py +412 -0
- unicode_logic_kit/hol/isabelle_runner.py +1147 -0
- unicode_logic_kit/hol/isabelle_substructural.py +884 -0
- unicode_logic_kit/hol/lean.py +1018 -0
- unicode_logic_kit/hol/manyvalued.py +921 -0
- unicode_logic_kit/hol/secondorder.py +687 -0
- unicode_logic_kit/hol/thf_modal.py +941 -0
- unicode_logic_kit/hol/thirdorder.py +397 -0
- unicode_logic_kit/ilp/__init__.py +89 -0
- unicode_logic_kit/ilp/readback.py +389 -0
- unicode_logic_kit/ilp/separation.py +153 -0
- unicode_logic_kit/ilp/task.py +730 -0
- unicode_logic_kit/logic.py +163 -0
- unicode_logic_kit/mcp/__init__.py +28 -0
- unicode_logic_kit/mcp/__main__.py +5 -0
- unicode_logic_kit/mcp/chem_tools.py +1031 -0
- unicode_logic_kit/mcp/server.py +2453 -0
- unicode_logic_kit/mcp/syntax_spec.py +681 -0
- unicode_logic_kit/prob/__init__.py +53 -0
- unicode_logic_kit/prob/_bdd.py +225 -0
- unicode_logic_kit/prob/_column_gen.py +668 -0
- unicode_logic_kit/prob/distribution.py +686 -0
- unicode_logic_kit/prob/nilsson.py +470 -0
- unicode_logic_kit/py.typed +0 -0
- unicode_logic_kit/semantics/__init__.py +137 -0
- unicode_logic_kit/semantics/_modal_reject.py +156 -0
- unicode_logic_kit/semantics/action_models.py +466 -0
- unicode_logic_kit/semantics/asp_models.py +1200 -0
- unicode_logic_kit/semantics/conditional.py +580 -0
- unicode_logic_kit/semantics/dynamic_epistemic.py +95 -0
- unicode_logic_kit/semantics/free_logic.py +913 -0
- unicode_logic_kit/semantics/fuzzy.py +384 -0
- unicode_logic_kit/semantics/fuzzy_kripke.py +442 -0
- unicode_logic_kit/semantics/intuitionistic.py +581 -0
- unicode_logic_kit/semantics/kripke.py +1139 -0
- unicode_logic_kit/semantics/manyvalued.py +580 -0
- unicode_logic_kit/semantics/matrix.py +342 -0
- unicode_logic_kit/semantics/model_eval.py +1135 -0
- unicode_logic_kit/semantics/modelfinder.py +1036 -0
- unicode_logic_kit/semantics/nonmonotonic.py +372 -0
- unicode_logic_kit/semantics/relevant.py +331 -0
- unicode_logic_kit/semantics/secondorder.py +657 -0
- unicode_logic_kit/semantics/structures.py +352 -0
- unicode_logic_kit/semantics/tarski.py +975 -0
- unicode_logic_kit/semantics/team.py +315 -0
- unicode_logic_kit/semantics/team_translation.py +416 -0
- unicode_logic_kit/semantics/thirdorder.py +358 -0
- unicode_logic_kit/semantics/tnorm.py +85 -0
- unicode_logic_kit/semantics/truthtable.py +201 -0
- unicode_logic_kit-0.31.0.dist-info/METADATA +333 -0
- unicode_logic_kit-0.31.0.dist-info/RECORD +237 -0
- unicode_logic_kit-0.31.0.dist-info/WHEEL +4 -0
- 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}
|