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