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