unicode-fol-kit 0.7.0__tar.gz → 0.8.0__tar.gz

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 (64) hide show
  1. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/CHANGELOG.md +73 -0
  2. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/PKG-INFO +29 -2
  3. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/README.md +28 -1
  4. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/pyproject.toml +1 -1
  5. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/unicode_fol_kit/__init__.py +9 -1
  6. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/unicode_fol_kit/hol/__init__.py +20 -4
  7. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/unicode_fol_kit/hol/intuitionistic.py +53 -8
  8. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/unicode_fol_kit/hol/isabelle_modal.py +117 -18
  9. unicode_fol_kit-0.8.0/unicode_fol_kit/hol/isabelle_runner.py +538 -0
  10. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/unicode_fol_kit/hol/manyvalued.py +72 -8
  11. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/.gitignore +0 -0
  12. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/LICENSE +0 -0
  13. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/unicode_fol_kit/__main__.py +0 -0
  14. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/unicode_fol_kit/atp/__init__.py +0 -0
  15. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/unicode_fol_kit/atp/fitch.py +0 -0
  16. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/unicode_fol_kit/atp/fitch_search.py +0 -0
  17. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/unicode_fol_kit/atp/lj.py +0 -0
  18. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/unicode_fol_kit/atp/prover9_entailment.py +0 -0
  19. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/unicode_fol_kit/atp/resolution.py +0 -0
  20. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/unicode_fol_kit/atp/sequent.py +0 -0
  21. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/unicode_fol_kit/atp/tableau.py +0 -0
  22. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/unicode_fol_kit/atp/vampire_entailment.py +0 -0
  23. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/unicode_fol_kit/atp/z3_arith.py +0 -0
  24. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/unicode_fol_kit/atp/z3_equivalence.py +0 -0
  25. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/unicode_fol_kit/atp/z3_fuzzy.py +0 -0
  26. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/unicode_fol_kit/atp/z3_input.py +0 -0
  27. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/unicode_fol_kit/atp/z3_models.py +0 -0
  28. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/unicode_fol_kit/eval/__init__.py +0 -0
  29. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/unicode_fol_kit/eval/canonical.py +0 -0
  30. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/unicode_fol_kit/eval/predicate_match.py +0 -0
  31. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/unicode_fol_kit/eval/validate.py +0 -0
  32. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/unicode_fol_kit/fol/__init__.py +0 -0
  33. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/unicode_fol_kit/fol/_fol_nodes.py +0 -0
  34. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/unicode_fol_kit/fol/_modal_nodes.py +0 -0
  35. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/unicode_fol_kit/fol/_msfl_nodes.py +0 -0
  36. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/unicode_fol_kit/fol/_so_nodes.py +0 -0
  37. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/unicode_fol_kit/fol/_symbol_names.py +0 -0
  38. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/unicode_fol_kit/fol/grammars/terminals.lark +0 -0
  39. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/unicode_fol_kit/fol/lambda_tools.py +0 -0
  40. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/unicode_fol_kit/fol/latex_input.py +0 -0
  41. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/unicode_fol_kit/fol/modal_translation.py +0 -0
  42. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/unicode_fol_kit/fol/msflparser.py +0 -0
  43. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/unicode_fol_kit/fol/naming.py +0 -0
  44. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/unicode_fol_kit/fol/nodes.py +0 -0
  45. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/unicode_fol_kit/fol/normalforms.py +0 -0
  46. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/unicode_fol_kit/fol/prover9_input.py +0 -0
  47. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/unicode_fol_kit/fol/qml.py +0 -0
  48. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/unicode_fol_kit/fol/tptp_input.py +0 -0
  49. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/unicode_fol_kit/fol/unification.py +0 -0
  50. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/unicode_fol_kit/fol/verbalize.py +0 -0
  51. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/unicode_fol_kit/hol/classical.py +0 -0
  52. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/unicode_fol_kit/hol/secondorder.py +0 -0
  53. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/unicode_fol_kit/hol/thf_modal.py +0 -0
  54. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/unicode_fol_kit/py.typed +0 -0
  55. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/unicode_fol_kit/semantics/__init__.py +0 -0
  56. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/unicode_fol_kit/semantics/_modal_reject.py +0 -0
  57. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/unicode_fol_kit/semantics/fuzzy.py +0 -0
  58. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/unicode_fol_kit/semantics/intuitionistic.py +0 -0
  59. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/unicode_fol_kit/semantics/kripke.py +0 -0
  60. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/unicode_fol_kit/semantics/manyvalued.py +0 -0
  61. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/unicode_fol_kit/semantics/modelfinder.py +0 -0
  62. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/unicode_fol_kit/semantics/secondorder.py +0 -0
  63. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/unicode_fol_kit/semantics/tarski.py +0 -0
  64. {unicode_fol_kit-0.7.0 → unicode_fol_kit-0.8.0}/unicode_fol_kit/semantics/truthtable.py +0 -0
@@ -5,6 +5,79 @@ loosely based on [Keep a Changelog](https://keepachangelog.com/). Versioning is
5
5
  semantic, but the project is pre-1.0 (alpha): a **minor** release may contain
6
6
  breaking changes.
7
7
 
8
+ ## [0.8.0] - 2026-06-28
9
+
10
+ ### Added
11
+
12
+ - **Run a local Isabelle to actually *prove* the modal embeddings — `unicode_fol_kit.hol.isabelle_runner`.**
13
+ The `hol` exporters only *emit*; this opt-in module turns "emit" into "proven / refuted" when an
14
+ Isabelle/HOL install is present. `isabelle_decide_modal(φ, frame=…, mode=…)` decides validity by a
15
+ two-step procedure, read off the build's exit code: (1) emit the lemma with a proof battery that
16
+ brings the frame/domain axioms into scope (`using … by (blast | force | … | meson … | metis …)`) — a
17
+ successful `isabelle build` means **VALID**; (2) otherwise emit `nitpick[expect = genuine]`, whose
18
+ build succeeds **iff** a genuine finite counter-model exists — that means **INVALID**; (3) otherwise
19
+ **UNKNOWN**. Sound (Isabelle's kernel certifies the proof; nitpick reports only genuine
20
+ counter-models) and, necessarily, incomplete. The verdict is validated *differentially* against an
21
+ independent brute-force Kripke oracle (`satisfies_modal`) across K/T/S4/S5 in the test suite.
22
+ - `find_isabelle` / `isabelle_available` locate an install (explicit path → `UFK_ISABELLE_HOME` /
23
+ `ISABELLE_HOME` → `isabelle` on `PATH` → a light scan); `check_theory(text, name)` builds any
24
+ self-contained theory; `ModalVerdict` / `BuildResult` / `IsabelleInstall` carry the results.
25
+ - **Linux/macOS is the primary path** — `isabelle` is invoked directly. **Windows** is also
26
+ supported: the build is additionally routed through Isabelle's bundled Cygwin (Windows→`/cygdrive`
27
+ path translation, launcher exec-bit fixup, `bin` on `PATH`). No install path is hard-coded —
28
+ installations are discovered generically. Absent Isabelle raises a clear `IsabelleNotAvailable`;
29
+ the live tests skip.
30
+ - All re-exported at the package top level.
31
+
32
+ ### Fixed
33
+
34
+ - **`to_isabelle_modal` emitted proofs could not discharge axiom-dependent validities.** A bare
35
+ `axiomatization where r_refl: …` fact is not in Isabelle's default claset, so `by blast` / `by auto`
36
+ / `by (metis …)` could not see it: every validity that *depends* on a frame/domain axiom (T, S4, S5,
37
+ KD, KD45, the temporal closure, a domain regime) failed to prove even though the formula is valid and
38
+ the theory sound (only the pure-K fragment, like the K axiom, went through). The `by`-style tactics
39
+ now emit `using <frame/domain axioms> by …`. New `modal_axiom_names(φ, …)` exposes the axioms in
40
+ scope, and `isabelle_modal_theory(…, proof=…)` accepts an explicit proof override.
41
+ - **`to_isabelle_k3lp` / `to_isabelle_k3lp_entailment` emitted a non-discharging proof.**
42
+ `by (simp add: des_def)` does **not** close the validity lemma — `simp` cannot reduce `kneg v` /
43
+ `kor v …` while the quantified truth-value `v` is abstract (e.g. the LP-valid `p ∨ ¬p` failed). The
44
+ `∀` (valid) form is now discharged by exhausting each variable's three `tv` constructors
45
+ (`case_tac` + `simp_all`); the `∃` (refutation) form by supplying the counter-valuation as `rule exI`
46
+ witnesses computed at emit time. Every form is verified to build in real Isabelle.
47
+
48
+ ### Changed
49
+
50
+ - **`to_isabelle_intuitionistic` now emits a real, Isabelle-checked proof for valid formulas.** The
51
+ proof is verdict-dependent: an intuitionistically valid formula gets `using r_refl r_trans by
52
+ (metis … | meson … | blast | auto)` that Isabelle discharges; a non-valid one is left `oops` (loads,
53
+ claims nothing — see `int_countermodel`). Previously the lemma was always `oops`. The verdict is
54
+ taken from the **decidable** S4 oracle `gmt_is_s4_valid` (Z3 on the GMT→S4 translation), *not* from
55
+ `int_valid`'s default 3-world bound — which is incomplete (IPL's finite-model bound grows with the
56
+ formula, so a non-theorem like `(p→q)∨(q→r)∨(r→p)` would otherwise be mis-emitted with a real
57
+ proof that cannot close).
58
+
59
+ ### Audit hardening
60
+
61
+ A multi-agent adversarial soundness audit of the new subsystem confirmed five issues (no false
62
+ *VALID* is possible — Isabelle's kernel rejects a proof of a false goal), all fixed and re-checked
63
+ against a live Isabelle:
64
+
65
+ - **Intuitionistic proof gated on the decidable oracle** (above) — was a real proof emitted for an
66
+ IPL-*invalid* formula (theory then failed to build).
67
+ - **Atom-name collisions in the intuitionistic export.** An atom named `r` (or `w`/`v`/`u`) collided
68
+ with the accessibility relation / frame-axiom variables, emitting a duplicate `consts r` (or an
69
+ ill-typed axiom) so the theory never loaded — even for the valid `r → r`. `_isa_atom_name` now
70
+ reserves the structural identifiers and de-collides (`r` → `p_r`).
71
+ - **Temporal closure now pinned faithfully.** When `Always`/`Eventually` co-occur with `Next`, the
72
+ emitted henceforth relation `t` is now forced to equal the reflexive-transitive closure of the
73
+ one-step `n` (`t ⊆ rtranclp n`, with the existing `t_refl`/`t_trans`/`n_in_t` giving the converse),
74
+ matching `satisfies_modal`. Previously `t` could be any refl-trans superset of `n`, so a
75
+ `satisfies_modal`-valid temporal induction `(p ∧ G(p→Xp)) → Gp` was spuriously refuted (a false
76
+ *INVALID*); it is now never refuted (UNKNOWN — the closure fragment is a documented approximation).
77
+ - **`ModalVerdict.infra_error`.** An `UNKNOWN` whose build failed for an infrastructure reason
78
+ (syntax / JVM / timeout / …) now carries a short signature, so a broken theory or environment is no
79
+ longer indistinguishable from honest incompleteness. Never changes the verdict.
80
+
8
81
  ## [0.7.0] - 2026-06-28
9
82
 
10
83
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: unicode-fol-kit
3
- Version: 0.7.0
3
+ Version: 0.8.0
4
4
  Summary: Parser and toolkit for first-order logic formulas using Unicode operators
5
5
  Project-URL: Repository, https://github.com/fvossel/unicode-fol-kit
6
6
  Project-URL: Issues, https://github.com/fvossel/unicode-fol-kit/issues
@@ -1095,7 +1095,7 @@ thf = to_thf_modal(BARCAN, mode="constant", frame="S5")
1095
1095
 
1096
1096
  ## Higher-order proving: Isabelle / THF exporters (the `hol` subpackage)
1097
1097
 
1098
- Beyond the first-order embedding above, `unicode_fol_kit.hol` emits **Benzmüller-style shallow embeddings into higher-order logic** for *all* the non-fuzzy logics — as complete, self-contained problem files for an external prover (Leo-III / Satallax on TPTP **THF**, or Isabelle/HOL theories for Sledgehammer). As everywhere in the kit it **emits**; it does not run a prover — and it cannot decide everything: first-order modal logic, FOL, and SOL are undecidable / not even semi-decidable, so a successful emission means *"here is a sound problem a prover may discharge"*, never *"decided"*. (The propositional fragments — K3/LP, modal K/T/S4/S5 — are decidable, but these exporters target the general case.)
1098
+ Beyond the first-order embedding above, `unicode_fol_kit.hol` emits **Benzmüller-style shallow embeddings into higher-order logic** for *all* the non-fuzzy logics — as complete, self-contained problem files for an external prover (Leo-III / Satallax on TPTP **THF**, or Isabelle/HOL theories for Sledgehammer). The exporters **emit**; they do not themselves run a prover — and cannot decide everything: first-order modal logic, FOL, and SOL are undecidable / not even semi-decidable, so a successful emission means *"here is a sound problem a prover may discharge"*, never *"decided"*. (The propositional fragments — K3/LP, modal K/T/S4/S5 — are decidable, but these exporters target the general case.) If you *do* have Isabelle installed, the opt-in [`isabelle_runner`](#actually-running-it-the-isabelle-runner) runs it for you and reads back a real verdict.
1099
1099
 
1100
1100
  ```python
1101
1101
  from unicode_fol_kit import MSFLParser
@@ -1120,6 +1120,33 @@ print(to_thf_modal_full(f)) # a complete TPTP THF problem; rk is AGENT-indexe
1120
1120
 
1121
1121
  Each embedding is faithful to its in-toolkit ground-truth oracle (`satisfies_modal`, `kleene_value`, `satisfies_so`, `int_valid`), verified by an adversarial differential audit. Equality `=` / `≠` is an **uninterpreted, world-relativized** predicate throughout (not primitive HOL identity), consistently across every exporter.
1122
1122
 
1123
+ ### Actually running it: the Isabelle runner
1124
+
1125
+ If a local **Isabelle/HOL** is installed, `unicode_fol_kit.hol.isabelle_runner` turns *emit* into *proven / refuted* — the toolkit writes the embedding to a scratch session, runs `isabelle build`, and reads the verdict off the build. It is **opt-in**: with no Isabelle present everything else still works and these calls raise a clear `IsabelleNotAvailable` (the live tests skip).
1126
+
1127
+ ```python
1128
+ # doctest: +SKIP — needs a local Isabelle install
1129
+ from unicode_fol_kit import MSFLParser, isabelle_available, isabelle_decide_modal
1130
+
1131
+ parse = MSFLParser(modal=True).parse
1132
+ print(isabelle_available()) # True if Isabelle was located
1133
+
1134
+ print(isabelle_decide_modal(parse("□P → P"), frame="K")) # ModalVerdict[invalid, frame=K, …]
1135
+ print(isabelle_decide_modal(parse("□P → P"), frame="T")) # ModalVerdict[valid (by prove-battery), …]
1136
+ print(isabelle_decide_modal(parse("□P → □□P"), frame="S4")) # ModalVerdict[valid …]
1137
+ ```
1138
+
1139
+ `isabelle_decide_modal` decides validity (for the chosen `frame` / `mode`) in two steps, read off the build's exit code:
1140
+
1141
+ 1. **Prove** — emit the lemma with a proof battery that brings the frame/domain axioms into scope (`using <axioms> by (blast | force | fastforce | auto | meson … | metis …)`). A successful `isabelle build` ⇒ **VALID**.
1142
+ 2. **Refute** — otherwise emit `nitpick[expect = genuine]`, whose build succeeds **iff** Isabelle finds a genuine finite counter-model ⇒ **INVALID**.
1143
+ 3. Otherwise ⇒ **UNKNOWN** (expected — first-order modal logic is undecidable).
1144
+
1145
+ This is **sound** (Isabelle's kernel certifies the proof; nitpick reports only *genuine* counter-models) and necessarily **incomplete**; `UNKNOWN` is a real outcome, not a failure. The verdict is validated *differentially* against an independent brute-force Kripke oracle (`satisfies_modal`) across K/T/S4/S5 in the test suite.
1146
+
1147
+ - **Locating Isabelle.** `find_isabelle()` looks at an explicit path, then `UFK_ISABELLE_HOME` / `ISABELLE_HOME`, then `isabelle` on `PATH`, then a light scan of standard install locations (no path is hard-coded); `isabelle_available()` is the cheap predicate. **Linux/macOS is the primary path** — `isabelle` is invoked directly; **Windows** is also supported, with the build routed through Isabelle's bundled Cygwin automatically (path translation + launcher exec-bit fixup).
1148
+ - **Any theory.** `check_theory(theory_text, theory_name)` builds an arbitrary self-contained theory and returns a `BuildResult` — used internally, and handy for the non-modal exporters (`to_isabelle_fol`, `to_isabelle_k3lp`, `to_isabelle_intuitionistic` …), whose emitted proofs are themselves built against real Isabelle in the test suite.
1149
+
1123
1150
  ## Many-valued logic (Kleene K3 / Priest LP)
1124
1151
 
1125
1152
  `kleene_value()` evaluates a classical formula over the three truth values **0 (false)**, **½ (undefined / both)**, and **1 (true)** using the strong Kleene tables (`¬x = 1−x`, `∧ = min`, `∨ = max`, `→ = max(1−x, y)`). Validity, satisfiability, and entailment are decided by enumeration under a chosen set of **designated** values — Kleene's **K3** designates `{1}`, Priest's paraconsistent **LP** designates `{½, 1}`:
@@ -1071,7 +1071,7 @@ thf = to_thf_modal(BARCAN, mode="constant", frame="S5")
1071
1071
 
1072
1072
  ## Higher-order proving: Isabelle / THF exporters (the `hol` subpackage)
1073
1073
 
1074
- Beyond the first-order embedding above, `unicode_fol_kit.hol` emits **Benzmüller-style shallow embeddings into higher-order logic** for *all* the non-fuzzy logics — as complete, self-contained problem files for an external prover (Leo-III / Satallax on TPTP **THF**, or Isabelle/HOL theories for Sledgehammer). As everywhere in the kit it **emits**; it does not run a prover — and it cannot decide everything: first-order modal logic, FOL, and SOL are undecidable / not even semi-decidable, so a successful emission means *"here is a sound problem a prover may discharge"*, never *"decided"*. (The propositional fragments — K3/LP, modal K/T/S4/S5 — are decidable, but these exporters target the general case.)
1074
+ Beyond the first-order embedding above, `unicode_fol_kit.hol` emits **Benzmüller-style shallow embeddings into higher-order logic** for *all* the non-fuzzy logics — as complete, self-contained problem files for an external prover (Leo-III / Satallax on TPTP **THF**, or Isabelle/HOL theories for Sledgehammer). The exporters **emit**; they do not themselves run a prover — and cannot decide everything: first-order modal logic, FOL, and SOL are undecidable / not even semi-decidable, so a successful emission means *"here is a sound problem a prover may discharge"*, never *"decided"*. (The propositional fragments — K3/LP, modal K/T/S4/S5 — are decidable, but these exporters target the general case.) If you *do* have Isabelle installed, the opt-in [`isabelle_runner`](#actually-running-it-the-isabelle-runner) runs it for you and reads back a real verdict.
1075
1075
 
1076
1076
  ```python
1077
1077
  from unicode_fol_kit import MSFLParser
@@ -1096,6 +1096,33 @@ print(to_thf_modal_full(f)) # a complete TPTP THF problem; rk is AGENT-indexe
1096
1096
 
1097
1097
  Each embedding is faithful to its in-toolkit ground-truth oracle (`satisfies_modal`, `kleene_value`, `satisfies_so`, `int_valid`), verified by an adversarial differential audit. Equality `=` / `≠` is an **uninterpreted, world-relativized** predicate throughout (not primitive HOL identity), consistently across every exporter.
1098
1098
 
1099
+ ### Actually running it: the Isabelle runner
1100
+
1101
+ If a local **Isabelle/HOL** is installed, `unicode_fol_kit.hol.isabelle_runner` turns *emit* into *proven / refuted* — the toolkit writes the embedding to a scratch session, runs `isabelle build`, and reads the verdict off the build. It is **opt-in**: with no Isabelle present everything else still works and these calls raise a clear `IsabelleNotAvailable` (the live tests skip).
1102
+
1103
+ ```python
1104
+ # doctest: +SKIP — needs a local Isabelle install
1105
+ from unicode_fol_kit import MSFLParser, isabelle_available, isabelle_decide_modal
1106
+
1107
+ parse = MSFLParser(modal=True).parse
1108
+ print(isabelle_available()) # True if Isabelle was located
1109
+
1110
+ print(isabelle_decide_modal(parse("□P → P"), frame="K")) # ModalVerdict[invalid, frame=K, …]
1111
+ print(isabelle_decide_modal(parse("□P → P"), frame="T")) # ModalVerdict[valid (by prove-battery), …]
1112
+ print(isabelle_decide_modal(parse("□P → □□P"), frame="S4")) # ModalVerdict[valid …]
1113
+ ```
1114
+
1115
+ `isabelle_decide_modal` decides validity (for the chosen `frame` / `mode`) in two steps, read off the build's exit code:
1116
+
1117
+ 1. **Prove** — emit the lemma with a proof battery that brings the frame/domain axioms into scope (`using <axioms> by (blast | force | fastforce | auto | meson … | metis …)`). A successful `isabelle build` ⇒ **VALID**.
1118
+ 2. **Refute** — otherwise emit `nitpick[expect = genuine]`, whose build succeeds **iff** Isabelle finds a genuine finite counter-model ⇒ **INVALID**.
1119
+ 3. Otherwise ⇒ **UNKNOWN** (expected — first-order modal logic is undecidable).
1120
+
1121
+ This is **sound** (Isabelle's kernel certifies the proof; nitpick reports only *genuine* counter-models) and necessarily **incomplete**; `UNKNOWN` is a real outcome, not a failure. The verdict is validated *differentially* against an independent brute-force Kripke oracle (`satisfies_modal`) across K/T/S4/S5 in the test suite.
1122
+
1123
+ - **Locating Isabelle.** `find_isabelle()` looks at an explicit path, then `UFK_ISABELLE_HOME` / `ISABELLE_HOME`, then `isabelle` on `PATH`, then a light scan of standard install locations (no path is hard-coded); `isabelle_available()` is the cheap predicate. **Linux/macOS is the primary path** — `isabelle` is invoked directly; **Windows** is also supported, with the build routed through Isabelle's bundled Cygwin automatically (path translation + launcher exec-bit fixup).
1124
+ - **Any theory.** `check_theory(theory_text, theory_name)` builds an arbitrary self-contained theory and returns a `BuildResult` — used internally, and handy for the non-modal exporters (`to_isabelle_fol`, `to_isabelle_k3lp`, `to_isabelle_intuitionistic` …), whose emitted proofs are themselves built against real Isabelle in the test suite.
1125
+
1099
1126
  ## Many-valued logic (Kleene K3 / Priest LP)
1100
1127
 
1101
1128
  `kleene_value()` evaluates a classical formula over the three truth values **0 (false)**, **½ (undefined / both)**, and **1 (true)** using the strong Kleene tables (`¬x = 1−x`, `∧ = min`, `∨ = max`, `→ = max(1−x, y)`). Validity, satisfiability, and entailment are decided by enumeration under a chosen set of **designated** values — Kleene's **K3** designates `{1}`, Priest's paraconsistent **LP** designates `{½, 1}`:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "unicode-fol-kit"
7
- version = "0.7.0"
7
+ version = "0.8.0"
8
8
  description = "Parser and toolkit for first-order logic formulas using Unicode operators"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -63,8 +63,13 @@ from .eval import (
63
63
  validate, is_wellformed, validate_text, ValidationReport,
64
64
  formulas_are_identical, match_predicates, formulas_are_matched_identical,
65
65
  )
66
+ from .hol import (
67
+ find_isabelle, isabelle_available, isabelle_decide_modal, check_theory,
68
+ IsabelleInstall, IsabelleNotAvailable, BuildResult, ModalVerdict,
69
+ modal_axiom_names,
70
+ )
66
71
 
67
- __version__ = "0.7.0"
72
+ __version__ = "0.8.0"
68
73
 
69
74
  __all__ = [
70
75
  "MSFLParser",
@@ -97,6 +102,9 @@ __all__ = [
97
102
  "parse_prover9",
98
103
  "qml_translate", "qml_is_valid", "qml_equivalent",
99
104
  "to_thf_modal", "to_isabelle_modal", "BARCAN", "CONVERSE_BARCAN",
105
+ "find_isabelle", "isabelle_available", "isabelle_decide_modal", "check_theory",
106
+ "IsabelleInstall", "IsabelleNotAvailable", "BuildResult", "ModalVerdict",
107
+ "modal_axiom_names",
100
108
  "from_z3", "parse_smtlib", "load_smtlib",
101
109
  "find_fitch_proof", "fitch_prove", "is_valid_fitch",
102
110
  "tableau_closed", "is_valid_tableau", "prove_tableau", "tableau_model",
@@ -3,9 +3,15 @@
3
3
  This subpackage *emits* Benzmüller-style shallow embeddings of the toolkit's non-fuzzy
4
4
  logics into higher-order logic — as complete, self-contained problem files for an
5
5
  external prover (Leo-III / Satallax on TPTP **THF**, or Isabelle/HOL theories for
6
- Sledgehammer). The toolkit emits; it does **not** run the prover. First-order modal
7
- logic, FOL and SOL are undecidable / not semi-decidable, so a successful emission means
8
- "here is a sound problem an external prover *may* discharge", never "decided".
6
+ Sledgehammer). The exporters only emit; a successful emission means "here is a sound
7
+ problem an external prover *may* discharge", never "decided" — first-order modal logic,
8
+ FOL and SOL are undecidable / not semi-decidable.
9
+
10
+ The optional :mod:`~unicode_fol_kit.hol.isabelle_runner` is the other half: if a local
11
+ Isabelle/HOL is installed, it actually *runs* it on the emitted modal theories
12
+ (prove-battery + nitpick) and reads a VALID / INVALID / UNKNOWN verdict off the build —
13
+ turning "emit" into "proven / refuted". It is sound (Isabelle's kernel certifies the
14
+ proof; nitpick reports only genuine counter-models) and, necessarily, incomplete.
9
15
 
10
16
  Modules:
11
17
 
@@ -24,7 +30,13 @@ Modules:
24
30
  via the Gödel–McKinsey–Tarski translation into S4 then the alethic SSE.
25
31
  """
26
32
 
27
- from .isabelle_modal import to_isabelle_modal, isabelle_modal_theory, ISABELLE_TACTICS
33
+ from .isabelle_modal import (
34
+ to_isabelle_modal, isabelle_modal_theory, ISABELLE_TACTICS, modal_axiom_names,
35
+ )
36
+ from .isabelle_runner import (
37
+ find_isabelle, isabelle_available, isabelle_decide_modal, check_theory,
38
+ IsabelleInstall, IsabelleNotAvailable, BuildResult, ModalVerdict, DEFAULT_METHODS,
39
+ )
28
40
  from .thf_modal import to_thf_modal_full, thf_full_definitions, thf_full_frame_axioms
29
41
  from .classical import to_thf_fol, to_isabelle_fol, to_thf_msfol, to_isabelle_msfol
30
42
  from .manyvalued import (
@@ -39,6 +51,10 @@ from .intuitionistic import (
39
51
 
40
52
  __all__ = [
41
53
  "to_isabelle_modal", "isabelle_modal_theory", "ISABELLE_TACTICS",
54
+ "modal_axiom_names",
55
+ "find_isabelle", "isabelle_available", "isabelle_decide_modal", "check_theory",
56
+ "IsabelleInstall", "IsabelleNotAvailable", "BuildResult", "ModalVerdict",
57
+ "DEFAULT_METHODS",
42
58
  "to_thf_modal_full", "thf_full_definitions", "thf_full_frame_axioms",
43
59
  "to_thf_fol", "to_isabelle_fol", "to_thf_msfol", "to_isabelle_msfol",
44
60
  "to_thf_k3lp", "to_isabelle_k3lp",
@@ -153,21 +153,40 @@ _ISA_BOX = "\\<^bold>\\<box>"
153
153
  # a distinct, valid lowercase identifier. (The THF path aliases these separately.)
154
154
  _ISA_ATOM_ALIASES = {"⊥": "bottom", "⊤": "top", "=": "feq", "≠": "fneq"}
155
155
 
156
+ # Identifiers the emitted theory already uses structurally: the world type ``i``, the
157
+ # accessibility relation ``r`` and the free variables ``w``/``v``/``u`` of its frame
158
+ # axioms (``r_refl: "r w w"``, ``r_trans: "r w v ⟹ r v u ⟹ r w u"``), and the lifted
159
+ # operator names. An atom predicate that sanitised to one of these would emit a SECOND
160
+ # ``consts`` with that name (a duplicate-constant clash that Isabelle rejects — so the
161
+ # theory would not load even for a valid formula, e.g. the atom ``r`` in ``r → r``), or
162
+ # make the frame axioms ill-typed (an atom ``w`` shadowing the type-``i`` bound var).
163
+ # A colliding atom is de-collided with the ``p_`` prefix (a trailing ``_`` would be a
164
+ # *bad* Isabelle identifier).
165
+ _ISA_RESERVED = frozenset({
166
+ "i", "r", "w", "v", "u",
167
+ "mnot", "mand", "mor", "mimp", "mbox", "mvalid",
168
+ })
169
+
156
170
 
157
171
  def _isa_atom_name(name: str) -> str:
158
172
  """A safe, *distinct* Isabelle constant name for an atom predicate.
159
173
 
160
174
  Distinct source atoms must map to distinct legal identifiers. Reserved/symbolic
161
175
  atoms (⊥, ⊤, =, ≠) are aliased to dedicated lowercase ids; everything else is
162
- sanitised char-by-char (alnum/underscore), and a ``p_`` prefix is prepended when
163
- the result is empty, is the bare reserved ``_`` token, or starts with ``_`` (all
164
- of which are illegal or reserved as a bare Isabelle constant name).
176
+ sanitised char-by-char (alnum/underscore), a ``p_`` prefix is prepended when the
177
+ result is empty, is the bare reserved ``_`` token, or starts with ``_`` (all of
178
+ which are illegal or reserved as a bare Isabelle constant name), and a name that
179
+ would collide with a structural identifier of the theory (the relation ``r``, an
180
+ axiom variable ``w``/``v``/``u``, the world type ``i``, a lifted operator) is
181
+ de-collided with the ``p_`` prefix (``r`` → ``p_r``).
165
182
  """
166
183
  if name in _ISA_ATOM_ALIASES:
167
184
  return _ISA_ATOM_ALIASES[name]
168
185
  safe = "".join(c if (c.isalnum() or c == "_") else "_" for c in name)
169
186
  if not safe or safe[0] == "_" or not safe[0].isalpha():
170
187
  safe = "p_" + safe
188
+ if safe in _ISA_RESERVED:
189
+ safe = "p_" + safe # p_r / p_w / … (a trailing '_' is a bad Isabelle id)
171
190
  return safe
172
191
 
173
192
 
@@ -209,9 +228,19 @@ def to_isabelle_intuitionistic(formula: Node, theory_name: str = "IPL_GMT") -> s
209
228
  theory: worlds as a type ``i``; an accessibility relation ``r`` fixed to be
210
229
  reflexive + transitive (the **S4** frame); the lifted modal operators as
211
230
  abbreviations; the atoms as ``consts``; and the embedded formula as a genuine
212
- ``lemma`` (``\\<lfloor> T(formula) \\<rfloor>``) to be discharged with
213
- Sledgehammer / ``auto``. The lemma is provable iff ``formula`` is
214
- intuitionistically valid (Gödel–McKinsey–Tarski).
231
+ ``lemma`` (``\\<lfloor> T(formula) \\<rfloor>``). The lemma is a theorem iff
232
+ ``formula`` is intuitionistically valid (Gödel–McKinsey–Tarski).
233
+
234
+ The proof is emitted **verdict-dependently** from the module's *decidable* S4
235
+ oracle :func:`gmt_is_s4_valid` (Z3 on the GMT→S4 translation, decisive on this
236
+ propositional fragment): when the formula is valid the theory carries a real,
237
+ Isabelle-checked proof (``using r_refl r_trans by (metis … | meson … | blast |
238
+ auto)`` — the S4 frame facts must be in scope, a bare ``axiomatization`` fact is
239
+ not in the default claset); when it is not valid the lemma is left ``oops`` (no
240
+ proof exists; see
241
+ :func:`~unicode_fol_kit.semantics.intuitionistic.int_countermodel`). The decision
242
+ deliberately does **not** use ``int_valid``'s default 3-world bound, which is
243
+ incomplete (IPL's finite-model bound grows with the formula).
215
244
 
216
245
  Unlike :func:`unicode_fol_kit.fol.qml.to_isabelle_modal` (an alethic skeleton
217
246
  that puts the lemma in a comment and defines only ``mnot``/``mbox``/``mvalid``),
@@ -219,6 +248,14 @@ def to_isabelle_intuitionistic(formula: Node, theory_name: str = "IPL_GMT") -> s
219
248
  The toolkit only *emits* the theory; it does not run Isabelle.
220
249
  """
221
250
  s4 = gmt_translate(formula)
251
+ # Decide validity with the module's own DECIDABLE S4 oracle (Z3 on the GMT→S4
252
+ # translation), NOT int_valid's bounded Kripke search: int_valid defaults to a
253
+ # 3-world bound, but IPL's finite-model-property bound grows with the formula, so
254
+ # int_valid can wrongly call a non-theorem valid — e.g. (p→q)∨(q→r)∨(r→p) is
255
+ # IPL-INVALID but needs 4 worlds to refute — which would emit a real proof for a
256
+ # NON-theorem that then fails to build. gmt_is_s4_valid is decisive on this
257
+ # propositional S4 fragment, so the emitted proof is sound by construction.
258
+ valid = gmt_is_s4_valid(formula)
222
259
  atoms = _isa_atoms(s4)
223
260
  R = "\\<Rightarrow>"
224
261
  lines = [
@@ -259,8 +296,16 @@ def to_isabelle_intuitionistic(formula: Node, theory_name: str = "IPL_GMT") -> s
259
296
  lines.append(f"consts {_isa_atom_name(a)} :: \"\\<sigma>\"")
260
297
  lines.append("")
261
298
  lines.append("lemma gmt_goal: \"\\<lfloor> " + _isa_render(s4) + " \\<rfloor>\"")
262
- lines.append(" \\<comment> \\<open>discharge with: by (metis r_refl r_trans) or sledgehammer\\<close>")
263
- lines.append(" oops \\<comment> \\<open>replace 'oops' with a proof once a prover closes it\\<close>")
299
+ if valid:
300
+ # A real, Isabelle-checked proof. The S4 frame facts must be brought into
301
+ # scope (a bare `axiomatization` fact is not in the default claset), then a
302
+ # small method battery closes the GMT goal.
303
+ lines.append(" using r_refl r_trans")
304
+ lines.append(" by (metis r_refl r_trans | meson r_refl r_trans | blast | auto)")
305
+ else:
306
+ lines.append(" \\<comment> \\<open>NOT intuitionistically valid: no proof exists "
307
+ "(see int_countermodel for a Kripke counter-model)\\<close>")
308
+ lines.append(" oops")
264
309
  lines.append("")
265
310
  lines.append("end")
266
311
  return "\n".join(lines) + "\n"
@@ -64,7 +64,8 @@ Public API
64
64
  and the constant :data:`ISABELLE_TACTICS`.
65
65
  """
66
66
 
67
- from typing import List
67
+ import re
68
+ from typing import List, Optional
68
69
 
69
70
  from unicode_fol_kit.fol.nodes import (
70
71
  Node, Variable, Constant, Number, Function,
@@ -112,6 +113,27 @@ ISABELLE_TACTICS = {
112
113
  "sorry": " sorry",
113
114
  }
114
115
 
116
+ # Tactics whose ``by ...`` proof needs the frame / domain axioms brought into the
117
+ # proof context: a bare ``axiomatization where r_refl: ...`` fact is NOT in the
118
+ # default claset/simpset, so ``by blast`` / ``by auto`` / ``by (metis ...)`` cannot
119
+ # see it and any axiom-dependent validity (T/S4/S5/KD/KD45, temporal closure, a
120
+ # domain regime) fails to discharge even though the formula is valid and the theory
121
+ # sound. For these tactics we emit ``using <axioms> by <tactic>``. ``sledgehammer``
122
+ # finds its own facts; ``oops`` / ``sorry`` prove nothing, so they are excluded.
123
+ _BY_TACTICS = frozenset({"auto", "blast", "metis", "smt", "simp", "force"})
124
+
125
+ _AXIOM_NAME_RE = re.compile(r"axiomatization where (\w+):")
126
+
127
+
128
+ def _axiom_names(axiom_lines: List[str]) -> List[str]:
129
+ """Read the fact names out of emitted ``axiomatization where NAME: ...`` lines."""
130
+ out: List[str] = []
131
+ for ln in axiom_lines:
132
+ m = _AXIOM_NAME_RE.search(ln)
133
+ if m:
134
+ out.append(m.group(1))
135
+ return out
136
+
115
137
  # Equality / inequality are uninterpreted, world-relativized predicates here (NOT
116
138
  # primitive HOL `=`), matching satisfies_modal and the THF export. These aliases
117
139
  # give them valid, distinct Isabelle constant names.
@@ -490,6 +512,20 @@ def _next_in_temporal_axiom() -> List[str]:
490
512
  return ['axiomatization where n_in_t: "n w v \\<Longrightarrow> t w v"']
491
513
 
492
514
 
515
+ def _temporal_next_closure_axiom() -> List[str]:
516
+ r"""Pin the henceforth relation ``t`` to the reflexive-transitive CLOSURE of ``n``.
517
+
518
+ ``n_in_t`` gives ``n ⊆ t`` and the temporal axioms make ``t`` reflexive+transitive,
519
+ so ``n** ⊆ t``. This adds the converse ``t ⊆ n**`` (``rtranclp n``), so ``t = n**``
520
+ EXACTLY — matching :func:`satisfies_modal`, which reads ``Always`` / ``Eventually``
521
+ over the reflexive-transitive *closure* of the one-step ``"temporal"`` relation.
522
+ Without it ``t`` may be any refl-trans superset of ``n``, and a formula valid only
523
+ because ``t = closure(n)`` (e.g. the temporal induction ``(p ∧ G(p→Xp)) → Gp``) is
524
+ spuriously refutable in the embedding — a **false INVALID** under the runner.
525
+ """
526
+ return ['axiomatization where t_in_nstar: "t w v \\<Longrightarrow> rtranclp n w v"']
527
+
528
+
493
529
  def _deontic_axioms() -> List[str]:
494
530
  """Standard Deontic Logic KD: the deontic relation is serial."""
495
531
  return ['axiomatization where d_serial: "\\<exists>v. d w v"']
@@ -511,6 +547,48 @@ def _domain_axioms(mode: str) -> List[str]:
511
547
  return out
512
548
 
513
549
 
550
+ def _collect_axioms(sig: "_Sig", frame: str, mode: str,
551
+ temporal_closure: bool) -> List[str]:
552
+ """All ``axiomatization where ...`` lines the theory emits, in emission order.
553
+
554
+ Centralised so the proof emitter and :func:`modal_axiom_names` agree on exactly
555
+ which axioms are in scope (the proof must ``using`` precisely these to discharge
556
+ an axiom-dependent validity).
557
+ """
558
+ axioms: List[str] = []
559
+ if sig.uses_alethic:
560
+ axioms += _frame_axioms(frame)
561
+ if sig.uses_deontic:
562
+ axioms += _deontic_axioms()
563
+ if sig.uses_temporal and temporal_closure:
564
+ axioms += _temporal_axioms()
565
+ # When Next (n) co-occurs with Always/Eventually (t), link n into t so the
566
+ # oracle-valid Always(P) -> Next(P) is a theorem of the emitted theory too...
567
+ if sig.uses_temporal and sig.uses_next:
568
+ axioms += _next_in_temporal_axiom()
569
+ # ...and, when t is the closure relation, pin t = n** exactly (not merely a
570
+ # refl-trans superset of n), so satisfies_modal-valid temporal-induction
571
+ # formulas are not spuriously refuted (false INVALID).
572
+ if temporal_closure:
573
+ axioms += _temporal_next_closure_axiom()
574
+ if sig.has_quant:
575
+ axioms += _domain_axioms(mode)
576
+ return axioms
577
+
578
+
579
+ def _proof_lines(tactic: str, axiom_ids: List[str]) -> List[str]:
580
+ """The proof block for ``tactic``, bringing the frame/domain axioms into scope.
581
+
582
+ For a ``by``-style tactic (:data:`_BY_TACTICS`) we prepend ``using <axioms>`` so
583
+ ``blast`` / ``auto`` / ``metis`` can actually use them; ``sledgehammer`` / ``oops``
584
+ / ``sorry`` are emitted verbatim.
585
+ """
586
+ body = ISABELLE_TACTICS[tactic]
587
+ if tactic in _BY_TACTICS and axiom_ids:
588
+ return [" using " + " ".join(axiom_ids), body]
589
+ return [body]
590
+
591
+
514
592
  # --------------------------------------------------------------------------- #
515
593
  # Type declarations for the discovered signature.
516
594
  # --------------------------------------------------------------------------- #
@@ -558,6 +636,7 @@ def isabelle_modal_theory(
558
636
  tactic: str = "sledgehammer",
559
637
  theory_name: str = "ModalEmbedding",
560
638
  temporal_closure: bool = True,
639
+ proof: Optional[str] = None,
561
640
  ) -> str:
562
641
  """Emit a complete, loadable Isabelle/HOL theory shallow-embedding ``formula``.
563
642
 
@@ -588,8 +667,15 @@ def isabelle_modal_theory(
588
667
  tactic: a key of :data:`ISABELLE_TACTICS`. The default ``"sledgehammer"``
589
668
  emits a Sledgehammer hook plus ``oops`` so the theory loads without
590
669
  claiming a proof; ``"metis"`` / ``"auto"`` / ``"blast"`` / ``"smt"`` emit a
591
- ``by ...`` proof; ``"sorry"`` admits the goal.
670
+ ``by ...`` proof. For these ``by``-style tactics the proof is prefixed
671
+ with ``using <frame/domain axioms>`` so an axiom-dependent validity
672
+ (T/S4/S5/KD/KD45, temporal closure, domain regime) can actually
673
+ discharge — a bare ``axiomatization`` fact is not in the default claset.
674
+ ``"sorry"`` admits the goal.
592
675
  theory_name: the Isabelle theory name (must match the .thy filename).
676
+ proof: an explicit proof block to emit verbatim in place of the ``tactic``
677
+ preset (used by the Isabelle runner to splice in a prove-battery or a
678
+ ``nitpick`` invocation). When ``None`` the ``tactic`` preset is used.
593
679
  temporal_closure: when True, constrain ``t`` to be reflexive+transitive so
594
680
  ``Always`` / ``Eventually`` denote the henceforth (refl-trans-closure)
595
681
  reading of :func:`satisfies_modal`. ``Next`` always uses the one-step
@@ -661,37 +747,50 @@ def isabelle_modal_theory(
661
747
  lines.append("")
662
748
 
663
749
  # Axioms.
664
- axioms: List[str] = []
665
- if sig.uses_alethic:
666
- axioms += _frame_axioms(frame)
667
- if sig.uses_deontic:
668
- axioms += _deontic_axioms()
669
- if sig.uses_temporal and temporal_closure:
670
- axioms += _temporal_axioms()
671
- # When Next (n) co-occurs with Always/Eventually (t), link n into t so the
672
- # oracle-valid Always(P) -> Next(P) is a theorem of the emitted theory too.
673
- if sig.uses_temporal and sig.uses_next:
674
- axioms += _next_in_temporal_axiom()
675
- if sig.has_quant:
676
- axioms += _domain_axioms(mode)
750
+ axioms = _collect_axioms(sig, frame, mode, temporal_closure)
677
751
  if axioms:
678
752
  lines += axioms
679
753
  lines.append("")
680
754
 
681
- # The real lemma.
755
+ # The real lemma. Its proof brings the frame/domain axioms into scope (see
756
+ # _proof_lines) so an axiom-dependent validity actually discharges; an explicit
757
+ # ``proof`` override (used by the runner's prove-battery / nitpick) wins.
682
758
  lines.append(f'lemma modal_goal: "\\<lfloor> {body} \\<rfloor>"')
683
- lines.append(ISABELLE_TACTICS[tactic])
759
+ if proof is not None:
760
+ lines.append(proof.rstrip("\n"))
761
+ else:
762
+ lines += _proof_lines(tactic, _axiom_names(axioms))
684
763
  lines.append("")
685
764
  lines.append("end")
686
765
  return "\n".join(lines) + "\n"
687
766
 
688
767
 
768
+ def modal_axiom_names(
769
+ formula: Node,
770
+ mode: str = "constant",
771
+ frame: str = "K",
772
+ temporal_closure: bool = True,
773
+ ) -> List[str]:
774
+ """The names of the ``axiomatization`` facts the emitted theory would declare.
775
+
776
+ These are exactly the frame / domain / temporal-link axioms in scope for the
777
+ proof of ``modal_goal``. The Isabelle runner needs them to build a ``using
778
+ <axioms> by <method>`` proof (or to know none are required), so this exposes the
779
+ same computation :func:`isabelle_modal_theory` uses internally.
780
+ """
781
+ _validate(mode, frame, "oops")
782
+ sig = _Sig()
783
+ _scan(formula, sig)
784
+ return _axiom_names(_collect_axioms(sig, frame, mode, temporal_closure))
785
+
786
+
689
787
  def to_isabelle_modal(
690
788
  formula: Node,
691
789
  mode: str = "constant",
692
790
  frame: str = "K",
693
791
  tactic: str = "sledgehammer",
694
792
  temporal_closure: bool = True,
793
+ proof: Optional[str] = None,
695
794
  ) -> str:
696
795
  """Emit a complete, loadable Isabelle/HOL theory embedding ``formula`` (real lemma).
697
796
 
@@ -709,4 +808,4 @@ def to_isabelle_modal(
709
808
  """
710
809
  return isabelle_modal_theory(
711
810
  formula, mode=mode, frame=frame, tactic=tactic,
712
- theory_name="ModalEmbedding", temporal_closure=temporal_closure)
811
+ theory_name="ModalEmbedding", temporal_closure=temporal_closure, proof=proof)