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.
Files changed (237) hide show
  1. unicode_logic_kit/__init__.py +385 -0
  2. unicode_logic_kit/__main__.py +520 -0
  3. unicode_logic_kit/_deadline.py +219 -0
  4. unicode_logic_kit/ace/__init__.py +126 -0
  5. unicode_logic_kit/ace/_align.py +135 -0
  6. unicode_logic_kit/ace/chem_lexicon.py +128 -0
  7. unicode_logic_kit/ace/drs_reader.py +570 -0
  8. unicode_logic_kit/ace/mapping.py +666 -0
  9. unicode_logic_kit/ace/reverse_modal.py +138 -0
  10. unicode_logic_kit/ace/runner.py +551 -0
  11. unicode_logic_kit/ace/translate.py +452 -0
  12. unicode_logic_kit/ace/verbalize.py +1070 -0
  13. unicode_logic_kit/api.py +1284 -0
  14. unicode_logic_kit/atp/__init__.py +177 -0
  15. unicode_logic_kit/atp/_ascii_names.py +113 -0
  16. unicode_logic_kit/atp/_html.py +72 -0
  17. unicode_logic_kit/atp/_substructural_input.py +228 -0
  18. unicode_logic_kit/atp/_tff_problem.py +715 -0
  19. unicode_logic_kit/atp/_tptp_problem.py +1111 -0
  20. unicode_logic_kit/atp/_writer_support.py +289 -0
  21. unicode_logic_kit/atp/clingo_backend.py +1180 -0
  22. unicode_logic_kit/atp/cvc5_backend.py +1385 -0
  23. unicode_logic_kit/atp/eprover_backend.py +732 -0
  24. unicode_logic_kit/atp/finite_domain.py +1055 -0
  25. unicode_logic_kit/atp/fitch.py +1547 -0
  26. unicode_logic_kit/atp/fitch_search.py +551 -0
  27. unicode_logic_kit/atp/hets_backend.py +339 -0
  28. unicode_logic_kit/atp/hybrid_down.py +120 -0
  29. unicode_logic_kit/atp/incremental.py +250 -0
  30. unicode_logic_kit/atp/kripke_enum.py +741 -0
  31. unicode_logic_kit/atp/lambek.py +436 -0
  32. unicode_logic_kit/atp/leo3_backend.py +332 -0
  33. unicode_logic_kit/atp/linear.py +738 -0
  34. unicode_logic_kit/atp/lj.py +705 -0
  35. unicode_logic_kit/atp/logic_backends.py +566 -0
  36. unicode_logic_kit/atp/ltl_tableau.py +1084 -0
  37. unicode_logic_kit/atp/minizinc_backend.py +1402 -0
  38. unicode_logic_kit/atp/modal_tableau.py +1382 -0
  39. unicode_logic_kit/atp/nanocop_backend.py +410 -0
  40. unicode_logic_kit/atp/portfolio.py +489 -0
  41. unicode_logic_kit/atp/protocol.py +1803 -0
  42. unicode_logic_kit/atp/prover9_entailment.py +1153 -0
  43. unicode_logic_kit/atp/resolution.py +1376 -0
  44. unicode_logic_kit/atp/resolution_check.py +1114 -0
  45. unicode_logic_kit/atp/sequent.py +1050 -0
  46. unicode_logic_kit/atp/tableau.py +921 -0
  47. unicode_logic_kit/atp/tableau_check.py +543 -0
  48. unicode_logic_kit/atp/tptp_ncl.py +811 -0
  49. unicode_logic_kit/atp/tptp_tff.py +1546 -0
  50. unicode_logic_kit/atp/tstp.py +1333 -0
  51. unicode_logic_kit/atp/tstp_check.py +1096 -0
  52. unicode_logic_kit/atp/twee_backend.py +236 -0
  53. unicode_logic_kit/atp/twee_check.py +711 -0
  54. unicode_logic_kit/atp/twee_entailment.py +953 -0
  55. unicode_logic_kit/atp/vampire_entailment.py +540 -0
  56. unicode_logic_kit/atp/z3_arith.py +470 -0
  57. unicode_logic_kit/atp/z3_equivalence.py +36 -0
  58. unicode_logic_kit/atp/z3_fuzzy.py +362 -0
  59. unicode_logic_kit/atp/z3_input.py +500 -0
  60. unicode_logic_kit/atp/z3_models.py +208 -0
  61. unicode_logic_kit/chem/__init__.py +88 -0
  62. unicode_logic_kit/chem/_naming.py +284 -0
  63. unicode_logic_kit/chem/cache.py +185 -0
  64. unicode_logic_kit/chem/interop.py +244 -0
  65. unicode_logic_kit/chem/mol.py +525 -0
  66. unicode_logic_kit/chem/signature.py +112 -0
  67. unicode_logic_kit/comorphism.py +497 -0
  68. unicode_logic_kit/dl/__init__.py +384 -0
  69. unicode_logic_kit/dl/classification.py +227 -0
  70. unicode_logic_kit/dl/concepts.py +632 -0
  71. unicode_logic_kit/dl/datatypes.py +818 -0
  72. unicode_logic_kit/dl/owl_functional.py +2433 -0
  73. unicode_logic_kit/dl/owl_manchester.py +1637 -0
  74. unicode_logic_kit/dl/owl_reasoner.py +790 -0
  75. unicode_logic_kit/dl/parser.py +391 -0
  76. unicode_logic_kit/dl/tableau.py +4048 -0
  77. unicode_logic_kit/dl/translate.py +2704 -0
  78. unicode_logic_kit/drt/__init__.py +94 -0
  79. unicode_logic_kit/drt/export.py +179 -0
  80. unicode_logic_kit/drt/nodes.py +506 -0
  81. unicode_logic_kit/drt/parser.py +965 -0
  82. unicode_logic_kit/drt/resolve.py +195 -0
  83. unicode_logic_kit/drt/reverse.py +175 -0
  84. unicode_logic_kit/eval/__init__.py +106 -0
  85. unicode_logic_kit/eval/batch.py +382 -0
  86. unicode_logic_kit/eval/canonical.py +663 -0
  87. unicode_logic_kit/eval/chem_batch.py +606 -0
  88. unicode_logic_kit/eval/converses.py +200 -0
  89. unicode_logic_kit/eval/datasets/__init__.py +136 -0
  90. unicode_logic_kit/eval/datasets/_base.py +263 -0
  91. unicode_logic_kit/eval/datasets/_proofwriter_proof.py +422 -0
  92. unicode_logic_kit/eval/datasets/c3po.py +678 -0
  93. unicode_logic_kit/eval/datasets/folio.py +158 -0
  94. unicode_logic_kit/eval/datasets/fracas.py +418 -0
  95. unicode_logic_kit/eval/datasets/groves.py +191 -0
  96. unicode_logic_kit/eval/datasets/logicbench.py +467 -0
  97. unicode_logic_kit/eval/datasets/logicnli.py +303 -0
  98. unicode_logic_kit/eval/datasets/malls.py +133 -0
  99. unicode_logic_kit/eval/datasets/pfolio.py +594 -0
  100. unicode_logic_kit/eval/datasets/pmb.py +242 -0
  101. unicode_logic_kit/eval/datasets/prontoqa.py +611 -0
  102. unicode_logic_kit/eval/datasets/proofwriter.py +1431 -0
  103. unicode_logic_kit/eval/datasets/proverqa.py +674 -0
  104. unicode_logic_kit/eval/datasets/willow.py +478 -0
  105. unicode_logic_kit/eval/equivalence.py +466 -0
  106. unicode_logic_kit/eval/exercise_gen.py +533 -0
  107. unicode_logic_kit/eval/explain.py +791 -0
  108. unicode_logic_kit/eval/generality.py +750 -0
  109. unicode_logic_kit/eval/metric_hf.py +458 -0
  110. unicode_logic_kit/eval/predicate_match.py +343 -0
  111. unicode_logic_kit/eval/theory_check.py +1170 -0
  112. unicode_logic_kit/eval/validate.py +306 -0
  113. unicode_logic_kit/fol/__init__.py +177 -0
  114. unicode_logic_kit/fol/_atom_keys.py +510 -0
  115. unicode_logic_kit/fol/_fol_nodes.py +3586 -0
  116. unicode_logic_kit/fol/_free_parameters.py +105 -0
  117. unicode_logic_kit/fol/_ho_nodes.py +448 -0
  118. unicode_logic_kit/fol/_hybrid_nodes.py +308 -0
  119. unicode_logic_kit/fol/_identifiers.py +1091 -0
  120. unicode_logic_kit/fol/_lambek_nodes.py +112 -0
  121. unicode_logic_kit/fol/_linear_nodes.py +352 -0
  122. unicode_logic_kit/fol/_modal_nodes.py +1467 -0
  123. unicode_logic_kit/fol/_msfl_nodes.py +2196 -0
  124. unicode_logic_kit/fol/_numeral_symbols.py +231 -0
  125. unicode_logic_kit/fol/_so_nodes.py +200 -0
  126. unicode_logic_kit/fol/_symbol_names.py +81 -0
  127. unicode_logic_kit/fol/_team_nodes.py +181 -0
  128. unicode_logic_kit/fol/_tptp_symbols.py +551 -0
  129. unicode_logic_kit/fol/_truth_constants.py +117 -0
  130. unicode_logic_kit/fol/casl_export.py +1135 -0
  131. unicode_logic_kit/fol/casl_import.py +929 -0
  132. unicode_logic_kit/fol/derivation.py +367 -0
  133. unicode_logic_kit/fol/dialect_detect.py +70 -0
  134. unicode_logic_kit/fol/dialect_repair.py +537 -0
  135. unicode_logic_kit/fol/frames.py +637 -0
  136. unicode_logic_kit/fol/grammars/terminals.lark +31 -0
  137. unicode_logic_kit/fol/lambda_tools.py +297 -0
  138. unicode_logic_kit/fol/latex_input.py +429 -0
  139. unicode_logic_kit/fol/modal_translation.py +944 -0
  140. unicode_logic_kit/fol/msflparser.py +1033 -0
  141. unicode_logic_kit/fol/naming.py +422 -0
  142. unicode_logic_kit/fol/nodes.py +241 -0
  143. unicode_logic_kit/fol/normalforms.py +492 -0
  144. unicode_logic_kit/fol/pal.py +287 -0
  145. unicode_logic_kit/fol/prolog_export.py +566 -0
  146. unicode_logic_kit/fol/prolog_input.py +505 -0
  147. unicode_logic_kit/fol/prover9_input.py +1325 -0
  148. unicode_logic_kit/fol/qml.py +1760 -0
  149. unicode_logic_kit/fol/qmltp_input.py +525 -0
  150. unicode_logic_kit/fol/sanitize.py +221 -0
  151. unicode_logic_kit/fol/serialize.py +79 -0
  152. unicode_logic_kit/fol/signature.py +1290 -0
  153. unicode_logic_kit/fol/simplify_check.py +544 -0
  154. unicode_logic_kit/fol/spans.py +594 -0
  155. unicode_logic_kit/fol/tptp_input.py +1503 -0
  156. unicode_logic_kit/fol/tptp_repair.py +941 -0
  157. unicode_logic_kit/fol/unification.py +157 -0
  158. unicode_logic_kit/fol/verbalize.py +263 -0
  159. unicode_logic_kit/hets/__init__.py +163 -0
  160. unicode_logic_kit/hets/bridge.py +142 -0
  161. unicode_logic_kit/hets/client.py +748 -0
  162. unicode_logic_kit/hets/docker.py +420 -0
  163. unicode_logic_kit/hets/dol.py +712 -0
  164. unicode_logic_kit/hets/haskell_json.py +355 -0
  165. unicode_logic_kit/hets/owl_backend.py +794 -0
  166. unicode_logic_kit/hets/owl_cli.py +598 -0
  167. unicode_logic_kit/hets/symbols.py +512 -0
  168. unicode_logic_kit/hol/__init__.py +140 -0
  169. unicode_logic_kit/hol/_ho_common.py +323 -0
  170. unicode_logic_kit/hol/_isabelle_binders.py +125 -0
  171. unicode_logic_kit/hol/classical.py +812 -0
  172. unicode_logic_kit/hol/deepshallow/__init__.py +45 -0
  173. unicode_logic_kit/hol/deepshallow/_common.py +177 -0
  174. unicode_logic_kit/hol/deepshallow/conditional.py +225 -0
  175. unicode_logic_kit/hol/deepshallow/intuitionistic.py +181 -0
  176. unicode_logic_kit/hol/deepshallow/modal.py +217 -0
  177. unicode_logic_kit/hol/deepshallow/qml.py +406 -0
  178. unicode_logic_kit/hol/deepshallow/relevant.py +206 -0
  179. unicode_logic_kit/hol/free.py +753 -0
  180. unicode_logic_kit/hol/goedel.py +336 -0
  181. unicode_logic_kit/hol/ho_modal.py +1743 -0
  182. unicode_logic_kit/hol/intuitionistic.py +403 -0
  183. unicode_logic_kit/hol/isabelle_conditional.py +593 -0
  184. unicode_logic_kit/hol/isabelle_modal.py +1908 -0
  185. unicode_logic_kit/hol/isabelle_relevant.py +412 -0
  186. unicode_logic_kit/hol/isabelle_runner.py +1147 -0
  187. unicode_logic_kit/hol/isabelle_substructural.py +884 -0
  188. unicode_logic_kit/hol/lean.py +1018 -0
  189. unicode_logic_kit/hol/manyvalued.py +921 -0
  190. unicode_logic_kit/hol/secondorder.py +687 -0
  191. unicode_logic_kit/hol/thf_modal.py +941 -0
  192. unicode_logic_kit/hol/thirdorder.py +397 -0
  193. unicode_logic_kit/ilp/__init__.py +89 -0
  194. unicode_logic_kit/ilp/readback.py +389 -0
  195. unicode_logic_kit/ilp/separation.py +153 -0
  196. unicode_logic_kit/ilp/task.py +730 -0
  197. unicode_logic_kit/logic.py +163 -0
  198. unicode_logic_kit/mcp/__init__.py +28 -0
  199. unicode_logic_kit/mcp/__main__.py +5 -0
  200. unicode_logic_kit/mcp/chem_tools.py +1031 -0
  201. unicode_logic_kit/mcp/server.py +2453 -0
  202. unicode_logic_kit/mcp/syntax_spec.py +681 -0
  203. unicode_logic_kit/prob/__init__.py +53 -0
  204. unicode_logic_kit/prob/_bdd.py +225 -0
  205. unicode_logic_kit/prob/_column_gen.py +668 -0
  206. unicode_logic_kit/prob/distribution.py +686 -0
  207. unicode_logic_kit/prob/nilsson.py +470 -0
  208. unicode_logic_kit/py.typed +0 -0
  209. unicode_logic_kit/semantics/__init__.py +137 -0
  210. unicode_logic_kit/semantics/_modal_reject.py +156 -0
  211. unicode_logic_kit/semantics/action_models.py +466 -0
  212. unicode_logic_kit/semantics/asp_models.py +1200 -0
  213. unicode_logic_kit/semantics/conditional.py +580 -0
  214. unicode_logic_kit/semantics/dynamic_epistemic.py +95 -0
  215. unicode_logic_kit/semantics/free_logic.py +913 -0
  216. unicode_logic_kit/semantics/fuzzy.py +384 -0
  217. unicode_logic_kit/semantics/fuzzy_kripke.py +442 -0
  218. unicode_logic_kit/semantics/intuitionistic.py +581 -0
  219. unicode_logic_kit/semantics/kripke.py +1139 -0
  220. unicode_logic_kit/semantics/manyvalued.py +580 -0
  221. unicode_logic_kit/semantics/matrix.py +342 -0
  222. unicode_logic_kit/semantics/model_eval.py +1135 -0
  223. unicode_logic_kit/semantics/modelfinder.py +1036 -0
  224. unicode_logic_kit/semantics/nonmonotonic.py +372 -0
  225. unicode_logic_kit/semantics/relevant.py +331 -0
  226. unicode_logic_kit/semantics/secondorder.py +657 -0
  227. unicode_logic_kit/semantics/structures.py +352 -0
  228. unicode_logic_kit/semantics/tarski.py +975 -0
  229. unicode_logic_kit/semantics/team.py +315 -0
  230. unicode_logic_kit/semantics/team_translation.py +416 -0
  231. unicode_logic_kit/semantics/thirdorder.py +358 -0
  232. unicode_logic_kit/semantics/tnorm.py +85 -0
  233. unicode_logic_kit/semantics/truthtable.py +201 -0
  234. unicode_logic_kit-0.31.0.dist-info/METADATA +333 -0
  235. unicode_logic_kit-0.31.0.dist-info/RECORD +237 -0
  236. unicode_logic_kit-0.31.0.dist-info/WHEEL +4 -0
  237. 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))