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