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,1147 @@
|
|
|
1
|
+
r"""Run a **local Isabelle** to actually discharge the emitted shallow-embedding
|
|
2
|
+
theories — turning the :mod:`unicode_logic_kit.hol` exporters from *emit* into
|
|
3
|
+
*proven / refuted*.
|
|
4
|
+
|
|
5
|
+
The rest of the toolkit only *emits* HOL/Isabelle/THF problem files (see the
|
|
6
|
+
``hol`` package docstring): it is honest that first-order modal logic, FOL and
|
|
7
|
+
SOL are undecidable, so an emission is "a sound problem a prover *may* discharge".
|
|
8
|
+
This module is the **opt-in** other half: if a local Isabelle/HOL is installed, it
|
|
9
|
+
writes the emitted theory to a scratch session, invokes ``isabelle build``, and
|
|
10
|
+
reads the verdict off the build — a real proof, not a promise.
|
|
11
|
+
|
|
12
|
+
Decision procedure for modal validity (:func:`isabelle_decide_modal`)
|
|
13
|
+
---------------------------------------------------------------------
|
|
14
|
+
The shallow embedding is *faithful* to the Kripke evaluator
|
|
15
|
+
:func:`unicode_logic_kit.semantics.kripke.satisfies_modal`, so deciding the emitted
|
|
16
|
+
lemma decides the modal formula (for the chosen frame / domain regime):
|
|
17
|
+
|
|
18
|
+
1. **Prove** — emit ``lemma <goal>`` with a proof that brings the frame/domain
|
|
19
|
+
axioms into scope and tries a battery of methods
|
|
20
|
+
(``using <axioms> by (blast | force | fastforce | auto | meson … | metis …)``).
|
|
21
|
+
If ``isabelle build`` exits 0, the lemma is a **theorem** ⇒ the formula is
|
|
22
|
+
**VALID**.
|
|
23
|
+
2. **Refute** — otherwise emit the same lemma followed by
|
|
24
|
+
``nitpick[expect = genuine]``. Nitpick searches for a finite counter-model;
|
|
25
|
+
``expect = genuine`` makes the build **succeed iff a genuine counter-model is
|
|
26
|
+
found**, so exit 0 here ⇒ the formula is **INVALID**. The verdict is certified by
|
|
27
|
+
the exit code; for the propositional alethic fragment ``ModalVerdict.countermodel``
|
|
28
|
+
then carries a concrete Kripke counter-model reconstructed from the toolkit's own
|
|
29
|
+
evaluator (``isabelle build`` does not echo nitpick's model), and is ``None`` for
|
|
30
|
+
fragments the bounded search does not cover — never weakening the verdict.
|
|
31
|
+
3. Otherwise the result is **UNKNOWN** (neither a battery proof nor a finite
|
|
32
|
+
counter-model within the time/size budget — expected, since the logic is
|
|
33
|
+
undecidable).
|
|
34
|
+
|
|
35
|
+
This is *sound* (Isabelle's kernel certifies the proof; nitpick reports only
|
|
36
|
+
*genuine* counter-models), and necessarily *incomplete*.
|
|
37
|
+
|
|
38
|
+
Honesty / soundness boundary
|
|
39
|
+
----------------------------
|
|
40
|
+
A VALID / INVALID verdict is only as meaningful as the embedding's faithfulness
|
|
41
|
+
to the chosen semantics. UNKNOWN is a real outcome, not a failure. The runner
|
|
42
|
+
never claims a decision it did not obtain from the prover.
|
|
43
|
+
|
|
44
|
+
Which logic a verdict is *about* is selected by the options that reach the
|
|
45
|
+
emitter, and each of them is forwarded to **every** theory the decision builds —
|
|
46
|
+
the prove theory, the proof battery, and the nitpick theory. That is not
|
|
47
|
+
tidiness: the two steps decide opposite questions, so an option that reached only
|
|
48
|
+
one of them would produce a verdict about a logic nobody asked for (a prove
|
|
49
|
+
theory missing an axiom degrades to UNKNOWN; a *nitpick* theory missing one
|
|
50
|
+
certifies a "genuine" counter-model outside the requested class, i.e. a false
|
|
51
|
+
INVALID). The options are ``frame`` / ``mode`` / ``systems`` / ``temporal_closure``
|
|
52
|
+
/ ``bridges`` for :func:`isabelle_decide_modal`, and ``centering`` for
|
|
53
|
+
:func:`isabelle_decide_counterfactual` — whose default ``"weak"`` matches
|
|
54
|
+
:func:`~unicode_logic_kit.semantics.conditional.cf_valid`, so the two routes decide
|
|
55
|
+
the same conditional logic unless told otherwise.
|
|
56
|
+
|
|
57
|
+
Platform support
|
|
58
|
+
----------------
|
|
59
|
+
**Linux and macOS are the primary path**: the ``isabelle`` binary is invoked
|
|
60
|
+
directly (``isabelle build -D <dir>``) with nothing platform-specific. **Windows**
|
|
61
|
+
is also supported — Isabelle ships its own Cygwin and its CLI is a Bash script, so
|
|
62
|
+
there the runner additionally routes the build through ``contrib/cygwin/bin/bash``,
|
|
63
|
+
translates the scratch path to a ``/cygdrive/...`` path, and (idempotently) sets the
|
|
64
|
+
execute bit on the launcher scripts (some installs ship them without it, which makes
|
|
65
|
+
the internal ``exec isabelle java`` fail with *Permission denied*). No install path
|
|
66
|
+
is ever hard-coded; installations are discovered generically (see below).
|
|
67
|
+
|
|
68
|
+
Locating Isabelle
|
|
69
|
+
-----------------
|
|
70
|
+
:func:`find_isabelle` looks, in order, at an explicit path, the env vars
|
|
71
|
+
``UFK_ISABELLE_HOME`` / ``ISABELLE_HOME``, ``isabelle`` on ``PATH``, and a light
|
|
72
|
+
scan of standard install locations. :func:`isabelle_available` is the cheap
|
|
73
|
+
predicate tests gate on (skip when no Isabelle is present).
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
from __future__ import annotations
|
|
77
|
+
|
|
78
|
+
import os
|
|
79
|
+
import re
|
|
80
|
+
import shutil
|
|
81
|
+
import subprocess
|
|
82
|
+
import sys
|
|
83
|
+
import tempfile
|
|
84
|
+
import time
|
|
85
|
+
import uuid
|
|
86
|
+
from dataclasses import dataclass, field
|
|
87
|
+
from typing import Iterable, List, Optional, Sequence, Tuple
|
|
88
|
+
|
|
89
|
+
from unicode_logic_kit.fol.nodes import Node
|
|
90
|
+
from unicode_logic_kit.hol.isabelle_modal import (
|
|
91
|
+
isabelle_modal_theory, modal_axiom_names,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
__all__ = [
|
|
95
|
+
"IsabelleNotAvailable", "IsabelleInstall", "BuildResult",
|
|
96
|
+
"ModalVerdict", "FolVerdict",
|
|
97
|
+
"find_isabelle", "isabelle_available",
|
|
98
|
+
"check_theory", "isabelle_decide_modal", "isabelle_decide_fol",
|
|
99
|
+
"isabelle_decide_relevant", "isabelle_decide_free",
|
|
100
|
+
"DEFAULT_METHODS",
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
# Ordered so cheap, fail-fast methods run before the ones that can loop; the first
|
|
104
|
+
# alternative to close the goal wins. meson/metis take the axioms as explicit args.
|
|
105
|
+
DEFAULT_METHODS: Tuple[str, ...] = (
|
|
106
|
+
"blast", "force", "fastforce", "auto", "meson", "metis",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class IsabelleNotAvailable(RuntimeError):
|
|
111
|
+
"""Raised when an Isabelle install is required but none could be located."""
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# --------------------------------------------------------------------------- #
|
|
115
|
+
# Locating an Isabelle installation.
|
|
116
|
+
# --------------------------------------------------------------------------- #
|
|
117
|
+
|
|
118
|
+
_VERSION_RE = re.compile(r"Isabelle\d{4}(?:-\d+)?", re.I)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@dataclass(frozen=True)
|
|
122
|
+
class IsabelleInstall:
|
|
123
|
+
"""A located Isabelle/HOL installation and how to invoke its CLI."""
|
|
124
|
+
|
|
125
|
+
home: str # ISABELLE_HOME (the install directory)
|
|
126
|
+
is_windows: bool # invoke via the bundled Cygwin bash?
|
|
127
|
+
isabelle_exe: str # POSIX: path to bin/isabelle (direct)
|
|
128
|
+
cygwin_bash: Optional[str] = None # Windows: contrib/cygwin/bin/bash.exe
|
|
129
|
+
version: Optional[str] = None
|
|
130
|
+
|
|
131
|
+
def __str__(self) -> str:
|
|
132
|
+
return f"Isabelle({self.version or '?'} at {self.home})"
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _validate_install(home: Optional[str]) -> Optional[IsabelleInstall]:
|
|
136
|
+
"""Turn a candidate ``ISABELLE_HOME`` into an :class:`IsabelleInstall` or None."""
|
|
137
|
+
if not home:
|
|
138
|
+
return None
|
|
139
|
+
home = os.path.abspath(home)
|
|
140
|
+
if not os.path.isdir(home):
|
|
141
|
+
return None
|
|
142
|
+
launcher = os.path.join(home, "bin", "isabelle")
|
|
143
|
+
if not os.path.isfile(launcher):
|
|
144
|
+
return None
|
|
145
|
+
m = _VERSION_RE.search(os.path.basename(home.rstrip("\\/")))
|
|
146
|
+
version = m.group(0) if m else None
|
|
147
|
+
if os.name == "nt":
|
|
148
|
+
bash = os.path.join(home, "contrib", "cygwin", "bin", "bash.exe")
|
|
149
|
+
if not os.path.isfile(bash):
|
|
150
|
+
return None
|
|
151
|
+
return IsabelleInstall(home=home, is_windows=True, isabelle_exe=launcher,
|
|
152
|
+
cygwin_bash=bash, version=version)
|
|
153
|
+
return IsabelleInstall(home=home, is_windows=False, isabelle_exe=launcher,
|
|
154
|
+
version=version)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _scan_standard_locations() -> List[str]:
|
|
158
|
+
"""A light, best-effort scan for ``Isabelle*`` install dirs (no deep recursion)."""
|
|
159
|
+
out: List[str] = []
|
|
160
|
+
try:
|
|
161
|
+
if os.name == "nt":
|
|
162
|
+
import glob
|
|
163
|
+
roots = [r"C:\Program Files", r"C:\Program Files (x86)"]
|
|
164
|
+
drives = [f"{d}:\\" for d in "CDEFG" if os.path.isdir(f"{d}:\\")]
|
|
165
|
+
roots += drives
|
|
166
|
+
patterns: List[str] = []
|
|
167
|
+
for r in roots:
|
|
168
|
+
patterns.append(os.path.join(r, "Isabelle*"))
|
|
169
|
+
patterns.append(os.path.join(r, "*", "Isabelle*")) # an Isabelle* dir one level down
|
|
170
|
+
for pat in patterns:
|
|
171
|
+
try:
|
|
172
|
+
out += [p for p in glob.glob(pat) if os.path.isdir(p)]
|
|
173
|
+
except OSError:
|
|
174
|
+
continue
|
|
175
|
+
else:
|
|
176
|
+
import glob
|
|
177
|
+
home = os.path.expanduser("~")
|
|
178
|
+
for r in ("/usr/local", "/opt", home, os.path.join(home, ".local")):
|
|
179
|
+
try:
|
|
180
|
+
out += [p for p in glob.glob(os.path.join(r, "Isabelle*"))
|
|
181
|
+
if os.path.isdir(p)]
|
|
182
|
+
except OSError:
|
|
183
|
+
continue
|
|
184
|
+
except Exception:
|
|
185
|
+
return out
|
|
186
|
+
# Prefer the highest version string (lexicographic on Isabelleyyyy-n is fine).
|
|
187
|
+
out.sort(reverse=True)
|
|
188
|
+
return out
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
_FIND_CACHE: dict = {}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def find_isabelle(isabelle_home: Optional[str] = None,
|
|
195
|
+
*, use_cache: bool = True) -> Optional[IsabelleInstall]:
|
|
196
|
+
"""Locate an Isabelle installation, or return ``None`` if none is found.
|
|
197
|
+
|
|
198
|
+
Search order: ``isabelle_home`` argument → env ``UFK_ISABELLE_HOME`` → env
|
|
199
|
+
``ISABELLE_HOME`` → ``isabelle`` on ``PATH`` → a light scan of standard install
|
|
200
|
+
locations. The result is cached (keyed by the explicit argument); pass
|
|
201
|
+
``use_cache=False`` to force a fresh lookup.
|
|
202
|
+
"""
|
|
203
|
+
key = isabelle_home or ""
|
|
204
|
+
if use_cache and key in _FIND_CACHE:
|
|
205
|
+
return _FIND_CACHE[key]
|
|
206
|
+
|
|
207
|
+
candidates: List[str] = []
|
|
208
|
+
if isabelle_home:
|
|
209
|
+
candidates.append(isabelle_home)
|
|
210
|
+
for var in ("UFK_ISABELLE_HOME", "ISABELLE_HOME"):
|
|
211
|
+
v = os.environ.get(var)
|
|
212
|
+
if v:
|
|
213
|
+
candidates.append(v)
|
|
214
|
+
exe = shutil.which("isabelle")
|
|
215
|
+
if exe:
|
|
216
|
+
candidates.append(os.path.dirname(os.path.dirname(os.path.abspath(exe))))
|
|
217
|
+
candidates += _scan_standard_locations()
|
|
218
|
+
|
|
219
|
+
found: Optional[IsabelleInstall] = None
|
|
220
|
+
seen = set()
|
|
221
|
+
for home in candidates:
|
|
222
|
+
ah = os.path.abspath(home) if home else home
|
|
223
|
+
if ah in seen:
|
|
224
|
+
continue
|
|
225
|
+
seen.add(ah)
|
|
226
|
+
inst = _validate_install(home)
|
|
227
|
+
if inst:
|
|
228
|
+
found = inst
|
|
229
|
+
break
|
|
230
|
+
if use_cache:
|
|
231
|
+
_FIND_CACHE[key] = found
|
|
232
|
+
return found
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def isabelle_available(isabelle_home: Optional[str] = None) -> bool:
|
|
236
|
+
"""``True`` iff an Isabelle installation can be located (cheap; cached)."""
|
|
237
|
+
return find_isabelle(isabelle_home) is not None
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
# --------------------------------------------------------------------------- #
|
|
241
|
+
# Invoking ``isabelle build`` on a scratch session.
|
|
242
|
+
# --------------------------------------------------------------------------- #
|
|
243
|
+
|
|
244
|
+
def _cygpath(win_path: str) -> str:
|
|
245
|
+
r"""Translate a Windows path to a Cygwin ``/cygdrive/<d>/...`` path.
|
|
246
|
+
|
|
247
|
+
Parses the drive letter directly rather than via ``os.path`` (whose drive
|
|
248
|
+
handling differs on POSIX — ``splitdrive`` ignores ``C:`` and ``abspath``
|
|
249
|
+
prepends the cwd), so the function is correct and testable on every platform,
|
|
250
|
+
though it is only *used* on Windows. Inputs from the runner are already absolute.
|
|
251
|
+
"""
|
|
252
|
+
p = win_path.replace("\\", "/")
|
|
253
|
+
if len(p) >= 2 and p[0].isalpha() and p[1] == ":":
|
|
254
|
+
return "/cygdrive/" + p[0].lower() + p[2:]
|
|
255
|
+
return p
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _run_build(install: IsabelleInstall, session_dir: str,
|
|
259
|
+
wall_timeout: float) -> Tuple[int, str]:
|
|
260
|
+
"""Run ``isabelle build -D <session_dir>``; return (exit_code, combined_output)."""
|
|
261
|
+
if not install.is_windows:
|
|
262
|
+
# Linux / macOS — the primary path: invoke the isabelle binary directly.
|
|
263
|
+
argv = [install.isabelle_exe, "build", "-D", session_dir]
|
|
264
|
+
env = dict(os.environ)
|
|
265
|
+
else:
|
|
266
|
+
# Windows: route through Isabelle's bundled Cygwin bash. One bash -c puts bin
|
|
267
|
+
# on PATH, ensures the launcher scripts are executable (idempotent; some
|
|
268
|
+
# installs ship them without the bit), then builds the /cygdrive-translated dir.
|
|
269
|
+
cyg_home = _cygpath(install.home)
|
|
270
|
+
cyg_dir = _cygpath(session_dir)
|
|
271
|
+
script = (
|
|
272
|
+
f'I="{cyg_home}"\n'
|
|
273
|
+
f'export PATH="$I/bin:$PATH"\n'
|
|
274
|
+
f'chmod -R u+x "$I/bin" "$I/lib/Tools" "$I/lib/scripts" 2>/dev/null\n'
|
|
275
|
+
f'isabelle build -D "{cyg_dir}"\n'
|
|
276
|
+
)
|
|
277
|
+
argv = [install.cygwin_bash, "--login", "-c", script]
|
|
278
|
+
env = dict(os.environ, CHERE_INVOKING="true", LANG="en_US.UTF-8")
|
|
279
|
+
try:
|
|
280
|
+
proc = subprocess.run(
|
|
281
|
+
argv, capture_output=True, text=True, encoding="utf-8",
|
|
282
|
+
errors="replace", env=env, timeout=wall_timeout,
|
|
283
|
+
)
|
|
284
|
+
except subprocess.TimeoutExpired as e:
|
|
285
|
+
out = (e.stdout or "") + (e.stderr or "")
|
|
286
|
+
if isinstance(out, bytes):
|
|
287
|
+
out = out.decode("utf-8", "replace")
|
|
288
|
+
return 124, out + f"\n[runner] wall-clock timeout after {wall_timeout}s\n"
|
|
289
|
+
return proc.returncode, (proc.stdout or "") + (proc.stderr or "")
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
@dataclass
|
|
293
|
+
class BuildResult:
|
|
294
|
+
"""Outcome of building one scratch theory with ``isabelle build``."""
|
|
295
|
+
|
|
296
|
+
ok: bool # exit code == 0 (every lemma in the theory discharged)
|
|
297
|
+
exit_code: int
|
|
298
|
+
output: str # combined stdout+stderr of the build
|
|
299
|
+
theory_name: str
|
|
300
|
+
session: str
|
|
301
|
+
elapsed: float
|
|
302
|
+
|
|
303
|
+
def __bool__(self) -> bool:
|
|
304
|
+
return self.ok
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def check_theory(theory_text: str, theory_name: str, *,
|
|
308
|
+
install: Optional[IsabelleInstall] = None,
|
|
309
|
+
session: Optional[str] = None,
|
|
310
|
+
session_timeout: int = 120,
|
|
311
|
+
wall_timeout: Optional[float] = None,
|
|
312
|
+
keep: bool = False) -> BuildResult:
|
|
313
|
+
"""Build one self-contained Isabelle theory and report whether it loads/proves.
|
|
314
|
+
|
|
315
|
+
Writes ``<theory_name>.thy`` plus a ``ROOT`` (``session ... = HOL + options
|
|
316
|
+
[timeout] theories <theory_name>``) into a scratch directory and runs
|
|
317
|
+
``isabelle build`` on it. ``ok`` is ``True`` iff the build exits 0 — i.e. every
|
|
318
|
+
proof in the theory was discharged by Isabelle's kernel.
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
theory_text: the full ``theory <theory_name> ... end`` text. Its declared
|
|
322
|
+
theory name MUST equal ``theory_name``.
|
|
323
|
+
theory_name: the theory / file name (a valid Isabelle identifier).
|
|
324
|
+
install: an :class:`IsabelleInstall`; located via :func:`find_isabelle`
|
|
325
|
+
when ``None``.
|
|
326
|
+
session: the build session name (default ``"S_" + theory_name``).
|
|
327
|
+
session_timeout: per-session Isabelle ``timeout`` option, in seconds.
|
|
328
|
+
wall_timeout: hard subprocess timeout; defaults to ``session_timeout + 180``
|
|
329
|
+
to cover JVM startup + image load.
|
|
330
|
+
keep: keep the scratch directory (for debugging) instead of deleting it.
|
|
331
|
+
|
|
332
|
+
Raises:
|
|
333
|
+
IsabelleNotAvailable: if no Isabelle installation can be located.
|
|
334
|
+
"""
|
|
335
|
+
install = install or find_isabelle()
|
|
336
|
+
if install is None:
|
|
337
|
+
raise IsabelleNotAvailable(
|
|
338
|
+
"No Isabelle installation found. Set UFK_ISABELLE_HOME (or ISABELLE_HOME) "
|
|
339
|
+
"to the install directory, or put `isabelle` on PATH.")
|
|
340
|
+
session = session or ("S_" + theory_name)
|
|
341
|
+
if wall_timeout is None:
|
|
342
|
+
wall_timeout = float(session_timeout) + 180.0
|
|
343
|
+
|
|
344
|
+
work = tempfile.mkdtemp(prefix="ufk_isa_")
|
|
345
|
+
try:
|
|
346
|
+
with open(os.path.join(work, theory_name + ".thy"), "w",
|
|
347
|
+
encoding="utf-8") as f:
|
|
348
|
+
f.write(theory_text)
|
|
349
|
+
root = (f'session "{session}" = HOL +\n'
|
|
350
|
+
f' options [timeout = {int(session_timeout)}]\n'
|
|
351
|
+
f' theories\n {theory_name}\n')
|
|
352
|
+
with open(os.path.join(work, "ROOT"), "w", encoding="utf-8") as f:
|
|
353
|
+
f.write(root)
|
|
354
|
+
t0 = time.perf_counter()
|
|
355
|
+
code, output = _run_build(install, work, wall_timeout)
|
|
356
|
+
elapsed = time.perf_counter() - t0
|
|
357
|
+
return BuildResult(ok=(code == 0), exit_code=code, output=output,
|
|
358
|
+
theory_name=theory_name, session=session,
|
|
359
|
+
elapsed=elapsed)
|
|
360
|
+
finally:
|
|
361
|
+
if not keep:
|
|
362
|
+
shutil.rmtree(work, ignore_errors=True)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
# --------------------------------------------------------------------------- #
|
|
366
|
+
# Deciding modal validity through Isabelle.
|
|
367
|
+
# --------------------------------------------------------------------------- #
|
|
368
|
+
|
|
369
|
+
VALID = "valid"
|
|
370
|
+
INVALID = "invalid"
|
|
371
|
+
UNKNOWN = "unknown"
|
|
372
|
+
|
|
373
|
+
_NITPICK_CTEX_RE = re.compile(
|
|
374
|
+
r"Nitpick found a (?:genuine )?counterexample.*", re.S)
|
|
375
|
+
|
|
376
|
+
# Signatures of an INFRASTRUCTURE failure (a broken theory or a broken environment),
|
|
377
|
+
# as opposed to an honest "no proof / no counter-model". A non-zero build that matches
|
|
378
|
+
# one of these is surfaced on ModalVerdict.infra_error so an UNKNOWN is not silently
|
|
379
|
+
# mistaken for benign incompleteness. This never changes the verdict — both decisive
|
|
380
|
+
# verdicts require an exit-0 build, so an infra failure can only land on UNKNOWN, the
|
|
381
|
+
# conservative side; this just makes the cause visible.
|
|
382
|
+
_INFRA_ERROR_RE = re.compile(
|
|
383
|
+
r"Outer syntax error|Inner syntax error|Type unification failed|Bad name|"
|
|
384
|
+
r"Malformed|Undefined command|Undefined type|Duplicate constant|"
|
|
385
|
+
r"java\.lang\.\w*(?:OutOfMemoryError|Error)|Permission denied|"
|
|
386
|
+
r"wall-clock timeout|No such file")
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _infra_error(*outputs: str) -> Optional[str]:
|
|
390
|
+
"""Return the first infrastructure-error signature found in the build outputs."""
|
|
391
|
+
for out in outputs:
|
|
392
|
+
m = _INFRA_ERROR_RE.search(out or "")
|
|
393
|
+
if m:
|
|
394
|
+
return m.group(0)
|
|
395
|
+
return None
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
@dataclass
|
|
399
|
+
class ModalVerdict:
|
|
400
|
+
"""Result of deciding a modal formula's validity through Isabelle.
|
|
401
|
+
|
|
402
|
+
``status`` is one of ``"valid"`` (a battery proof closed the lemma),
|
|
403
|
+
``"invalid"`` (nitpick found a genuine counter-model), or ``"unknown"``
|
|
404
|
+
(neither, within budget). Truthiness follows validity. On an ``"unknown"`` result,
|
|
405
|
+
``infra_error`` is set to a short signature when a build failed for an
|
|
406
|
+
infrastructure reason (syntax/JVM/timeout/…) rather than honest incompleteness —
|
|
407
|
+
it never changes the status (an infra failure can only land on UNKNOWN).
|
|
408
|
+
"""
|
|
409
|
+
|
|
410
|
+
status: str
|
|
411
|
+
frame: str
|
|
412
|
+
mode: str
|
|
413
|
+
method: Optional[str] = None # the proof family that closed it (when valid)
|
|
414
|
+
countermodel: Optional[str] = None # for an INVALID alethic-propositional formula, a concrete
|
|
415
|
+
# Kripke counter-model from the toolkit's evaluator (isabelle
|
|
416
|
+
# build does not echo nitpick's model); None for fragments the
|
|
417
|
+
# bounded search does not cover — verdict still certified
|
|
418
|
+
prove_output: str = ""
|
|
419
|
+
refute_output: str = ""
|
|
420
|
+
prove_elapsed: float = 0.0
|
|
421
|
+
refute_elapsed: float = 0.0
|
|
422
|
+
infra_error: Optional[str] = None # set on UNKNOWN when a build failed for an
|
|
423
|
+
# infrastructure reason (syntax/JVM/timeout/…),
|
|
424
|
+
# not honest incompleteness; never affects status
|
|
425
|
+
|
|
426
|
+
@property
|
|
427
|
+
def is_valid(self) -> bool:
|
|
428
|
+
return self.status == VALID
|
|
429
|
+
|
|
430
|
+
@property
|
|
431
|
+
def is_invalid(self) -> bool:
|
|
432
|
+
return self.status == INVALID
|
|
433
|
+
|
|
434
|
+
@property
|
|
435
|
+
def is_unknown(self) -> bool:
|
|
436
|
+
return self.status == UNKNOWN
|
|
437
|
+
|
|
438
|
+
def __bool__(self) -> bool:
|
|
439
|
+
return self.status == VALID
|
|
440
|
+
|
|
441
|
+
def __str__(self) -> str:
|
|
442
|
+
extra = ""
|
|
443
|
+
if self.status == VALID and self.method:
|
|
444
|
+
extra = f" (by {self.method})"
|
|
445
|
+
return f"ModalVerdict[{self.status}{extra}, frame={self.frame}, mode={self.mode}]"
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
# Frame conditions on the alethic relation r (mirror the emitter's _FRAMES).
|
|
449
|
+
_FRAME_CONDS = {
|
|
450
|
+
"K": (), "T": ("refl",), "S4": ("refl", "trans"),
|
|
451
|
+
"S5": ("refl", "trans", "sym"), "KD": ("serial",),
|
|
452
|
+
"KD45": ("serial", "trans", "eucl"),
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
# Node types of the purely propositional ALETHIC fragment the bounded countermodel
|
|
456
|
+
# search can exhibit a witness for (no quantifiers / agents / deontic / temporal).
|
|
457
|
+
_ALETHIC_OK = ("Atom", "Not", "And", "Or", "Xor", "Implies", "Iff", "Box", "Diamond")
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def _is_alethic_propositional(formula: Node) -> bool:
|
|
461
|
+
"""True iff ``formula`` is Box/Diamond + connectives over GROUND atoms only.
|
|
462
|
+
|
|
463
|
+
An identity atom (``a = b`` / ``a ≠ b``) is NOT propositional: ``satisfies_modal``
|
|
464
|
+
reads an atom as a letter in a world's valuation, has no term semantics, and so
|
|
465
|
+
refuses identity by name. Answering ``True`` here would route the formula to that
|
|
466
|
+
evaluator AFTER Isabelle has already certified the verdict (nitpick said INVALID),
|
|
467
|
+
and the refusal would raise past a result the runner already holds. Identity is
|
|
468
|
+
decided by Isabelle alone, where it is rigid HOL ``=``; there is simply no
|
|
469
|
+
propositional Kripke witness to exhibit for it, so ``countermodel`` stays ``None``.
|
|
470
|
+
"""
|
|
471
|
+
from unicode_logic_kit.fol.nodes import Variable
|
|
472
|
+
from unicode_logic_kit.semantics._modal_reject import is_equality_atom
|
|
473
|
+
for node in formula.walk():
|
|
474
|
+
if is_equality_atom(node):
|
|
475
|
+
return False
|
|
476
|
+
if type(node).__name__ not in _ALETHIC_OK and not isinstance(node, Variable):
|
|
477
|
+
# allow the ground arguments of an atom (Constant/Number/Function), but a
|
|
478
|
+
# free/bound Variable means it is not propositional.
|
|
479
|
+
if type(node).__name__ in ("Constant", "Number", "Function"):
|
|
480
|
+
continue
|
|
481
|
+
return False
|
|
482
|
+
if isinstance(node, Variable):
|
|
483
|
+
return False
|
|
484
|
+
return True
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def _frame_ok(worlds, rel, conds) -> bool:
|
|
488
|
+
if "refl" in conds and not all((w, w) in rel for w in worlds):
|
|
489
|
+
return False
|
|
490
|
+
if "trans" in conds and any((a, b) in rel and (b, c) in rel and (a, c) not in rel
|
|
491
|
+
for a in worlds for b in worlds for c in worlds):
|
|
492
|
+
return False
|
|
493
|
+
if "sym" in conds and any((a, b) in rel and (b, a) not in rel
|
|
494
|
+
for a in worlds for b in worlds):
|
|
495
|
+
return False
|
|
496
|
+
if "serial" in conds and not all(any((w, v) in rel for v in worlds) for w in worlds):
|
|
497
|
+
return False
|
|
498
|
+
if "eucl" in conds and any((a, b) in rel and (a, c) in rel and (b, c) not in rel
|
|
499
|
+
for a in worlds for b in worlds for c in worlds):
|
|
500
|
+
return False
|
|
501
|
+
return True
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def _find_alethic_countermodel(formula: Node, frame: str,
|
|
505
|
+
max_worlds: int = 4) -> Optional[str]:
|
|
506
|
+
"""A concrete Kripke counter-model to ``formula`` over ``frame``, as a readable
|
|
507
|
+
string — or ``None`` if the formula is outside the propositional alethic fragment
|
|
508
|
+
or no counter-model exists within ``max_worlds``.
|
|
509
|
+
|
|
510
|
+
Isabelle/nitpick certifies the INVALID verdict; this exhibits a witness from the
|
|
511
|
+
toolkit's own evaluator (``isabelle build`` does not echo nitpick's model). Bounded
|
|
512
|
+
brute force over the frame's relations and valuations, decided by ``satisfies_modal``.
|
|
513
|
+
"""
|
|
514
|
+
if frame not in _FRAME_CONDS or not _is_alethic_propositional(formula):
|
|
515
|
+
return None
|
|
516
|
+
from itertools import product
|
|
517
|
+
from unicode_logic_kit.fol.nodes import Atom
|
|
518
|
+
from unicode_logic_kit.semantics.kripke import KripkeModel, satisfies_modal
|
|
519
|
+
|
|
520
|
+
conds = _FRAME_CONDS[frame]
|
|
521
|
+
from unicode_logic_kit.fol._truth_constants import truth_value
|
|
522
|
+
from unicode_logic_kit.fol._msfl_nodes import key_text
|
|
523
|
+
# the valuation keys are the ones satisfies_modal looks up, and the ones the readable
|
|
524
|
+
# counter-model shows: every constant by its bare name, as ever
|
|
525
|
+
keys = sorted({key_text(n) for n in formula.walk()
|
|
526
|
+
if isinstance(n, Atom) and truth_value(n) is None})
|
|
527
|
+
for n in range(1, max_worlds + 1):
|
|
528
|
+
worlds = list(range(n))
|
|
529
|
+
edges = [(i, j) for i in worlds for j in worlds]
|
|
530
|
+
for mask in product((False, True), repeat=len(edges)):
|
|
531
|
+
rel = frozenset(e for e, inc in zip(edges, mask) if inc)
|
|
532
|
+
if not _frame_ok(worlds, rel, conds):
|
|
533
|
+
continue
|
|
534
|
+
for bits in product((False, True), repeat=n * len(keys)):
|
|
535
|
+
val, idx = {}, 0
|
|
536
|
+
for w in worlds:
|
|
537
|
+
cell = set()
|
|
538
|
+
for k in keys:
|
|
539
|
+
if bits[idx]:
|
|
540
|
+
cell.add(k)
|
|
541
|
+
idx += 1
|
|
542
|
+
val[w] = cell
|
|
543
|
+
model = KripkeModel(worlds, {"alethic": rel}, val)
|
|
544
|
+
bad = [w for w in worlds if not satisfies_modal(formula, model, w)]
|
|
545
|
+
if bad:
|
|
546
|
+
rels = "{" + ", ".join(f"{a}->{b}" for (a, b) in sorted(rel)) + "}"
|
|
547
|
+
vals = ", ".join(f"{w}:{{{', '.join(sorted(val[w])) or '-'}}}"
|
|
548
|
+
for w in worlds)
|
|
549
|
+
return (f"Kripke counter-model (frame {frame}, {n} world(s)): "
|
|
550
|
+
f"worlds={{{', '.join(map(str, worlds))}}}, r={rels}, "
|
|
551
|
+
f"valuation=[{vals}]; formula is false at world {bad[0]}")
|
|
552
|
+
return None
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def _battery_proof(axiom_ids: Sequence[str], methods: Sequence[str]) -> str:
|
|
556
|
+
"""Build a ``using <axioms> by (m1 | m2 | ...)`` proof block (the prove step)."""
|
|
557
|
+
alts: List[str] = []
|
|
558
|
+
facts = (" " + " ".join(axiom_ids)) if axiom_ids else ""
|
|
559
|
+
for m in methods:
|
|
560
|
+
if m in ("meson", "metis"):
|
|
561
|
+
alts.append(m + facts) # accept the axioms as explicit facts
|
|
562
|
+
elif m == "smt":
|
|
563
|
+
alts.append("(smt (verit))")
|
|
564
|
+
else:
|
|
565
|
+
alts.append(m)
|
|
566
|
+
proof = ""
|
|
567
|
+
if axiom_ids:
|
|
568
|
+
proof += " using " + " ".join(axiom_ids) + "\n"
|
|
569
|
+
proof += " by (" + " | ".join(alts) + ")"
|
|
570
|
+
return proof
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def isabelle_decide_modal(
|
|
574
|
+
formula: Node, *,
|
|
575
|
+
frame: str = "K",
|
|
576
|
+
mode: str = "constant",
|
|
577
|
+
temporal_closure: bool = True,
|
|
578
|
+
systems: Optional[dict] = None,
|
|
579
|
+
bridges: Optional[Iterable[str]] = None,
|
|
580
|
+
methods: Sequence[str] = DEFAULT_METHODS,
|
|
581
|
+
refute: bool = True,
|
|
582
|
+
card: str = "1-4",
|
|
583
|
+
prove_timeout: int = 60,
|
|
584
|
+
refute_timeout: int = 60,
|
|
585
|
+
install: Optional[IsabelleInstall] = None,
|
|
586
|
+
) -> ModalVerdict:
|
|
587
|
+
"""Decide a modal formula's validity by running a local Isabelle.
|
|
588
|
+
|
|
589
|
+
Emits the Benzmüller shallow embedding of ``formula``
|
|
590
|
+
(:func:`~unicode_logic_kit.hol.isabelle_modal.isabelle_modal_theory`, faithful to
|
|
591
|
+
:func:`~unicode_logic_kit.semantics.kripke.satisfies_modal`) and:
|
|
592
|
+
|
|
593
|
+
1. tries a proof battery — exit 0 ⇒ :data:`VALID`;
|
|
594
|
+
2. otherwise (``refute``) runs ``nitpick[expect = genuine]`` — exit 0 ⇒
|
|
595
|
+
:data:`INVALID` (certified by the exit code; for the propositional alethic
|
|
596
|
+
fragment ``countermodel`` carries a concrete Kripke model from the toolkit's
|
|
597
|
+
evaluator, else ``None``);
|
|
598
|
+
3. otherwise :data:`UNKNOWN`.
|
|
599
|
+
|
|
600
|
+
Args:
|
|
601
|
+
formula: the modal AST node.
|
|
602
|
+
frame: alethic frame system (K/T/S4/S5/KD/KD45) — fixes the axioms on ``r``.
|
|
603
|
+
mode: object-quantifier domain regime (constant/possibilist/varying/…).
|
|
604
|
+
temporal_closure: constrain the temporal relation to refl+trans (henceforth).
|
|
605
|
+
systems: optional per-family frame systems for the agent-indexed relations
|
|
606
|
+
(``{"epistemic": "S5", "doxastic": "KD45", "assertive"/"bouletic": …}``),
|
|
607
|
+
passed through to the emitter — so e.g. factivity ``K_a P → P`` is
|
|
608
|
+
provable under ``{"epistemic": "T"}``.
|
|
609
|
+
bridges: optional CROSS-family bridge names from
|
|
610
|
+
:data:`~unicode_logic_kit.hol.isabelle_modal.BRIDGES`
|
|
611
|
+
(``knowledge_implies_belief`` / ``sincerity`` / ``ought_implies_can``),
|
|
612
|
+
off by default. Threading this through is load-bearing on BOTH steps,
|
|
613
|
+
not only on the prove step: the prove theory needs the axiom *and* its
|
|
614
|
+
name in the ``using`` list (an ``axiomatization`` fact is not in the
|
|
615
|
+
default claset), and the nitpick theory needs the same axiom or nitpick
|
|
616
|
+
would happily build a model violating the bridge and certify a
|
|
617
|
+
**false INVALID** for a formula that is valid over the requested class.
|
|
618
|
+
``ValueError`` when a requested bridge's partner family does not occur
|
|
619
|
+
in ``formula`` (see
|
|
620
|
+
:func:`~unicode_logic_kit.hol.isabelle_modal._bridge_axioms`).
|
|
621
|
+
methods: proof methods tried, in order, as a ``by (… | …)`` battery.
|
|
622
|
+
refute: also run nitpick to certify INVALID when the proof battery fails.
|
|
623
|
+
card: nitpick cardinality range for the world type ``i`` (e.g. ``"1-4"``).
|
|
624
|
+
prove_timeout / refute_timeout: per-session Isabelle timeouts (seconds).
|
|
625
|
+
install: an :class:`IsabelleInstall`; located via :func:`find_isabelle` when
|
|
626
|
+
``None``.
|
|
627
|
+
|
|
628
|
+
Returns:
|
|
629
|
+
A :class:`ModalVerdict`.
|
|
630
|
+
|
|
631
|
+
Raises:
|
|
632
|
+
IsabelleNotAvailable: if no Isabelle installation can be located.
|
|
633
|
+
ValueError / NotImplementedError: propagated from the emitter for an
|
|
634
|
+
unsupported frame/mode/operator (e.g. ``Until``).
|
|
635
|
+
"""
|
|
636
|
+
install = install or find_isabelle()
|
|
637
|
+
if install is None:
|
|
638
|
+
raise IsabelleNotAvailable(
|
|
639
|
+
"No Isabelle installation found. Set UFK_ISABELLE_HOME (or ISABELLE_HOME) "
|
|
640
|
+
"to the install directory, or put `isabelle` on PATH.")
|
|
641
|
+
|
|
642
|
+
# An unknown bridge name (or a bridge whose partner family is absent) is a
|
|
643
|
+
# ValueError from the emitter; raising it here, before the first build, keeps a
|
|
644
|
+
# typo from costing a JVM start — and every call below passes the SAME bridges,
|
|
645
|
+
# so the axiom list, the prove theory and the nitpick theory cannot disagree
|
|
646
|
+
# about which logic is being decided.
|
|
647
|
+
axiom_ids = modal_axiom_names(formula, mode=mode, frame=frame,
|
|
648
|
+
temporal_closure=temporal_closure, systems=systems,
|
|
649
|
+
bridges=bridges)
|
|
650
|
+
|
|
651
|
+
# --- step 1: prove ---------------------------------------------------- #
|
|
652
|
+
tok = "G" + uuid.uuid4().hex[:8]
|
|
653
|
+
prove_proof = _battery_proof(axiom_ids, methods)
|
|
654
|
+
prove_thy = isabelle_modal_theory(
|
|
655
|
+
formula, mode=mode, frame=frame, tactic="oops", theory_name=tok,
|
|
656
|
+
temporal_closure=temporal_closure, proof=prove_proof, systems=systems,
|
|
657
|
+
bridges=bridges)
|
|
658
|
+
r1 = check_theory(prove_thy, tok, install=install,
|
|
659
|
+
session_timeout=prove_timeout)
|
|
660
|
+
if r1.ok:
|
|
661
|
+
return ModalVerdict(status=VALID, frame=frame, mode=mode,
|
|
662
|
+
method="prove-battery", prove_output=r1.output,
|
|
663
|
+
prove_elapsed=r1.elapsed)
|
|
664
|
+
|
|
665
|
+
# --- step 2: refute (nitpick) ----------------------------------------- #
|
|
666
|
+
refute_output = ""
|
|
667
|
+
refute_elapsed = 0.0
|
|
668
|
+
if refute:
|
|
669
|
+
tok2 = "G" + uuid.uuid4().hex[:8]
|
|
670
|
+
nit_proof = (f" nitpick[card i = {card}, timeout = {int(refute_timeout)}, "
|
|
671
|
+
f"expect = genuine]\n oops")
|
|
672
|
+
# temporal_def=True: when Always/Eventually co-occur with Next, define
|
|
673
|
+
# t = rtranclp n so nitpick can construct the closure and actually refute a
|
|
674
|
+
# non-theorem of the temporal fragment (the prove theory above keeps the
|
|
675
|
+
# axiom form, which the battery needs — both encode t = n**).
|
|
676
|
+
nit_thy = isabelle_modal_theory(
|
|
677
|
+
formula, mode=mode, frame=frame, tactic="oops", theory_name=tok2,
|
|
678
|
+
temporal_closure=temporal_closure, proof=nit_proof, temporal_def=True,
|
|
679
|
+
systems=systems, bridges=bridges)
|
|
680
|
+
# nitpick can use most of the session budget; give the wall clock headroom.
|
|
681
|
+
r2 = check_theory(nit_thy, tok2, install=install,
|
|
682
|
+
session_timeout=refute_timeout + 30,
|
|
683
|
+
wall_timeout=float(refute_timeout) + 240.0)
|
|
684
|
+
refute_output = r2.output
|
|
685
|
+
refute_elapsed = r2.elapsed
|
|
686
|
+
if r2.ok:
|
|
687
|
+
m = _NITPICK_CTEX_RE.search(r2.output)
|
|
688
|
+
ctex = m.group(0).strip() if m else None
|
|
689
|
+
if ctex is None and not bridges:
|
|
690
|
+
# isabelle build does not echo nitpick's model; exhibit a witness from
|
|
691
|
+
# the toolkit's own evaluator for the propositional alethic fragment.
|
|
692
|
+
# Skipped under bridges=: the evaluator has no notion of a cross-family
|
|
693
|
+
# frame condition, so its witness would not be checked against the
|
|
694
|
+
# requested class. (Today this is belt-and-braces — a bridge requires
|
|
695
|
+
# its partner family to OCCUR, and every bridge has a non-alethic
|
|
696
|
+
# family, so _is_alethic_propositional already rejects such a formula —
|
|
697
|
+
# but the guard states the invariant instead of relying on it.)
|
|
698
|
+
ctex = _find_alethic_countermodel(formula, frame)
|
|
699
|
+
return ModalVerdict(status=INVALID, frame=frame, mode=mode,
|
|
700
|
+
countermodel=ctex, prove_output=r1.output,
|
|
701
|
+
refute_output=refute_output,
|
|
702
|
+
prove_elapsed=r1.elapsed,
|
|
703
|
+
refute_elapsed=refute_elapsed)
|
|
704
|
+
|
|
705
|
+
return ModalVerdict(status=UNKNOWN, frame=frame, mode=mode,
|
|
706
|
+
prove_output=r1.output, refute_output=refute_output,
|
|
707
|
+
prove_elapsed=r1.elapsed, refute_elapsed=refute_elapsed,
|
|
708
|
+
infra_error=_infra_error(r1.output, refute_output))
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
# --------------------------------------------------------------------------- #
|
|
712
|
+
# Deciding classical FOL / MSFOL through Isabelle.
|
|
713
|
+
# --------------------------------------------------------------------------- #
|
|
714
|
+
|
|
715
|
+
@dataclass
|
|
716
|
+
class FolVerdict:
|
|
717
|
+
"""Result of deciding a classical FOL formula's validity through Isabelle.
|
|
718
|
+
|
|
719
|
+
Same VALID / INVALID / UNKNOWN scheme as :class:`ModalVerdict` (no frame/mode).
|
|
720
|
+
Note that FOL is only *semi-decidable*, so UNKNOWN is common and expected; and
|
|
721
|
+
equality ``=`` / ``≠`` is by default the **uninterpreted** predicate ``feq`` /
|
|
722
|
+
``fneq`` of :func:`~unicode_logic_kit.hol.classical.to_isabelle_fol` (no equality
|
|
723
|
+
axioms are assumed), so e.g. ``∀x. x = x`` is *not* VALID here unless
|
|
724
|
+
:func:`isabelle_decide_fol` was called with ``native_equality=True``.
|
|
725
|
+
"""
|
|
726
|
+
|
|
727
|
+
status: str
|
|
728
|
+
method: Optional[str] = None
|
|
729
|
+
countermodel: Optional[str] = None # nitpick text if surfaced (usually None; verdict certified)
|
|
730
|
+
prove_output: str = ""
|
|
731
|
+
refute_output: str = ""
|
|
732
|
+
prove_elapsed: float = 0.0
|
|
733
|
+
refute_elapsed: float = 0.0
|
|
734
|
+
infra_error: Optional[str] = None
|
|
735
|
+
|
|
736
|
+
@property
|
|
737
|
+
def is_valid(self) -> bool:
|
|
738
|
+
return self.status == VALID
|
|
739
|
+
|
|
740
|
+
@property
|
|
741
|
+
def is_invalid(self) -> bool:
|
|
742
|
+
return self.status == INVALID
|
|
743
|
+
|
|
744
|
+
@property
|
|
745
|
+
def is_unknown(self) -> bool:
|
|
746
|
+
return self.status == UNKNOWN
|
|
747
|
+
|
|
748
|
+
def __bool__(self) -> bool:
|
|
749
|
+
return self.status == VALID
|
|
750
|
+
|
|
751
|
+
def __str__(self) -> str:
|
|
752
|
+
extra = f" (by {self.method})" if self.status == VALID and self.method else ""
|
|
753
|
+
return f"FolVerdict[{self.status}{extra}]"
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
def isabelle_decide_fol(
|
|
757
|
+
formula: Node, *,
|
|
758
|
+
msfol: bool = False,
|
|
759
|
+
native_equality: bool = False,
|
|
760
|
+
methods: Sequence[str] = DEFAULT_METHODS,
|
|
761
|
+
refute: bool = True,
|
|
762
|
+
card: str = "1-4",
|
|
763
|
+
prove_timeout: int = 60,
|
|
764
|
+
refute_timeout: int = 60,
|
|
765
|
+
install: Optional[IsabelleInstall] = None,
|
|
766
|
+
) -> FolVerdict:
|
|
767
|
+
"""Decide a classical **FOL** (or MSFOL) formula's validity by running Isabelle.
|
|
768
|
+
|
|
769
|
+
Emits the uninterpreted-signature embedding
|
|
770
|
+
(:func:`~unicode_logic_kit.hol.classical.to_isabelle_fol`, or ``to_isabelle_msfol``
|
|
771
|
+
when ``msfol=True``) and, exactly like :func:`isabelle_decide_modal`:
|
|
772
|
+
|
|
773
|
+
1. tries a proof battery — exit 0 ⇒ :data:`VALID`;
|
|
774
|
+
2. otherwise (``refute``) runs ``nitpick[expect = genuine]`` over the individual
|
|
775
|
+
type ``i`` — exit 0 ⇒ :data:`INVALID`;
|
|
776
|
+
3. otherwise :data:`UNKNOWN` (common — FOL is only semi-decidable).
|
|
777
|
+
|
|
778
|
+
Sound (Isabelle's kernel certifies the proof; nitpick reports only genuine finite
|
|
779
|
+
counter-models) and necessarily incomplete. By default equality is uninterpreted
|
|
780
|
+
(see :class:`FolVerdict`), so an INVALID verdict is about FOL *without* identity;
|
|
781
|
+
pass ``native_equality=True`` for classical FOL with identity, the reading of
|
|
782
|
+
the kit's own semantics and of :class:`~unicode_logic_kit.atp.protocol.IsabelleBackend`.
|
|
783
|
+
|
|
784
|
+
Args:
|
|
785
|
+
formula: the FOL AST node.
|
|
786
|
+
msfol: emit the many-sorted embedding (sorts relativised to guard predicates)
|
|
787
|
+
instead of plain FOL.
|
|
788
|
+
native_equality: render ``=`` / ``≠`` as HOL identity instead of the
|
|
789
|
+
uninterpreted ``feq`` / ``fneq`` (see
|
|
790
|
+
:func:`~unicode_logic_kit.hol.classical.to_isabelle_fol`).
|
|
791
|
+
methods / refute / card / prove_timeout / refute_timeout / install: as for
|
|
792
|
+
:func:`isabelle_decide_modal` (``card`` bounds the individual type ``i``).
|
|
793
|
+
|
|
794
|
+
Raises:
|
|
795
|
+
IsabelleNotAvailable: if no Isabelle installation can be located.
|
|
796
|
+
"""
|
|
797
|
+
install = install or find_isabelle()
|
|
798
|
+
if install is None:
|
|
799
|
+
raise IsabelleNotAvailable(
|
|
800
|
+
"No Isabelle installation found. Set UFK_ISABELLE_HOME (or ISABELLE_HOME) "
|
|
801
|
+
"to the install directory, or put `isabelle` on PATH.")
|
|
802
|
+
from unicode_logic_kit.hol.classical import to_isabelle_fol, to_isabelle_msfol
|
|
803
|
+
emit = to_isabelle_msfol if msfol else to_isabelle_fol
|
|
804
|
+
|
|
805
|
+
# --- step 1: prove ---------------------------------------------------- #
|
|
806
|
+
tok = "G" + uuid.uuid4().hex[:8]
|
|
807
|
+
prove_proof = _battery_proof([], methods).lstrip() # no axioms in the FOL embedding
|
|
808
|
+
prove_thy = emit(formula, theory_name=tok, proof=prove_proof,
|
|
809
|
+
native_equality=native_equality)
|
|
810
|
+
r1 = check_theory(prove_thy, tok, install=install, session_timeout=prove_timeout)
|
|
811
|
+
if r1.ok:
|
|
812
|
+
return FolVerdict(status=VALID, method="prove-battery",
|
|
813
|
+
prove_output=r1.output, prove_elapsed=r1.elapsed)
|
|
814
|
+
|
|
815
|
+
# --- step 2: refute (nitpick) ----------------------------------------- #
|
|
816
|
+
refute_output = ""
|
|
817
|
+
refute_elapsed = 0.0
|
|
818
|
+
if refute:
|
|
819
|
+
tok2 = "G" + uuid.uuid4().hex[:8]
|
|
820
|
+
nit_proof = (f"nitpick[card i = {card}, timeout = {int(refute_timeout)}, "
|
|
821
|
+
f"expect = genuine]\n oops")
|
|
822
|
+
nit_thy = emit(formula, theory_name=tok2, proof=nit_proof,
|
|
823
|
+
native_equality=native_equality)
|
|
824
|
+
r2 = check_theory(nit_thy, tok2, install=install,
|
|
825
|
+
session_timeout=refute_timeout + 30,
|
|
826
|
+
wall_timeout=float(refute_timeout) + 240.0)
|
|
827
|
+
refute_output = r2.output
|
|
828
|
+
refute_elapsed = r2.elapsed
|
|
829
|
+
if r2.ok:
|
|
830
|
+
m = _NITPICK_CTEX_RE.search(r2.output)
|
|
831
|
+
return FolVerdict(status=INVALID,
|
|
832
|
+
countermodel=(m.group(0).strip() if m else None),
|
|
833
|
+
prove_output=r1.output, refute_output=refute_output,
|
|
834
|
+
prove_elapsed=r1.elapsed, refute_elapsed=refute_elapsed)
|
|
835
|
+
|
|
836
|
+
return FolVerdict(status=UNKNOWN, prove_output=r1.output,
|
|
837
|
+
refute_output=refute_output, prove_elapsed=r1.elapsed,
|
|
838
|
+
refute_elapsed=refute_elapsed,
|
|
839
|
+
infra_error=_infra_error(r1.output, refute_output))
|
|
840
|
+
|
|
841
|
+
|
|
842
|
+
# --------------------------------------------------------------------------- #
|
|
843
|
+
# Deciding counterfactual (Lewis sphere) validity through Isabelle.
|
|
844
|
+
# --------------------------------------------------------------------------- #
|
|
845
|
+
|
|
846
|
+
def isabelle_decide_counterfactual(
|
|
847
|
+
formula: Node, *,
|
|
848
|
+
centering: str = "weak",
|
|
849
|
+
methods: Optional[Sequence[str]] = None,
|
|
850
|
+
refute: bool = True,
|
|
851
|
+
card: str = "1-3",
|
|
852
|
+
prove_timeout: int = 60,
|
|
853
|
+
refute_timeout: int = 60,
|
|
854
|
+
install: Optional[IsabelleInstall] = None,
|
|
855
|
+
) -> FolVerdict:
|
|
856
|
+
"""Decide a counterfactual formula's validity by running a local Isabelle.
|
|
857
|
+
|
|
858
|
+
Emits the Lewis sphere embedding
|
|
859
|
+
(:func:`~unicode_logic_kit.hol.isabelle_conditional.isabelle_conditional_theory`,
|
|
860
|
+
the same truth condition
|
|
861
|
+
:func:`~unicode_logic_kit.semantics.conditional.cf_satisfies` evaluates) and,
|
|
862
|
+
exactly like :func:`isabelle_decide_fol`:
|
|
863
|
+
|
|
864
|
+
1. tries a proof battery — exit 0 ⇒ :data:`VALID` over every **nested** sphere
|
|
865
|
+
system meeting the requested ``centering`` level;
|
|
866
|
+
2. otherwise (``refute``) runs ``nitpick[expect = genuine]`` over the world
|
|
867
|
+
type ``w`` — exit 0 ⇒ :data:`INVALID` (nesting and centering are *premises*
|
|
868
|
+
of the goal, so nitpick constructs the sphere system itself and certifies
|
|
869
|
+
the model as genuine);
|
|
870
|
+
3. otherwise :data:`UNKNOWN`.
|
|
871
|
+
|
|
872
|
+
Args:
|
|
873
|
+
formula: the AST node — the propositional connectives plus ``□→`` / ``◇→``
|
|
874
|
+
(``Would`` / ``Might``).
|
|
875
|
+
centering: which sphere class to decide over — ``"none"`` (Lewis **V**,
|
|
876
|
+
nesting only) / ``"weak"`` (**VW**, the default) / ``"strong"``
|
|
877
|
+
(**VC**). The default matches
|
|
878
|
+
:func:`~unicode_logic_kit.semantics.conditional.cf_valid`'s, so this
|
|
879
|
+
route and the internal evaluator answer the same question unless told
|
|
880
|
+
otherwise. It is threaded to **all three** emission sites — the prove
|
|
881
|
+
theory, the proof battery's ``unfolding`` list, and the nitpick theory:
|
|
882
|
+
a partial forward would silently decide a different logic than
|
|
883
|
+
requested (a premise the battery cannot unfold degrades to UNKNOWN, and
|
|
884
|
+
a nitpick theory carrying the wrong premise can certify a counter-model
|
|
885
|
+
that is not in the requested class at all).
|
|
886
|
+
methods: proof battery; defaults to the embedding's own
|
|
887
|
+
:data:`~unicode_logic_kit.hol.isabelle_conditional.DEFAULT_METHODS`
|
|
888
|
+
(verit-first — see there for why the order matters). Built at
|
|
889
|
+
``centering`` here, so a caller-supplied battery need not repeat it.
|
|
890
|
+
refute / card / prove_timeout / refute_timeout / install: as for
|
|
891
|
+
:func:`isabelle_decide_fol` (``card`` bounds the world type ``w``).
|
|
892
|
+
|
|
893
|
+
Note:
|
|
894
|
+
``card`` here and ``max_worlds`` in
|
|
895
|
+
:func:`~unicode_logic_kit.semantics.conditional.cf_countermodel` bound
|
|
896
|
+
different searches (nitpick's finite-model search over the world type vs.
|
|
897
|
+
the Python enumeration of sphere chains), so equal numbers do not mean
|
|
898
|
+
equal coverage. The default *numbers* do coincide at the centered levels —
|
|
899
|
+
``card="1-3"`` here against
|
|
900
|
+
:data:`~unicode_logic_kit.semantics.conditional.DEFAULT_MAX_WORLDS`'s 3 —
|
|
901
|
+
which is why the schemas whose only countermodels have three worlds
|
|
902
|
+
(conditional excluded middle at VC, importation at VW) are now refuted by
|
|
903
|
+
both routes rather than only this one.
|
|
904
|
+
|
|
905
|
+
Raises:
|
|
906
|
+
IsabelleNotAvailable: if no Isabelle installation can be located.
|
|
907
|
+
ValueError: on an unknown ``centering`` level — checked *before* the
|
|
908
|
+
install lookup, so a typo is reported as a typo on a machine with no
|
|
909
|
+
Isabelle rather than masked by ``IsabelleNotAvailable``.
|
|
910
|
+
NotImplementedError: propagated from the emitter on a quantifier or a
|
|
911
|
+
modal operator (□/◇ range over an accessibility relation, not a
|
|
912
|
+
similarity ordering).
|
|
913
|
+
"""
|
|
914
|
+
from unicode_logic_kit.hol.isabelle_conditional import (
|
|
915
|
+
isabelle_conditional_theory, battery_proof, nitpick_proof,
|
|
916
|
+
DEFAULT_METHODS as _CF_METHODS)
|
|
917
|
+
from unicode_logic_kit.semantics.conditional import check_centering
|
|
918
|
+
|
|
919
|
+
check_centering(centering)
|
|
920
|
+
install = install or find_isabelle()
|
|
921
|
+
if install is None:
|
|
922
|
+
raise IsabelleNotAvailable(
|
|
923
|
+
"No Isabelle installation found. Set UFK_ISABELLE_HOME (or ISABELLE_HOME) "
|
|
924
|
+
"to the install directory, or put `isabelle` on PATH.")
|
|
925
|
+
|
|
926
|
+
# --- step 1: prove ---------------------------------------------------- #
|
|
927
|
+
tok = "G" + uuid.uuid4().hex[:8]
|
|
928
|
+
prove_thy = isabelle_conditional_theory(
|
|
929
|
+
formula, theory_name=tok, centering=centering,
|
|
930
|
+
proof=battery_proof(methods if methods is not None else _CF_METHODS,
|
|
931
|
+
centering=centering))
|
|
932
|
+
r1 = check_theory(prove_thy, tok, install=install, session_timeout=prove_timeout)
|
|
933
|
+
if r1.ok:
|
|
934
|
+
return FolVerdict(status=VALID, method="prove-battery",
|
|
935
|
+
prove_output=r1.output, prove_elapsed=r1.elapsed)
|
|
936
|
+
|
|
937
|
+
# --- step 2: refute (nitpick) ----------------------------------------- #
|
|
938
|
+
refute_output = ""
|
|
939
|
+
refute_elapsed = 0.0
|
|
940
|
+
if refute:
|
|
941
|
+
tok2 = "G" + uuid.uuid4().hex[:8]
|
|
942
|
+
nit_thy = isabelle_conditional_theory(
|
|
943
|
+
formula, theory_name=tok2, centering=centering,
|
|
944
|
+
proof=nitpick_proof(card=card, timeout=refute_timeout))
|
|
945
|
+
r2 = check_theory(nit_thy, tok2, install=install,
|
|
946
|
+
session_timeout=refute_timeout + 30,
|
|
947
|
+
wall_timeout=float(refute_timeout) + 240.0)
|
|
948
|
+
refute_output = r2.output
|
|
949
|
+
refute_elapsed = r2.elapsed
|
|
950
|
+
if r2.ok:
|
|
951
|
+
m = _NITPICK_CTEX_RE.search(r2.output)
|
|
952
|
+
return FolVerdict(status=INVALID,
|
|
953
|
+
countermodel=(m.group(0).strip() if m else None),
|
|
954
|
+
prove_output=r1.output, refute_output=refute_output,
|
|
955
|
+
prove_elapsed=r1.elapsed, refute_elapsed=refute_elapsed)
|
|
956
|
+
|
|
957
|
+
return FolVerdict(status=UNKNOWN, prove_output=r1.output,
|
|
958
|
+
refute_output=refute_output, prove_elapsed=r1.elapsed,
|
|
959
|
+
refute_elapsed=refute_elapsed,
|
|
960
|
+
infra_error=_infra_error(r1.output, refute_output))
|
|
961
|
+
|
|
962
|
+
|
|
963
|
+
# --------------------------------------------------------------------------- #
|
|
964
|
+
# Deciding relevant logic B (simplified Routley-Meyer) validity through Isabelle.
|
|
965
|
+
# --------------------------------------------------------------------------- #
|
|
966
|
+
|
|
967
|
+
def isabelle_decide_relevant(
|
|
968
|
+
formula: Node, *,
|
|
969
|
+
methods: Optional[Sequence[str]] = None,
|
|
970
|
+
refute: bool = True,
|
|
971
|
+
card: str = "1-3",
|
|
972
|
+
prove_timeout: int = 60,
|
|
973
|
+
refute_timeout: int = 60,
|
|
974
|
+
install: Optional[IsabelleInstall] = None,
|
|
975
|
+
) -> FolVerdict:
|
|
976
|
+
"""Decide a relevant-logic-B formula's validity by running a local Isabelle.
|
|
977
|
+
|
|
978
|
+
Emits the simplified Routley-Meyer shallow embedding
|
|
979
|
+
(:func:`~unicode_logic_kit.hol.isabelle_relevant.to_isabelle_relevant`, the same
|
|
980
|
+
truth conditions
|
|
981
|
+
:func:`~unicode_logic_kit.semantics.relevant.rel_satisfies` evaluates) and,
|
|
982
|
+
exactly like :func:`isabelle_decide_counterfactual`:
|
|
983
|
+
|
|
984
|
+
1. tries a proof battery -- exit 0 => :data:`VALID` over every interpretation
|
|
985
|
+
satisfying the ``wellformed`` frame conditions (N nonempty, ``star`` a
|
|
986
|
+
total involution, ``R`` sourced only at non-normal worlds);
|
|
987
|
+
2. otherwise (``refute``) runs ``nitpick[expect = genuine]`` over the world
|
|
988
|
+
type ``w`` -- exit 0 => :data:`INVALID` (``wellformed`` is a *premise* of
|
|
989
|
+
the goal, so nitpick constructs ``N``/``star``/``R`` itself and certifies
|
|
990
|
+
the model as genuine);
|
|
991
|
+
3. otherwise :data:`UNKNOWN`.
|
|
992
|
+
|
|
993
|
+
Args:
|
|
994
|
+
formula: the AST node -- the propositional connectives
|
|
995
|
+
not/and/or/implies/iff over nullary atoms (no quantifiers,
|
|
996
|
+
modalities, lambda terms, or ``Xor`` -- there is no B reading for
|
|
997
|
+
them, matching
|
|
998
|
+
:func:`unicode_logic_kit.semantics.relevant._reject_non_propositional`).
|
|
999
|
+
methods: proof battery; defaults to the embedding's own
|
|
1000
|
+
:data:`~unicode_logic_kit.hol.isabelle_relevant.DEFAULT_METHODS`.
|
|
1001
|
+
refute / card / prove_timeout / refute_timeout / install: as for
|
|
1002
|
+
:func:`isabelle_decide_counterfactual` (``card`` bounds the world
|
|
1003
|
+
type ``w``).
|
|
1004
|
+
|
|
1005
|
+
Raises:
|
|
1006
|
+
IsabelleNotAvailable: if no Isabelle installation can be located.
|
|
1007
|
+
TypeError: propagated from the emitter on a quantifier, modality, lambda
|
|
1008
|
+
term, ``Xor``, or non-nullary atom.
|
|
1009
|
+
"""
|
|
1010
|
+
install = install or find_isabelle()
|
|
1011
|
+
if install is None:
|
|
1012
|
+
raise IsabelleNotAvailable(
|
|
1013
|
+
"No Isabelle installation found. Set UFK_ISABELLE_HOME (or ISABELLE_HOME) "
|
|
1014
|
+
"to the install directory, or put `isabelle` on PATH.")
|
|
1015
|
+
from unicode_logic_kit.hol.isabelle_relevant import (
|
|
1016
|
+
to_isabelle_relevant, battery_proof, nitpick_proof,
|
|
1017
|
+
DEFAULT_METHODS as _REL_METHODS)
|
|
1018
|
+
|
|
1019
|
+
# --- step 1: prove ---------------------------------------------------- #
|
|
1020
|
+
tok = "G" + uuid.uuid4().hex[:8]
|
|
1021
|
+
prove_thy = to_isabelle_relevant(
|
|
1022
|
+
formula, theory_name=tok,
|
|
1023
|
+
proof=battery_proof(methods if methods is not None else _REL_METHODS))
|
|
1024
|
+
r1 = check_theory(prove_thy, tok, install=install, session_timeout=prove_timeout)
|
|
1025
|
+
if r1.ok:
|
|
1026
|
+
return FolVerdict(status=VALID, method="prove-battery",
|
|
1027
|
+
prove_output=r1.output, prove_elapsed=r1.elapsed)
|
|
1028
|
+
|
|
1029
|
+
# --- step 2: refute (nitpick) ----------------------------------------- #
|
|
1030
|
+
refute_output = ""
|
|
1031
|
+
refute_elapsed = 0.0
|
|
1032
|
+
if refute:
|
|
1033
|
+
tok2 = "G" + uuid.uuid4().hex[:8]
|
|
1034
|
+
nit_thy = to_isabelle_relevant(
|
|
1035
|
+
formula, theory_name=tok2,
|
|
1036
|
+
proof=nitpick_proof(card=card, timeout=refute_timeout))
|
|
1037
|
+
r2 = check_theory(nit_thy, tok2, install=install,
|
|
1038
|
+
session_timeout=refute_timeout + 30,
|
|
1039
|
+
wall_timeout=float(refute_timeout) + 240.0)
|
|
1040
|
+
refute_output = r2.output
|
|
1041
|
+
refute_elapsed = r2.elapsed
|
|
1042
|
+
if r2.ok:
|
|
1043
|
+
m = _NITPICK_CTEX_RE.search(r2.output)
|
|
1044
|
+
return FolVerdict(status=INVALID,
|
|
1045
|
+
countermodel=(m.group(0).strip() if m else None),
|
|
1046
|
+
prove_output=r1.output, refute_output=refute_output,
|
|
1047
|
+
prove_elapsed=r1.elapsed, refute_elapsed=refute_elapsed)
|
|
1048
|
+
|
|
1049
|
+
return FolVerdict(status=UNKNOWN, prove_output=r1.output,
|
|
1050
|
+
refute_output=refute_output, prove_elapsed=r1.elapsed,
|
|
1051
|
+
refute_elapsed=refute_elapsed,
|
|
1052
|
+
infra_error=_infra_error(r1.output, refute_output))
|
|
1053
|
+
|
|
1054
|
+
|
|
1055
|
+
# --------------------------------------------------------------------------- #
|
|
1056
|
+
# Deciding free logic (D/E! guard embedding) validity through Isabelle.
|
|
1057
|
+
# --------------------------------------------------------------------------- #
|
|
1058
|
+
|
|
1059
|
+
def isabelle_decide_free(
|
|
1060
|
+
formula: Node, *,
|
|
1061
|
+
policy: str = "negative",
|
|
1062
|
+
methods: Sequence[str] = DEFAULT_METHODS,
|
|
1063
|
+
refute: bool = True,
|
|
1064
|
+
card: str = "1-4",
|
|
1065
|
+
prove_timeout: int = 60,
|
|
1066
|
+
refute_timeout: int = 60,
|
|
1067
|
+
install: Optional[IsabelleInstall] = None,
|
|
1068
|
+
) -> FolVerdict:
|
|
1069
|
+
"""Decide a free-logic formula's validity by running a local Isabelle.
|
|
1070
|
+
|
|
1071
|
+
Emits the ``D``/``E!`` guard embedding
|
|
1072
|
+
(:func:`~unicode_logic_kit.hol.free.free_theory`, the same truth condition
|
|
1073
|
+
:func:`~unicode_logic_kit.semantics.free_logic.free_holds` computes for
|
|
1074
|
+
``policy`` — see that module's docstring) and, exactly like
|
|
1075
|
+
:func:`isabelle_decide_fol`:
|
|
1076
|
+
|
|
1077
|
+
1. tries a proof battery -- exit 0 => :data:`VALID`;
|
|
1078
|
+
2. otherwise (``refute``) runs ``nitpick[expect = genuine]`` over the
|
|
1079
|
+
individual type ``e`` -- exit 0 => :data:`INVALID`. The ``E! -> D`` tie
|
|
1080
|
+
is a PREMISE of the goal, not ``axiomatization`` (see ``hol.free``'s
|
|
1081
|
+
module docstring), so nitpick constructs ``D``/``E!`` itself and can
|
|
1082
|
+
certify a genuine countermodel. ``=`` is HOL identity under the
|
|
1083
|
+
denotation guard and the inner domain may be empty, so both verdicts
|
|
1084
|
+
are verdicts about :func:`~unicode_logic_kit.semantics.free_logic.free_is_valid`'s
|
|
1085
|
+
own semantics (``domain_split="any"``);
|
|
1086
|
+
3. otherwise :data:`UNKNOWN` (expected — first-order free logic is only
|
|
1087
|
+
semi-decidable, same as classical FOL).
|
|
1088
|
+
|
|
1089
|
+
Args:
|
|
1090
|
+
formula: the free-logic AST node.
|
|
1091
|
+
policy: ``"negative"`` (default) or ``"positive"``. ``card`` bounds the
|
|
1092
|
+
individual type ``e``, exactly like ``card i`` for
|
|
1093
|
+
:func:`isabelle_decide_fol`.
|
|
1094
|
+
methods / refute / card / prove_timeout / refute_timeout / install: as
|
|
1095
|
+
for :func:`isabelle_decide_fol`.
|
|
1096
|
+
|
|
1097
|
+
Raises:
|
|
1098
|
+
IsabelleNotAvailable: if no Isabelle installation can be located.
|
|
1099
|
+
NotImplementedError: propagated from the emitter for
|
|
1100
|
+
``policy="supervaluation"`` (see ``hol.free``'s module docstring —
|
|
1101
|
+
checked BEFORE the install lookup, so this is reported even on a
|
|
1102
|
+
machine with no Isabelle).
|
|
1103
|
+
ValueError: propagated from the emitter for any other unknown
|
|
1104
|
+
``policy``.
|
|
1105
|
+
"""
|
|
1106
|
+
from unicode_logic_kit.hol.free import _check_policy, free_theory
|
|
1107
|
+
_check_policy(policy, "isabelle_decide_free")
|
|
1108
|
+
|
|
1109
|
+
install = install or find_isabelle()
|
|
1110
|
+
if install is None:
|
|
1111
|
+
raise IsabelleNotAvailable(
|
|
1112
|
+
"No Isabelle installation found. Set UFK_ISABELLE_HOME (or ISABELLE_HOME) "
|
|
1113
|
+
"to the install directory, or put `isabelle` on PATH.")
|
|
1114
|
+
|
|
1115
|
+
# --- step 1: prove ---------------------------------------------------- #
|
|
1116
|
+
tok = "G" + uuid.uuid4().hex[:8]
|
|
1117
|
+
prove_proof = _battery_proof([], methods).lstrip() # no `using` facts needed
|
|
1118
|
+
prove_thy = free_theory(formula, policy=policy, theory_name=tok, proof=prove_proof)
|
|
1119
|
+
r1 = check_theory(prove_thy, tok, install=install, session_timeout=prove_timeout)
|
|
1120
|
+
if r1.ok:
|
|
1121
|
+
return FolVerdict(status=VALID, method="prove-battery",
|
|
1122
|
+
prove_output=r1.output, prove_elapsed=r1.elapsed)
|
|
1123
|
+
|
|
1124
|
+
# --- step 2: refute (nitpick) ----------------------------------------- #
|
|
1125
|
+
refute_output = ""
|
|
1126
|
+
refute_elapsed = 0.0
|
|
1127
|
+
if refute:
|
|
1128
|
+
tok2 = "G" + uuid.uuid4().hex[:8]
|
|
1129
|
+
nit_proof = (f"nitpick[card e = {card}, timeout = {int(refute_timeout)}, "
|
|
1130
|
+
f"expect = genuine]\n oops")
|
|
1131
|
+
nit_thy = free_theory(formula, policy=policy, theory_name=tok2, proof=nit_proof)
|
|
1132
|
+
r2 = check_theory(nit_thy, tok2, install=install,
|
|
1133
|
+
session_timeout=refute_timeout + 30,
|
|
1134
|
+
wall_timeout=float(refute_timeout) + 240.0)
|
|
1135
|
+
refute_output = r2.output
|
|
1136
|
+
refute_elapsed = r2.elapsed
|
|
1137
|
+
if r2.ok:
|
|
1138
|
+
m = _NITPICK_CTEX_RE.search(r2.output)
|
|
1139
|
+
return FolVerdict(status=INVALID,
|
|
1140
|
+
countermodel=(m.group(0).strip() if m else None),
|
|
1141
|
+
prove_output=r1.output, refute_output=refute_output,
|
|
1142
|
+
prove_elapsed=r1.elapsed, refute_elapsed=refute_elapsed)
|
|
1143
|
+
|
|
1144
|
+
return FolVerdict(status=UNKNOWN, prove_output=r1.output,
|
|
1145
|
+
refute_output=refute_output, prove_elapsed=r1.elapsed,
|
|
1146
|
+
refute_elapsed=refute_elapsed,
|
|
1147
|
+
infra_error=_infra_error(r1.output, refute_output))
|