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,436 @@
|
|
|
1
|
+
"""The Lambek calculus **L** — a complete, terminating decision procedure + a checker.
|
|
2
|
+
|
|
3
|
+
The Lambek calculus (Lambek 1958) is the sequent calculus of **ordered** resources:
|
|
4
|
+
on top of dropping weakening and contraction (as linear logic does) it drops
|
|
5
|
+
**exchange**, so a sequent's antecedent is a *sequence* of formulas, not a multiset,
|
|
6
|
+
and ``A, A\\B ⊢ B`` is derivable while ``A\\B, A ⊢ B`` is not. Antecedents must be
|
|
7
|
+
**nonempty** (Lambek's restriction — the version relevant to categorial grammar).
|
|
8
|
+
The connectives are the ``lambek`` parser mode's
|
|
9
|
+
(:mod:`unicode_logic_kit.fol._lambek_nodes`): ``•`` / ``\\`` / ``/``.
|
|
10
|
+
|
|
11
|
+
Rules (cut-free; ``Γ``, ``Δ`` range over sequences, ``Γ`` nonempty where stated)::
|
|
12
|
+
|
|
13
|
+
Ax: A ⊢ A
|
|
14
|
+
•L: Γ, A, B, Δ ⊢ C ⇒ Γ, A•B, Δ ⊢ C
|
|
15
|
+
•R: Γ ⊢ A and Δ ⊢ B ⇒ Γ, Δ ⊢ A•B (contiguous split, in order)
|
|
16
|
+
\\L: Γ ⊢ A and Δ1, B, Δ2 ⊢ C ⇒ Δ1, Γ, A\\B, Δ2 ⊢ C (Γ nonempty)
|
|
17
|
+
\\R: A, Γ ⊢ B ⇒ Γ ⊢ A\\B (Γ nonempty; A prepended)
|
|
18
|
+
/L: Γ ⊢ A and Δ1, B, Δ2 ⊢ C ⇒ Δ1, B/A, Γ, Δ2 ⊢ C (Γ nonempty)
|
|
19
|
+
/R: Γ, A ⊢ B ⇒ Γ ⊢ B/A (Γ nonempty; A appended)
|
|
20
|
+
|
|
21
|
+
**Decidability.** Every rule's premises have strictly smaller total size (node
|
|
22
|
+
count) than its conclusion, and there are finitely many rule instances per sequent,
|
|
23
|
+
so the backward search below — exhaustive, memoised on ``(sequence, goal)`` — is a
|
|
24
|
+
**complete, terminating decision procedure**: L is decidable, and
|
|
25
|
+
``lambek_prove(...) is None`` *proves* underivability. No depth bound is needed.
|
|
26
|
+
|
|
27
|
+
Deliberately, there is **no classical export**: collapsing ``A\\B`` and ``B/A`` to
|
|
28
|
+
``A → B`` forgets word order, which is the calculus's entire point. (The collapse is
|
|
29
|
+
*sound* — every L theorem collapses to a classical one, which the test-suite checks
|
|
30
|
+
against Z3 — but it identifies types L keeps apart.)
|
|
31
|
+
|
|
32
|
+
**What is read.** The calculus reads exactly the node classes it has rules for: ``•``
|
|
33
|
+
(``Product``), ``\\`` (``Under``) and ``/`` (``Over``), over atoms. An atom over terms
|
|
34
|
+
(``NP(john)``) is ONE category, identified by its predicate and its terms as written. Every
|
|
35
|
+
other node is refused by name with ``NotImplementedError``: a quantifier (sorted or not), a
|
|
36
|
+
counting or cardinality node, a sorted constant, an equality atom, and a node of another logic
|
|
37
|
+
(``And``, ``Or``, ``Not``, ``Box``, a connective of linear logic, ...), because reading it as one
|
|
38
|
+
more category would answer about another formula (``∀x P(x) ⊢ P(alpha)`` holds in every
|
|
39
|
+
first-order reading and has no derivation between the two categories; ``And(A, B) ⊢ A`` holds
|
|
40
|
+
classically and has none between ``And(A, B)`` and ``A``). Decide such input with a route of its
|
|
41
|
+
own logic.
|
|
42
|
+
|
|
43
|
+
Public API: :class:`LambekSequent`, :class:`LambekDerivation`, :func:`lambek_prove`,
|
|
44
|
+
:func:`lambek_derivable`, :func:`check_lambek_proof`, :func:`verify_lambek_proof`,
|
|
45
|
+
:func:`render_lambek_proof`.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
from dataclasses import dataclass
|
|
49
|
+
from typing import Callable, Dict, Iterable, List, Optional, Tuple
|
|
50
|
+
|
|
51
|
+
from ..fol.nodes import Node, Product, Under, Over
|
|
52
|
+
from ..fol._truth_constants import refuse_truth_constants
|
|
53
|
+
from ._substructural_input import LAMBEK, refuse_unreadable_input, unreadable_reason
|
|
54
|
+
from .sequent import SequentResult
|
|
55
|
+
|
|
56
|
+
#: Why the Lambek calculus has no reading of the TPTP constants ``$true`` /
|
|
57
|
+
#: ``$false`` (the reason :func:`lambek_prove` and the Isabelle export name when
|
|
58
|
+
#: they refuse one).
|
|
59
|
+
_NO_TRUTH_CONSTANT_WHY = (
|
|
60
|
+
"the Lambek calculus has no propositional constants: its formulas are "
|
|
61
|
+
"category types built from atoms by •, \\ and /, and no type is true or "
|
|
62
|
+
"false of every sequence")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
# Sequents and derivations
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True)
|
|
70
|
+
class LambekSequent:
|
|
71
|
+
"""A Lambek sequent ``antecedent ⊢ succedent``.
|
|
72
|
+
|
|
73
|
+
``antecedent`` is a **nonempty sequence** (tuple) of formulas — order matters,
|
|
74
|
+
there is no exchange; ``succedent`` is one formula. Frozen and hashable.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
antecedent: Tuple[Node, ...]
|
|
78
|
+
succedent: Node
|
|
79
|
+
|
|
80
|
+
def __post_init__(self):
|
|
81
|
+
"""Coerce the antecedent to a tuple so the sequent stays hashable."""
|
|
82
|
+
object.__setattr__(self, "antecedent", tuple(self.antecedent))
|
|
83
|
+
|
|
84
|
+
def __str__(self) -> str:
|
|
85
|
+
"""Render as ``A, B ⊢ C`` using each formula's Unicode form."""
|
|
86
|
+
left = ", ".join(f.to_unicode_str() for f in self.antecedent)
|
|
87
|
+
return f"{left} ⊢ {self.succedent.to_unicode_str()}"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass(frozen=True)
|
|
91
|
+
class LambekDerivation:
|
|
92
|
+
"""A node of a Lambek-calculus derivation tree.
|
|
93
|
+
|
|
94
|
+
``conclusion`` is the sequent this node derives; ``rule`` is the rule name
|
|
95
|
+
(``"Ax"``, ``"•R"``, ``"\\L"``, …); ``premises`` are the sub-derivations whose
|
|
96
|
+
conclusions are this rule's premises, **in the rule's written order** (for the
|
|
97
|
+
two-premise ``\\L`` / ``/L`` the argument premise ``Γ ⊢ A`` comes first; for
|
|
98
|
+
``•R`` the left part of the split comes first).
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
conclusion: LambekSequent
|
|
102
|
+
rule: str
|
|
103
|
+
premises: Tuple["LambekDerivation", ...] = ()
|
|
104
|
+
|
|
105
|
+
def __post_init__(self):
|
|
106
|
+
"""Coerce ``premises`` to a tuple so the derivation stays hashable."""
|
|
107
|
+
object.__setattr__(self, "premises", tuple(self.premises))
|
|
108
|
+
|
|
109
|
+
def render(self) -> str:
|
|
110
|
+
"""Render this derivation as an indented proof tree."""
|
|
111
|
+
return render_lambek_proof(self)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def render_lambek_proof(derivation: "LambekDerivation", indent: int = 0) -> str:
|
|
115
|
+
"""Render a derivation as an indented tree (conclusion first, premises below)."""
|
|
116
|
+
pad = " " * indent
|
|
117
|
+
lines = [f"{pad}{derivation.conclusion} [{derivation.rule}]"]
|
|
118
|
+
for premise in derivation.premises:
|
|
119
|
+
lines.append(render_lambek_proof(premise, indent + 1))
|
|
120
|
+
return "\n".join(lines)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _size(f: Node) -> int:
|
|
124
|
+
"""Return the node count of a formula (atomic categories count 1)."""
|
|
125
|
+
return 1 + sum(_size(c) for c in f._child_nodes())
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# ---------------------------------------------------------------------------
|
|
129
|
+
# Backward proof search (a decision procedure — see the module docstring)
|
|
130
|
+
# ---------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
def _prove(seq: Tuple[Node, ...], goal: Node,
|
|
133
|
+
memo: Dict) -> Optional[LambekDerivation]:
|
|
134
|
+
"""Exhaustive backward search for a derivation of ``seq ⊢ goal``.
|
|
135
|
+
|
|
136
|
+
Terminates because every premise's total node count is strictly smaller than
|
|
137
|
+
its conclusion's; memoised on ``(seq, goal)`` (both successes and failures —
|
|
138
|
+
sound here precisely because the search is unbounded and exhaustive).
|
|
139
|
+
"""
|
|
140
|
+
key = (seq, goal)
|
|
141
|
+
if key in memo:
|
|
142
|
+
return memo[key]
|
|
143
|
+
memo[key] = None # pre-set: harmless (no rule can revisit an equal sequent)
|
|
144
|
+
|
|
145
|
+
result = None
|
|
146
|
+
|
|
147
|
+
# Ax
|
|
148
|
+
if len(seq) == 1 and seq[0] == goal:
|
|
149
|
+
result = LambekDerivation(LambekSequent(seq, goal), "Ax")
|
|
150
|
+
|
|
151
|
+
# Right rules on the goal's main connective.
|
|
152
|
+
if result is None and isinstance(goal, Product):
|
|
153
|
+
for i in range(1, len(seq)): # both parts nonempty
|
|
154
|
+
p1 = _prove(seq[:i], goal.left, memo)
|
|
155
|
+
if p1 is None:
|
|
156
|
+
continue
|
|
157
|
+
p2 = _prove(seq[i:], goal.right, memo)
|
|
158
|
+
if p2 is not None:
|
|
159
|
+
result = LambekDerivation(LambekSequent(seq, goal), "•R", (p1, p2))
|
|
160
|
+
break
|
|
161
|
+
if result is None and isinstance(goal, Under):
|
|
162
|
+
# goal = A\B: prepend A on the LEFT.
|
|
163
|
+
p = _prove((goal.left,) + seq, goal.right, memo)
|
|
164
|
+
if p is not None:
|
|
165
|
+
result = LambekDerivation(LambekSequent(seq, goal), "\\R", (p,))
|
|
166
|
+
if result is None and isinstance(goal, Over):
|
|
167
|
+
# goal = B/A: append A on the RIGHT (fields: left=B, right=A).
|
|
168
|
+
p = _prove(seq + (goal.right,), goal.left, memo)
|
|
169
|
+
if p is not None:
|
|
170
|
+
result = LambekDerivation(LambekSequent(seq, goal), "/R", (p,))
|
|
171
|
+
|
|
172
|
+
# Left rules on each antecedent position.
|
|
173
|
+
if result is None:
|
|
174
|
+
for i, f in enumerate(seq):
|
|
175
|
+
if isinstance(f, Product):
|
|
176
|
+
p = _prove(seq[:i] + (f.left, f.right) + seq[i + 1:], goal, memo)
|
|
177
|
+
if p is not None:
|
|
178
|
+
result = LambekDerivation(LambekSequent(seq, goal), "•L", (p,))
|
|
179
|
+
break
|
|
180
|
+
elif isinstance(f, Under):
|
|
181
|
+
# f = A\B at position i: a nonempty Γ ending just before i derives A.
|
|
182
|
+
for j in range(i - 1, -1, -1):
|
|
183
|
+
p_arg = _prove(seq[j:i], f.left, memo)
|
|
184
|
+
if p_arg is None:
|
|
185
|
+
continue
|
|
186
|
+
p_main = _prove(seq[:j] + (f.right,) + seq[i + 1:], goal, memo)
|
|
187
|
+
if p_main is not None:
|
|
188
|
+
result = LambekDerivation(LambekSequent(seq, goal), "\\L",
|
|
189
|
+
(p_arg, p_main))
|
|
190
|
+
break
|
|
191
|
+
if result is not None:
|
|
192
|
+
break
|
|
193
|
+
elif isinstance(f, Over):
|
|
194
|
+
# f = B/A at position i: a nonempty Γ starting just after i derives A.
|
|
195
|
+
for j in range(i + 2, len(seq) + 1):
|
|
196
|
+
p_arg = _prove(seq[i + 1:j], f.right, memo)
|
|
197
|
+
if p_arg is None:
|
|
198
|
+
continue
|
|
199
|
+
p_main = _prove(seq[:i] + (f.left,) + seq[j:], goal, memo)
|
|
200
|
+
if p_main is not None:
|
|
201
|
+
result = LambekDerivation(LambekSequent(seq, goal), "/L",
|
|
202
|
+
(p_arg, p_main))
|
|
203
|
+
break
|
|
204
|
+
if result is not None:
|
|
205
|
+
break
|
|
206
|
+
|
|
207
|
+
memo[key] = result
|
|
208
|
+
return result
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def lambek_prove(sequence: Iterable[Node], goal: Node) -> Optional[LambekDerivation]:
|
|
212
|
+
"""Decide ``sequence ⊢ goal`` in the Lambek calculus L; return a derivation or None.
|
|
213
|
+
|
|
214
|
+
This is a genuine **decision procedure** (see the module docstring): a
|
|
215
|
+
``None`` *proves* the sequent underivable in L. The returned derivation is
|
|
216
|
+
re-validated by :func:`check_lambek_proof` before it is handed back, so a
|
|
217
|
+
search bug can only lose proofs, never invent them.
|
|
218
|
+
|
|
219
|
+
The sequent is read through the connectives ``• \\ /`` and atoms: an atom over terms is one
|
|
220
|
+
category, and every other node (a quantifier, a counting or cardinality node, a sorted
|
|
221
|
+
constant, an equality atom, a connective of another logic) is refused by name (see the
|
|
222
|
+
module docstring).
|
|
223
|
+
|
|
224
|
+
Raises:
|
|
225
|
+
ValueError: ``sequence`` is empty — L requires nonempty antecedents (Lambek's
|
|
226
|
+
restriction).
|
|
227
|
+
NotImplementedError: a truth constant ``$true`` / ``$false``, or a node L has no rule
|
|
228
|
+
for (a quantifier, a counting or cardinality node, a sorted constant, an
|
|
229
|
+
equality atom, a connective of another logic) occurs in the sequent; the message
|
|
230
|
+
names the node and what to use instead.
|
|
231
|
+
"""
|
|
232
|
+
seq = tuple(sequence)
|
|
233
|
+
if not seq:
|
|
234
|
+
raise ValueError(
|
|
235
|
+
"the Lambek calculus requires a nonempty antecedent sequence "
|
|
236
|
+
"(Lambek's restriction)")
|
|
237
|
+
refuse_truth_constants([*seq, goal], "lambek_prove", _NO_TRUTH_CONSTANT_WHY)
|
|
238
|
+
refuse_unreadable_input([*seq, goal], "lambek_prove", LAMBEK)
|
|
239
|
+
derivation = _prove(seq, goal, {})
|
|
240
|
+
if derivation is not None and not check_lambek_proof(derivation):
|
|
241
|
+
raise RuntimeError(
|
|
242
|
+
"internal error: the Lambek search assembled a derivation its own "
|
|
243
|
+
"checker rejects — please report this")
|
|
244
|
+
return derivation
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def lambek_derivable(sequence: Iterable[Node], goal: Node) -> bool:
|
|
248
|
+
"""Return True iff ``sequence ⊢ goal`` is derivable in L (a decision procedure).
|
|
249
|
+
|
|
250
|
+
Refuses an empty sequence and every node L has no rule for, as :func:`lambek_prove` does.
|
|
251
|
+
"""
|
|
252
|
+
return lambek_prove(sequence, goal) is not None
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
# ---------------------------------------------------------------------------
|
|
256
|
+
# The checker
|
|
257
|
+
# ---------------------------------------------------------------------------
|
|
258
|
+
#
|
|
259
|
+
# Each rule checker has signature (conclusion: LambekSequent, premises:
|
|
260
|
+
# List[LambekSequent]) and returns None when the step is licensed, or an error
|
|
261
|
+
# string. Antecedents are compared as SEQUENCES — order is the whole point.
|
|
262
|
+
|
|
263
|
+
def _r_ax(concl, prems):
|
|
264
|
+
"""Ax: ``A ⊢ A`` — a single antecedent formula equal to the succedent."""
|
|
265
|
+
if prems:
|
|
266
|
+
return "Ax takes no premises"
|
|
267
|
+
if len(concl.antecedent) != 1 or concl.antecedent[0] != concl.succedent:
|
|
268
|
+
return "Ax: the antecedent must be exactly the succedent formula"
|
|
269
|
+
return None
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _r_prod_l(concl, prems):
|
|
273
|
+
"""•L: from ``Γ, A, B, Δ ⊢ C`` infer ``Γ, A•B, Δ ⊢ C``."""
|
|
274
|
+
if len(prems) != 1:
|
|
275
|
+
return "•L has one premise"
|
|
276
|
+
ant, p = concl.antecedent, prems[0]
|
|
277
|
+
for i, f in enumerate(ant):
|
|
278
|
+
if isinstance(f, Product):
|
|
279
|
+
want = ant[:i] + (f.left, f.right) + ant[i + 1:]
|
|
280
|
+
if p.succedent == concl.succedent and p.antecedent == want:
|
|
281
|
+
return None
|
|
282
|
+
return "•L: needs A•B in the antecedent; the premise unfolds it in place"
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _r_prod_r(concl, prems):
|
|
286
|
+
"""•R: from ``Γ ⊢ A`` and ``Δ ⊢ B`` infer ``Γ, Δ ⊢ A•B`` (in order, both nonempty)."""
|
|
287
|
+
if len(prems) != 2:
|
|
288
|
+
return "•R has two premises"
|
|
289
|
+
g = concl.succedent
|
|
290
|
+
if not isinstance(g, Product):
|
|
291
|
+
return "•R must conclude a product A•B"
|
|
292
|
+
p1, p2 = prems
|
|
293
|
+
if not p1.antecedent or not p2.antecedent:
|
|
294
|
+
return "•R: both parts of the split must be nonempty"
|
|
295
|
+
if (p1.succedent == g.left and p2.succedent == g.right
|
|
296
|
+
and p1.antecedent + p2.antecedent == concl.antecedent):
|
|
297
|
+
return None
|
|
298
|
+
return "•R: premises Γ⊢A then Δ⊢B with Γ,Δ the conclusion's antecedent in order"
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _r_under_l(concl, prems):
|
|
302
|
+
"""\\L: from ``Γ ⊢ A`` and ``Δ1, B, Δ2 ⊢ C`` infer ``Δ1, Γ, A\\B, Δ2 ⊢ C``."""
|
|
303
|
+
if len(prems) != 2:
|
|
304
|
+
return "\\L has two premises"
|
|
305
|
+
p_arg, p_main = prems
|
|
306
|
+
if not p_arg.antecedent:
|
|
307
|
+
return "\\L: the argument premise's antecedent Γ must be nonempty"
|
|
308
|
+
if p_main.succedent != concl.succedent:
|
|
309
|
+
return "\\L: the main premise must share the conclusion's succedent"
|
|
310
|
+
ant, g = concl.antecedent, p_arg.antecedent
|
|
311
|
+
for i, f in enumerate(ant):
|
|
312
|
+
if not isinstance(f, Under) or f.left != p_arg.succedent:
|
|
313
|
+
continue
|
|
314
|
+
# Γ must sit immediately LEFT of the A\B occurrence.
|
|
315
|
+
if i - len(g) < 0 or ant[i - len(g):i] != g:
|
|
316
|
+
continue
|
|
317
|
+
d1, d2 = ant[:i - len(g)], ant[i + 1:]
|
|
318
|
+
if p_main.antecedent == d1 + (f.right,) + d2:
|
|
319
|
+
return None
|
|
320
|
+
return "\\L: needs Δ1, Γ, A\\B, Δ2 with Γ ⊢ A and Δ1, B, Δ2 ⊢ C"
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _r_under_r(concl, prems):
|
|
324
|
+
"""\\R: from ``A, Γ ⊢ B`` infer ``Γ ⊢ A\\B`` (Γ nonempty)."""
|
|
325
|
+
if len(prems) != 1:
|
|
326
|
+
return "\\R has one premise"
|
|
327
|
+
g = concl.succedent
|
|
328
|
+
if not isinstance(g, Under):
|
|
329
|
+
return "\\R must conclude A\\B"
|
|
330
|
+
if not concl.antecedent:
|
|
331
|
+
return "\\R: the conclusion's antecedent Γ must be nonempty"
|
|
332
|
+
p = prems[0]
|
|
333
|
+
if p.succedent == g.right and p.antecedent == (g.left,) + concl.antecedent:
|
|
334
|
+
return None
|
|
335
|
+
return "\\R: the premise must be A, Γ ⊢ B (A prepended on the LEFT)"
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _r_over_l(concl, prems):
|
|
339
|
+
"""/L: from ``Γ ⊢ A`` and ``Δ1, B, Δ2 ⊢ C`` infer ``Δ1, B/A, Γ, Δ2 ⊢ C``."""
|
|
340
|
+
if len(prems) != 2:
|
|
341
|
+
return "/L has two premises"
|
|
342
|
+
p_arg, p_main = prems
|
|
343
|
+
if not p_arg.antecedent:
|
|
344
|
+
return "/L: the argument premise's antecedent Γ must be nonempty"
|
|
345
|
+
if p_main.succedent != concl.succedent:
|
|
346
|
+
return "/L: the main premise must share the conclusion's succedent"
|
|
347
|
+
ant, g = concl.antecedent, p_arg.antecedent
|
|
348
|
+
for i, f in enumerate(ant):
|
|
349
|
+
if not isinstance(f, Over) or f.right != p_arg.succedent:
|
|
350
|
+
continue
|
|
351
|
+
# Γ must sit immediately RIGHT of the B/A occurrence.
|
|
352
|
+
if ant[i + 1:i + 1 + len(g)] != g:
|
|
353
|
+
continue
|
|
354
|
+
d1, d2 = ant[:i], ant[i + 1 + len(g):]
|
|
355
|
+
if p_main.antecedent == d1 + (f.left,) + d2:
|
|
356
|
+
return None
|
|
357
|
+
return "/L: needs Δ1, B/A, Γ, Δ2 with Γ ⊢ A and Δ1, B, Δ2 ⊢ C"
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _r_over_r(concl, prems):
|
|
361
|
+
"""/R: from ``Γ, A ⊢ B`` infer ``Γ ⊢ B/A`` (Γ nonempty)."""
|
|
362
|
+
if len(prems) != 1:
|
|
363
|
+
return "/R has one premise"
|
|
364
|
+
g = concl.succedent
|
|
365
|
+
if not isinstance(g, Over):
|
|
366
|
+
return "/R must conclude B/A"
|
|
367
|
+
if not concl.antecedent:
|
|
368
|
+
return "/R: the conclusion's antecedent Γ must be nonempty"
|
|
369
|
+
p = prems[0]
|
|
370
|
+
if p.succedent == g.left and p.antecedent == concl.antecedent + (g.right,):
|
|
371
|
+
return None
|
|
372
|
+
return "/R: the premise must be Γ, A ⊢ B (A appended on the RIGHT)"
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
_LAMBEK_RULES: Dict[str, Callable] = {
|
|
376
|
+
"Ax": _r_ax,
|
|
377
|
+
"•L": _r_prod_l, "•R": _r_prod_r,
|
|
378
|
+
"\\L": _r_under_l, "\\R": _r_under_r,
|
|
379
|
+
"/L": _r_over_l, "/R": _r_over_r,
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _verify(deriv: "LambekDerivation"):
|
|
384
|
+
"""Recursive worker: return ``(rule, error)`` or ``(None, None)`` on success."""
|
|
385
|
+
if not isinstance(deriv, LambekDerivation):
|
|
386
|
+
return ("?", f"expected a LambekDerivation, got {type(deriv).__name__}")
|
|
387
|
+
if not deriv.conclusion.antecedent:
|
|
388
|
+
return (deriv.rule, "Lambek sequents require a nonempty antecedent "
|
|
389
|
+
"(Lambek's restriction)")
|
|
390
|
+
for child in deriv.premises:
|
|
391
|
+
rule, err = _verify(child)
|
|
392
|
+
if err is not None:
|
|
393
|
+
return rule, err
|
|
394
|
+
fn = _LAMBEK_RULES.get(deriv.rule)
|
|
395
|
+
if fn is None:
|
|
396
|
+
return deriv.rule, f"unknown Lambek rule {deriv.rule!r}"
|
|
397
|
+
err = fn(deriv.conclusion, [c.conclusion for c in deriv.premises])
|
|
398
|
+
if err is not None:
|
|
399
|
+
return deriv.rule, err if err.startswith(deriv.rule) else f"{deriv.rule}: {err}"
|
|
400
|
+
return None, None
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _formulas_of(derivation: "LambekDerivation") -> List[Node]:
|
|
404
|
+
"""Every formula of every sequent of a derivation tree."""
|
|
405
|
+
formulas: List[Node] = []
|
|
406
|
+
pending = [derivation]
|
|
407
|
+
while pending:
|
|
408
|
+
node = pending.pop()
|
|
409
|
+
if isinstance(node, LambekDerivation) and isinstance(node.conclusion, LambekSequent):
|
|
410
|
+
formulas.extend(node.conclusion.antecedent)
|
|
411
|
+
formulas.append(node.conclusion.succedent)
|
|
412
|
+
pending.extend(node.premises)
|
|
413
|
+
return formulas
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def verify_lambek_proof(derivation: "LambekDerivation") -> SequentResult:
|
|
417
|
+
"""Check a Lambek derivation and return a
|
|
418
|
+
:class:`~unicode_logic_kit.atp.sequent.SequentResult`.
|
|
419
|
+
|
|
420
|
+
Recursively re-validates that every node's conclusion follows from its
|
|
421
|
+
premises' conclusions by the node's rule — antecedents compared as **ordered
|
|
422
|
+
sequences** and required nonempty throughout — returning the end-sequent and,
|
|
423
|
+
on failure, the first offending rule and reason. A derivation whose sequents hold a
|
|
424
|
+
node L has no rule for (a quantifier, an equality atom, a connective of another logic,
|
|
425
|
+
...) is not a derivation of L: it does not check, and the reason names the node
|
|
426
|
+
(``error_rule`` is ``"formula"``).
|
|
427
|
+
"""
|
|
428
|
+
unreadable = unreadable_reason(_formulas_of(derivation), "verify_lambek_proof", LAMBEK)
|
|
429
|
+
err_rule, err = ("formula", unreadable) if unreadable is not None else _verify(derivation)
|
|
430
|
+
end = derivation.conclusion if isinstance(derivation, LambekDerivation) else None
|
|
431
|
+
return SequentResult(err is None, end, err_rule, err)
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def check_lambek_proof(derivation: "LambekDerivation") -> bool:
|
|
435
|
+
"""Return True iff ``derivation`` is a valid cut-free L derivation (sound)."""
|
|
436
|
+
return verify_lambek_proof(derivation).ok
|