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,551 @@
|
|
|
1
|
+
"""Recursive backtracking proof *search* for Fitch natural deduction.
|
|
2
|
+
|
|
3
|
+
Where :mod:`unicode_logic_kit.atp.fitch` *checks* a hand-written proof, this module
|
|
4
|
+
*finds* one: a goal-directed, depth-bounded backtracking search that builds an
|
|
5
|
+
actual :class:`~unicode_logic_kit.atp.fitch.Proof` for the classical propositional
|
|
6
|
+
and first-order fragment.
|
|
7
|
+
|
|
8
|
+
**Soundness is free.** Whatever the search assembles is handed to ``check_proof``
|
|
9
|
+
before it is returned, so a bug in the search or the proof-assembly can only make
|
|
10
|
+
it *fail to find* a proof — never return an unsound one. Like
|
|
11
|
+
:func:`resolution.prove`, the search is sound and, under its depth bound,
|
|
12
|
+
*incomplete*: ``find_fitch_proof`` returning ``None`` means "no proof found within
|
|
13
|
+
the bound", never "not a theorem".
|
|
14
|
+
|
|
15
|
+
Strategy (classical natural deduction):
|
|
16
|
+
|
|
17
|
+
1. **Forward saturation** — cheap eliminations (∧E, →E, ↔E, ¬¬E, ⊥I, and bounded
|
|
18
|
+
∀E instantiation) are applied to a fixpoint, so anything directly derivable is
|
|
19
|
+
found before any branching.
|
|
20
|
+
2. **Goal-directed introduction** — for a goal with a main connective, the matching
|
|
21
|
+
introduction rule (∧I, →I via an assumption box, ¬I, ∨I, ↔I, ∀I via an
|
|
22
|
+
eigenvariable box, ∃I via a witness term).
|
|
23
|
+
3. **Elimination by case split** — ∨E over an available disjunction, ∃E over an
|
|
24
|
+
available existential (with a fresh eigenvariable).
|
|
25
|
+
4. **Reductio (RAA)** — assume ¬goal and derive ⊥; this is what makes the search
|
|
26
|
+
classically complete for the propositional fragment (it proves goals, such as
|
|
27
|
+
``P ∨ ¬P``, that no introduction rule reaches).
|
|
28
|
+
|
|
29
|
+
Public API: :func:`find_fitch_proof`, :func:`fitch_prove`, :func:`is_valid_fitch`.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from typing import Dict, List, Optional, Tuple
|
|
33
|
+
|
|
34
|
+
from ..fol.nodes import (
|
|
35
|
+
Node, Atom, Not, And, Or, Implies, Iff, Quantifier, Variable, Constant, Function,
|
|
36
|
+
)
|
|
37
|
+
from ..fol._msfl_nodes import key_text
|
|
38
|
+
from ..fol._truth_constants import is_true_constant
|
|
39
|
+
from .fitch import (
|
|
40
|
+
Proof, Subproof, Line, premise, assume, line, flag,
|
|
41
|
+
FALSUM, is_falsum, verify_proof, _canon_q, _subst_var, _free_vars, _q_kind, _is_term,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# A search result is an abstract derivation term — a tagged tuple describing HOW a
|
|
46
|
+
# formula is obtained, with no line numbers yet (the assembler adds those):
|
|
47
|
+
# ("ref", f) — cite an in-scope line holding f
|
|
48
|
+
# ("rule", tag, f, [deriv...], extra) — a flat rule (∧I/∧E/∨I/→E/↔E/¬E/⊥I/⊥E/∀E/∃I)
|
|
49
|
+
# ("impI", A, B, body) — →I: box [assume A ⊢ B]
|
|
50
|
+
# ("notI", A, body) — ¬I: box [assume A ⊢ ⊥]
|
|
51
|
+
# ("raa", goal, body) — RAA: box [assume ¬goal ⊢ ⊥]
|
|
52
|
+
# ("iffI", A, B, body1, body2) — ↔I: boxes [A⊢B], [B⊢A]
|
|
53
|
+
# ("orE", disj_deriv, A, B, body1, body2, goal) — ∨E
|
|
54
|
+
# ("allI", x, phi, e, body) — ∀I: flag box [e ⊢ phi[x:=e]]
|
|
55
|
+
# ("exE", ex_deriv, e, phi_e, body, goal) — ∃E: box [assume phi_e flag e ⊢ goal]
|
|
56
|
+
|
|
57
|
+
Deriv = tuple
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
# Terms and fresh variables
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
def _ground_terms(node: Node, acc: set) -> None:
|
|
65
|
+
"""Collect closed (variable-free) constant/function terms occurring in ``node``."""
|
|
66
|
+
if isinstance(node, (Constant,)):
|
|
67
|
+
acc.add(node)
|
|
68
|
+
elif isinstance(node, Function):
|
|
69
|
+
if not _free_vars(node):
|
|
70
|
+
acc.add(node)
|
|
71
|
+
for a in node.args:
|
|
72
|
+
_ground_terms(a, acc)
|
|
73
|
+
elif isinstance(node, Atom):
|
|
74
|
+
for a in node.args:
|
|
75
|
+
_ground_terms(a, acc)
|
|
76
|
+
else:
|
|
77
|
+
for child in node._child_nodes():
|
|
78
|
+
_ground_terms(child, acc)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _all_names(node: Node, acc: set) -> None:
|
|
82
|
+
"""Collect every variable name (free or bound) occurring in ``node``."""
|
|
83
|
+
if isinstance(node, Variable):
|
|
84
|
+
acc.add(node.name)
|
|
85
|
+
if isinstance(node, Quantifier):
|
|
86
|
+
acc.add(node.variable.name)
|
|
87
|
+
for child in node._child_nodes():
|
|
88
|
+
_all_names(child, acc)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class _Search:
|
|
92
|
+
"""Holds the immutable problem context (term pool, fresh-name source, budget)."""
|
|
93
|
+
|
|
94
|
+
def __init__(self, premises: List[Node], goal: Node, max_depth: int):
|
|
95
|
+
self.max_depth = max_depth
|
|
96
|
+
self.budget = [60000] # per-attempt step backstop
|
|
97
|
+
names: set = set()
|
|
98
|
+
terms: set = set()
|
|
99
|
+
for f in list(premises) + [goal]:
|
|
100
|
+
_all_names(f, names)
|
|
101
|
+
_ground_terms(f, terms)
|
|
102
|
+
if not terms:
|
|
103
|
+
terms.add(Constant("a")) # a witness/instantiation term must exist
|
|
104
|
+
self.used_names = names
|
|
105
|
+
# ordered by the bare names of the constants, so that the quotes of a quoted constant
|
|
106
|
+
# play no part in which term is tried first
|
|
107
|
+
self.base_terms = sorted(terms, key=key_text)
|
|
108
|
+
self._eigen = [0]
|
|
109
|
+
|
|
110
|
+
def fresh_var(self) -> Variable:
|
|
111
|
+
"""Return a globally fresh eigenvariable not occurring anywhere in the problem."""
|
|
112
|
+
while True:
|
|
113
|
+
name = f"_e{self._eigen[0]}"
|
|
114
|
+
self._eigen[0] += 1
|
|
115
|
+
if name not in self.used_names:
|
|
116
|
+
self.used_names.add(name)
|
|
117
|
+
return Variable(name)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# ---------------------------------------------------------------------------
|
|
121
|
+
# Forward saturation
|
|
122
|
+
# ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
def _saturate(derivs: Dict[Node, Deriv], terms: List[Node]) -> None:
|
|
125
|
+
"""Close ``derivs`` under cheap eliminations (mutates it in place)."""
|
|
126
|
+
pool = terms
|
|
127
|
+
changed = True
|
|
128
|
+
rounds = 0
|
|
129
|
+
while changed and rounds < 40:
|
|
130
|
+
changed = False
|
|
131
|
+
rounds += 1
|
|
132
|
+
for f in list(derivs):
|
|
133
|
+
if isinstance(f, And):
|
|
134
|
+
for part in (f.left, f.right):
|
|
135
|
+
if part not in derivs:
|
|
136
|
+
derivs[part] = ("rule", "∧E", part, [derivs[f]], ())
|
|
137
|
+
changed = True
|
|
138
|
+
elif isinstance(f, Not) and isinstance(f.formula, Not):
|
|
139
|
+
inner = f.formula.formula
|
|
140
|
+
if inner not in derivs:
|
|
141
|
+
derivs[inner] = ("rule", "¬E", inner, [derivs[f]], ())
|
|
142
|
+
changed = True
|
|
143
|
+
elif isinstance(f, Iff):
|
|
144
|
+
if f.left in derivs and f.right not in derivs:
|
|
145
|
+
derivs[f.right] = ("rule", "↔E", f.right, [derivs[f], derivs[f.left]], ())
|
|
146
|
+
changed = True
|
|
147
|
+
if f.right in derivs and f.left not in derivs:
|
|
148
|
+
derivs[f.left] = ("rule", "↔E", f.left, [derivs[f], derivs[f.right]], ())
|
|
149
|
+
changed = True
|
|
150
|
+
elif isinstance(f, Implies):
|
|
151
|
+
if f.left in derivs and f.right not in derivs:
|
|
152
|
+
derivs[f.right] = ("rule", "→E", f.right, [derivs[f], derivs[f.left]], ())
|
|
153
|
+
changed = True
|
|
154
|
+
elif _q_kind(f) == "∀":
|
|
155
|
+
for t in pool:
|
|
156
|
+
inst = _subst_var(f.formula, f.variable, t)
|
|
157
|
+
if inst not in derivs:
|
|
158
|
+
derivs[inst] = ("rule", "∀E", inst, [derivs[f]], (t,))
|
|
159
|
+
changed = True
|
|
160
|
+
if FALSUM not in derivs:
|
|
161
|
+
for f in list(derivs):
|
|
162
|
+
if Not(f) in derivs:
|
|
163
|
+
derivs[FALSUM] = ("rule", "⊥I", FALSUM, [derivs[f], derivs[Not(f)]], ())
|
|
164
|
+
changed = True
|
|
165
|
+
break
|
|
166
|
+
if FALSUM not in derivs:
|
|
167
|
+
# A fact that IS a falsum (the truth constant `$false`) puts a contradiction
|
|
168
|
+
# in scope on its own: ⊥E turns it into the search's own ⊥.
|
|
169
|
+
for f in list(derivs):
|
|
170
|
+
if is_falsum(f):
|
|
171
|
+
derivs[FALSUM] = ("rule", "⊥E", FALSUM, [derivs[f]], ())
|
|
172
|
+
changed = True
|
|
173
|
+
break
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _extend(derivs: Dict[Node, Deriv], formula: Node) -> Dict[Node, Deriv]:
|
|
177
|
+
"""Return a copy of ``derivs`` with ``formula`` added as a base (Ref) fact."""
|
|
178
|
+
new = dict(derivs)
|
|
179
|
+
if formula not in new:
|
|
180
|
+
new[formula] = ("ref", formula)
|
|
181
|
+
return new
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# ---------------------------------------------------------------------------
|
|
185
|
+
# The search
|
|
186
|
+
# ---------------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
def _search(derivs: Dict[Node, Deriv], goal: Node, depth: int,
|
|
189
|
+
ctx: "_Search", terms: List[Node]) -> Optional[Deriv]:
|
|
190
|
+
"""Find an abstract derivation of ``goal`` from ``derivs``, or None within bounds.
|
|
191
|
+
|
|
192
|
+
``terms`` is the instantiation/witness term set in scope on this branch — the
|
|
193
|
+
problem's ground terms plus the eigenvariables of enclosing ∀I / ∃E boxes. It is
|
|
194
|
+
threaded (not global) so the universal-instantiation pool stays bounded per path.
|
|
195
|
+
"""
|
|
196
|
+
if ctx.budget[0] <= 0:
|
|
197
|
+
return None
|
|
198
|
+
ctx.budget[0] -= 1
|
|
199
|
+
|
|
200
|
+
derivs = dict(derivs)
|
|
201
|
+
_saturate(derivs, terms)
|
|
202
|
+
if goal in derivs:
|
|
203
|
+
return derivs[goal]
|
|
204
|
+
if is_true_constant(goal):
|
|
205
|
+
return ("rule", "⊤I", goal, [], ()) # the truth constant needs no premise
|
|
206
|
+
# Ex falso quodlibet: once a contradiction is in scope, any goal follows (the
|
|
207
|
+
# search's own ⊥ is the goal it is looking for; the truth constant `$false` is an
|
|
208
|
+
# ordinary goal that ⊥E reaches like any other).
|
|
209
|
+
if FALSUM in derivs and goal != FALSUM:
|
|
210
|
+
return ("rule", "⊥E", goal, [derivs[FALSUM]], ())
|
|
211
|
+
if depth <= 0:
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
found = _try_intro(derivs, goal, depth, ctx, terms)
|
|
215
|
+
if found is not None:
|
|
216
|
+
return found
|
|
217
|
+
|
|
218
|
+
# Elimination by case split on an available disjunction.
|
|
219
|
+
for f in list(derivs):
|
|
220
|
+
if isinstance(f, Or):
|
|
221
|
+
d1 = _search(_extend(derivs, f.left), goal, depth - 1, ctx, terms)
|
|
222
|
+
if d1 is None:
|
|
223
|
+
continue
|
|
224
|
+
d2 = _search(_extend(derivs, f.right), goal, depth - 1, ctx, terms)
|
|
225
|
+
if d2 is None:
|
|
226
|
+
continue
|
|
227
|
+
return ("orE", derivs[f], f.left, f.right, d1, d2, goal)
|
|
228
|
+
|
|
229
|
+
# ∃-elimination over an available existential (fresh eigenvariable).
|
|
230
|
+
for f in list(derivs):
|
|
231
|
+
if _q_kind(f) == "∃":
|
|
232
|
+
e = ctx.fresh_var()
|
|
233
|
+
phi_e = _subst_var(f.formula, f.variable, e)
|
|
234
|
+
body = _search(_extend(derivs, phi_e), goal, depth - 1, ctx, terms + [e])
|
|
235
|
+
if body is not None and e not in _free_vars(goal) and e not in _free_vars(f):
|
|
236
|
+
return ("exE", derivs[f], e, phi_e, body, goal)
|
|
237
|
+
|
|
238
|
+
# Backward chaining: prove ``goal`` via an available implication X → goal.
|
|
239
|
+
for f in list(derivs):
|
|
240
|
+
if isinstance(f, Implies) and f.right == goal and f.left != goal:
|
|
241
|
+
d = _search(derivs, f.left, depth - 1, ctx, terms)
|
|
242
|
+
if d is not None:
|
|
243
|
+
return ("rule", "→E", goal, [derivs[f], d], ())
|
|
244
|
+
|
|
245
|
+
# Reductio ad absurdum (classical completeness for the propositional fragment).
|
|
246
|
+
if goal != FALSUM:
|
|
247
|
+
body = _search(_extend(derivs, Not(goal)), FALSUM, depth - 1, ctx, terms)
|
|
248
|
+
if body is not None:
|
|
249
|
+
return ("raa", goal, body)
|
|
250
|
+
|
|
251
|
+
return None
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _try_intro(derivs: Dict[Node, Deriv], goal: Node, depth: int,
|
|
255
|
+
ctx: "_Search", terms: List[Node]) -> Optional[Deriv]:
|
|
256
|
+
"""Try the introduction rule for ``goal``'s main connective."""
|
|
257
|
+
if isinstance(goal, And):
|
|
258
|
+
d1 = _search(derivs, goal.left, depth - 1, ctx, terms)
|
|
259
|
+
if d1 is None:
|
|
260
|
+
return None
|
|
261
|
+
d2 = _search(derivs, goal.right, depth - 1, ctx, terms)
|
|
262
|
+
if d2 is None:
|
|
263
|
+
return None
|
|
264
|
+
return ("rule", "∧I", goal, [d1, d2], ())
|
|
265
|
+
|
|
266
|
+
if isinstance(goal, Implies):
|
|
267
|
+
body = _search(_extend(derivs, goal.left), goal.right, depth - 1, ctx, terms)
|
|
268
|
+
if body is not None:
|
|
269
|
+
return ("impI", goal.left, goal.right, body)
|
|
270
|
+
return None
|
|
271
|
+
|
|
272
|
+
if isinstance(goal, Not):
|
|
273
|
+
body = _search(_extend(derivs, goal.formula), FALSUM, depth - 1, ctx, terms)
|
|
274
|
+
if body is not None:
|
|
275
|
+
return ("notI", goal.formula, body)
|
|
276
|
+
return None
|
|
277
|
+
|
|
278
|
+
if isinstance(goal, Iff):
|
|
279
|
+
d1 = _search(_extend(derivs, goal.left), goal.right, depth - 1, ctx, terms)
|
|
280
|
+
if d1 is None:
|
|
281
|
+
return None
|
|
282
|
+
d2 = _search(_extend(derivs, goal.right), goal.left, depth - 1, ctx, terms)
|
|
283
|
+
if d2 is None:
|
|
284
|
+
return None
|
|
285
|
+
return ("iffI", goal.left, goal.right, d1, d2)
|
|
286
|
+
|
|
287
|
+
if isinstance(goal, Or):
|
|
288
|
+
d = _search(derivs, goal.left, depth - 1, ctx, terms)
|
|
289
|
+
if d is not None:
|
|
290
|
+
return ("rule", "∨I", goal, [d], ())
|
|
291
|
+
d = _search(derivs, goal.right, depth - 1, ctx, terms)
|
|
292
|
+
if d is not None:
|
|
293
|
+
return ("rule", "∨I", goal, [d], ())
|
|
294
|
+
return None
|
|
295
|
+
|
|
296
|
+
if _q_kind(goal) == "∀":
|
|
297
|
+
e = ctx.fresh_var()
|
|
298
|
+
phi_e = _subst_var(goal.formula, goal.variable, e)
|
|
299
|
+
body = _search(derivs, phi_e, depth - 1, ctx, terms + [e])
|
|
300
|
+
if body is not None:
|
|
301
|
+
return ("allI", goal.variable, goal.formula, e, body)
|
|
302
|
+
return None
|
|
303
|
+
|
|
304
|
+
if _q_kind(goal) == "∃":
|
|
305
|
+
for t in terms:
|
|
306
|
+
witness = _subst_var(goal.formula, goal.variable, t)
|
|
307
|
+
d = _search(derivs, witness, depth - 1, ctx, terms)
|
|
308
|
+
if d is not None:
|
|
309
|
+
return ("rule", "∃I", goal, [d], (t,))
|
|
310
|
+
return None
|
|
311
|
+
|
|
312
|
+
if goal == FALSUM:
|
|
313
|
+
# Prove ⊥ from a fact f by proving its negation, or from ¬g by proving g.
|
|
314
|
+
for f in list(derivs):
|
|
315
|
+
if isinstance(f, Not):
|
|
316
|
+
proven = _search(derivs, f.formula, depth - 1, ctx, terms)
|
|
317
|
+
if proven is not None:
|
|
318
|
+
return ("rule", "⊥I", FALSUM, [proven, derivs[f]], ())
|
|
319
|
+
else:
|
|
320
|
+
neg = _search(derivs, Not(f), depth - 1, ctx, terms)
|
|
321
|
+
if neg is not None:
|
|
322
|
+
return ("rule", "⊥I", FALSUM, [derivs[f], neg], ())
|
|
323
|
+
return None
|
|
324
|
+
|
|
325
|
+
return None
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
# ---------------------------------------------------------------------------
|
|
329
|
+
# Assembly: abstract derivation -> a numbered Fitch Proof
|
|
330
|
+
# ---------------------------------------------------------------------------
|
|
331
|
+
|
|
332
|
+
class _Asm:
|
|
333
|
+
"""Mutable assembly context: a shared line counter + the in-scope fact→line map."""
|
|
334
|
+
|
|
335
|
+
def __init__(self, counter: List[int], available: Dict[Node, int]):
|
|
336
|
+
self.counter = counter
|
|
337
|
+
self.available = available
|
|
338
|
+
self.out: List = []
|
|
339
|
+
|
|
340
|
+
def fresh(self) -> int:
|
|
341
|
+
self.counter[0] += 1
|
|
342
|
+
return self.counter[0]
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _emit(deriv: Deriv, asm: "_Asm") -> int:
|
|
346
|
+
"""Emit the lines for ``deriv`` into ``asm`` and return the line number of its result."""
|
|
347
|
+
tag = deriv[0]
|
|
348
|
+
|
|
349
|
+
if tag == "ref":
|
|
350
|
+
return asm.available[deriv[1]]
|
|
351
|
+
|
|
352
|
+
if tag == "rule":
|
|
353
|
+
_, rule, formula, subs, extra = deriv
|
|
354
|
+
if formula in asm.available:
|
|
355
|
+
return asm.available[formula]
|
|
356
|
+
cites = [_emit(s, asm) for s in subs]
|
|
357
|
+
num = asm.fresh()
|
|
358
|
+
asm.out.append(line(num, formula, rule, *cites, extra=extra))
|
|
359
|
+
asm.available[formula] = num
|
|
360
|
+
return num
|
|
361
|
+
|
|
362
|
+
if tag == "impI":
|
|
363
|
+
_, ante, cons, body = deriv
|
|
364
|
+
span = _emit_box(asm, ante, None, body, cons)
|
|
365
|
+
num = asm.fresh()
|
|
366
|
+
asm.out.append(line(num, Implies(ante, cons), "→I", span))
|
|
367
|
+
asm.available[Implies(ante, cons)] = num
|
|
368
|
+
return num
|
|
369
|
+
|
|
370
|
+
if tag == "notI":
|
|
371
|
+
_, phi, body = deriv
|
|
372
|
+
span = _emit_box(asm, phi, None, body, FALSUM)
|
|
373
|
+
num = asm.fresh()
|
|
374
|
+
asm.out.append(line(num, Not(phi), "¬I", span))
|
|
375
|
+
asm.available[Not(phi)] = num
|
|
376
|
+
return num
|
|
377
|
+
|
|
378
|
+
if tag == "raa":
|
|
379
|
+
_, goal, body = deriv
|
|
380
|
+
span = _emit_box(asm, Not(goal), None, body, FALSUM)
|
|
381
|
+
num = asm.fresh()
|
|
382
|
+
asm.out.append(line(num, goal, "RAA", span))
|
|
383
|
+
asm.available[goal] = num
|
|
384
|
+
return num
|
|
385
|
+
|
|
386
|
+
if tag == "iffI":
|
|
387
|
+
_, a, b, body1, body2 = deriv
|
|
388
|
+
span1 = _emit_box(asm, a, None, body1, b)
|
|
389
|
+
span2 = _emit_box(asm, b, None, body2, a)
|
|
390
|
+
num = asm.fresh()
|
|
391
|
+
asm.out.append(line(num, Iff(a, b), "↔I", span1, span2))
|
|
392
|
+
asm.available[Iff(a, b)] = num
|
|
393
|
+
return num
|
|
394
|
+
|
|
395
|
+
if tag == "orE":
|
|
396
|
+
_, disj_deriv, a, b, body1, body2, goal = deriv
|
|
397
|
+
disj_line = _emit(disj_deriv, asm)
|
|
398
|
+
span1 = _emit_box(asm, a, None, body1, goal)
|
|
399
|
+
span2 = _emit_box(asm, b, None, body2, goal)
|
|
400
|
+
num = asm.fresh()
|
|
401
|
+
asm.out.append(line(num, goal, "∨E", disj_line, span1, span2))
|
|
402
|
+
asm.available[goal] = num
|
|
403
|
+
return num
|
|
404
|
+
|
|
405
|
+
if tag == "allI":
|
|
406
|
+
_, x, phi, e, body = deriv
|
|
407
|
+
phi_e = _subst_var(phi, x, e)
|
|
408
|
+
span = _emit_box(asm, None, e, body, phi_e)
|
|
409
|
+
num = asm.fresh()
|
|
410
|
+
asm.out.append(line(num, Quantifier("∀", x, phi), "∀I", span))
|
|
411
|
+
asm.available[Quantifier("∀", x, phi)] = num
|
|
412
|
+
return num
|
|
413
|
+
|
|
414
|
+
if tag == "exE":
|
|
415
|
+
_, ex_deriv, e, phi_e, body, goal = deriv
|
|
416
|
+
ex_line = _emit(ex_deriv, asm)
|
|
417
|
+
span = _emit_box(asm, phi_e, e, body, goal)
|
|
418
|
+
num = asm.fresh()
|
|
419
|
+
asm.out.append(line(num, goal, "∃E", ex_line, span))
|
|
420
|
+
asm.available[goal] = num
|
|
421
|
+
return num
|
|
422
|
+
|
|
423
|
+
raise AssertionError(f"unknown derivation tag {tag!r}")
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def _emit_box(asm: "_Asm", assumption: Optional[Node], flag_var: Optional[Variable],
|
|
427
|
+
body: Deriv, body_goal: Node) -> Tuple[int, int]:
|
|
428
|
+
"""Emit a subproof box and append it to ``asm.out``; return its (start, end) span."""
|
|
429
|
+
child = _Asm(asm.counter, dict(asm.available))
|
|
430
|
+
head_num = child.fresh()
|
|
431
|
+
if assumption is None and flag_var is not None:
|
|
432
|
+
head = flag(head_num, flag_var) # ∀I: pure eigenvariable box
|
|
433
|
+
else:
|
|
434
|
+
head = assume(head_num, assumption) # →I/¬I/RAA/∨E/↔I (and ∃E, with a flag)
|
|
435
|
+
child.available[assumption] = head_num
|
|
436
|
+
|
|
437
|
+
result = _emit(body, child)
|
|
438
|
+
# The box's last line must be body_goal; if it is not, reiterate it.
|
|
439
|
+
if not (child.out and isinstance(child.out[-1], Line)
|
|
440
|
+
and child.out[-1].formula == body_goal):
|
|
441
|
+
rnum = child.fresh()
|
|
442
|
+
child.out.append(line(rnum, body_goal, "Reit", result))
|
|
443
|
+
|
|
444
|
+
end_num = child.counter[0]
|
|
445
|
+
box = Subproof(assumption=head, body=tuple(child.out), flag=flag_var)
|
|
446
|
+
asm.out.append(box)
|
|
447
|
+
return (head_num, end_num)
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def _assemble(premises: List[Node], goal: Node, deriv: Deriv) -> Proof:
|
|
451
|
+
"""Turn an abstract derivation into a numbered :class:`Proof`."""
|
|
452
|
+
premise_lines = [premise(i, f) for i, f in enumerate(premises, start=1)]
|
|
453
|
+
available: Dict[Node, int] = {}
|
|
454
|
+
for i, f in enumerate(premises, start=1):
|
|
455
|
+
available.setdefault(f, i)
|
|
456
|
+
counter = [len(premises)]
|
|
457
|
+
asm = _Asm(counter, available)
|
|
458
|
+
result = _emit(deriv, asm)
|
|
459
|
+
# Ensure the proof's last top-level line is the goal.
|
|
460
|
+
if not (asm.out and isinstance(asm.out[-1], Line) and asm.out[-1].formula == goal):
|
|
461
|
+
num = asm.fresh()
|
|
462
|
+
asm.out.append(line(num, goal, "Reit", result))
|
|
463
|
+
return Proof(premises=tuple(premise_lines), steps=tuple(asm.out), logic="fol")
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
# ---------------------------------------------------------------------------
|
|
467
|
+
# Public API
|
|
468
|
+
# ---------------------------------------------------------------------------
|
|
469
|
+
|
|
470
|
+
def find_fitch_proof(premises, conclusion: Node, max_depth: int = 8) -> Optional[Proof]:
|
|
471
|
+
"""Search for a Fitch proof of ``premises ⊢ conclusion``; return it or ``None``.
|
|
472
|
+
|
|
473
|
+
Goal-directed backtracking over the classical propositional + first-order rules.
|
|
474
|
+
The returned proof is re-validated by ``check_proof`` before it is handed back,
|
|
475
|
+
so the result is always a genuinely valid proof (or ``None``). ``None`` means "no
|
|
476
|
+
proof found within ``max_depth``", never "not a theorem" — the search is sound
|
|
477
|
+
but, under its depth bound, incomplete (first-order provability is undecidable).
|
|
478
|
+
|
|
479
|
+
``Contrast`` and ``Count`` are reduced to their classical readings up front
|
|
480
|
+
(∧ and the distinct-witnesses expansion), so the returned proof derives the
|
|
481
|
+
reduced formula. A MODAL input is rejected with a pointer — this search has
|
|
482
|
+
no modal rules, and silently treating a modal operator as an opaque atom
|
|
483
|
+
used to return a misleading ``None`` for valid entailments like
|
|
484
|
+
``□(P→Q), □P ⊢ □Q``; :func:`fitch_prove` / :func:`is_valid_fitch` instead
|
|
485
|
+
route modal input to the modal tableau automatically.
|
|
486
|
+
|
|
487
|
+
Raises:
|
|
488
|
+
NotImplementedError: on modal/temporal/hybrid/counterfactual input, and on
|
|
489
|
+
substructural (ILL/Lambek), team-semantic, second-order, or Łukasiewicz
|
|
490
|
+
input — each with a pointer at its own decision procedure (an opaque-atom
|
|
491
|
+
treatment would silently return None for genuine theorems like the
|
|
492
|
+
ILL axiom ``A ⊸ A``).
|
|
493
|
+
"""
|
|
494
|
+
from ..fol._msfl_nodes import _reduce_nl_nodes
|
|
495
|
+
from .modal_tableau import has_modal
|
|
496
|
+
from .tableau import _reject_exotic
|
|
497
|
+
_reject_exotic(list(premises) + [conclusion], "find_fitch_proof")
|
|
498
|
+
premises = [_reduce_nl_nodes(p) for p in premises]
|
|
499
|
+
conclusion = _reduce_nl_nodes(conclusion)
|
|
500
|
+
if any(has_modal(f) for f in premises + [conclusion]):
|
|
501
|
+
raise NotImplementedError(
|
|
502
|
+
"find_fitch_proof: no proof search over the modal natural-deduction "
|
|
503
|
+
"rules (a modal operator treated as an opaque atom would silently "
|
|
504
|
+
"yield None for valid modal entailments). Decide validity with "
|
|
505
|
+
"fitch_prove / is_valid_fitch (which route to the modal tableau), or "
|
|
506
|
+
"check a hand-written modal Fitch proof with check_proof.")
|
|
507
|
+
premises = [_canon_q(p) for p in premises]
|
|
508
|
+
goal = _canon_q(conclusion)
|
|
509
|
+
base = {p: ("ref", p) for p in premises}
|
|
510
|
+
# Iterative deepening: try increasing depth bounds so a shallow proof is found
|
|
511
|
+
# before the search explores deep, fruitless branches. Each level gets a fresh
|
|
512
|
+
# step budget and eigenvariable counter.
|
|
513
|
+
for depth in range(1, max_depth + 1):
|
|
514
|
+
ctx = _Search(premises, goal, depth)
|
|
515
|
+
deriv = _search(base, goal, depth, ctx, ctx.base_terms)
|
|
516
|
+
if deriv is not None:
|
|
517
|
+
proof = _assemble(premises, goal, deriv)
|
|
518
|
+
if verify_proof(proof).ok:
|
|
519
|
+
return proof
|
|
520
|
+
return None
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def fitch_prove(premises, conclusion: Node, max_depth: int = 8) -> bool:
|
|
524
|
+
"""Return True iff ``premises ⊢ conclusion`` is established (sound, bounded).
|
|
525
|
+
|
|
526
|
+
Classical input is decided by the Fitch proof search. MODAL input is routed
|
|
527
|
+
to the modal tableau's local consequence over the system **K**
|
|
528
|
+
(:func:`~unicode_logic_kit.atp.modal_tableau.modal_prove`) — previously a
|
|
529
|
+
modal operator was treated as an opaque atom and e.g. the K-axiom instance
|
|
530
|
+
``□(P→Q), □P ⊢ □Q`` silently came back False. Either path is sound: a True
|
|
531
|
+
is a verified Fitch proof or a closed modal tableau.
|
|
532
|
+
"""
|
|
533
|
+
from .modal_tableau import has_modal, modal_prove
|
|
534
|
+
from .tableau import _reject_exotic
|
|
535
|
+
premises = list(premises)
|
|
536
|
+
# Substructural / team / SO / fuzzy input must not degrade to "opaque atom →
|
|
537
|
+
# silent False" (the exact bug class the modal routing below already fixed:
|
|
538
|
+
# is_valid_fitch(A ⊸ A) returned False for a genuine ILL theorem).
|
|
539
|
+
_reject_exotic(premises + [conclusion], "fitch_prove")
|
|
540
|
+
if any(has_modal(f) for f in premises + [conclusion]):
|
|
541
|
+
return modal_prove(premises, conclusion)
|
|
542
|
+
return find_fitch_proof(premises, conclusion, max_depth) is not None
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def is_valid_fitch(formula: Node, max_depth: int = 8) -> bool:
|
|
546
|
+
"""Return True iff ``formula`` is provable from no premises (a found-theorem check).
|
|
547
|
+
|
|
548
|
+
Modal input is decided by the modal tableau over **K** (see
|
|
549
|
+
:func:`fitch_prove`); classical input by the Fitch search.
|
|
550
|
+
"""
|
|
551
|
+
return fitch_prove([], formula, max_depth)
|