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,1908 @@
1
+ r"""Isabelle/HOL shallow semantical embedding (SSE) of quantified modal logic.
2
+
3
+ This module *emits* a complete, loadable Isabelle/HOL theory (``theory NAME
4
+ imports Main begin ... end``) that shallow-embeds a modal formula in the
5
+ Benzmüller style and states it as a **real** ``lemma`` (not a comment). It is the
6
+ faithful Isabelle counterpart of the THF export
7
+ :func:`unicode_logic_kit.fol.qml.to_thf_modal` and is kept consistent with the
8
+ ground-truth Kripke evaluator
9
+ :func:`unicode_logic_kit.semantics.kripke.satisfies_modal`.
10
+
11
+ What it covers
12
+ --------------
13
+ The full modal family handled by the AST:
14
+
15
+ - **alethic** ``Box`` □ / ``Diamond`` ◇ over an accessibility relation ``r``;
16
+ - **epistemic** ``Knows`` and **doxastic** ``Believes`` over **agent-indexed**
17
+ relations ``rk`` / ``rb`` of type ``'a \<Rightarrow> i \<Rightarrow> i \<Rightarrow> bool`` — the
18
+ agent is a first-class *term* (``Variable`` or ``Constant``), so a bound object
19
+ variable in agent position (``∀x (Student(x) → K_x φ)``) quantifies over agents,
20
+ exactly as in :func:`satisfies_modal`;
21
+ - **deontic** ``Obligatory`` / ``Permitted`` over a (serial) relation ``d``;
22
+ - **temporal** ``Always`` / ``Eventually`` over the henceforth relation ``t`` and
23
+ ``Next`` over the one-step relation ``n``, plus the binary interval operators
24
+ ``Until`` / ``Since`` as inductive least fixpoints over ``n`` (see below).
25
+
26
+ The embedding is **faithful to** :func:`satisfies_modal` on the overlapping
27
+ fragment: every modality is read as a box/diamond over the corresponding
28
+ relation and object quantifiers are ``existsAt``-guarded (actualist).
29
+
30
+ Identity is **rigid** (see "Equality" below), matching
31
+ :func:`unicode_logic_kit.fol.qml.qml_is_valid`.
32
+
33
+ Equality
34
+ --------
35
+ An identity atom ``t₁ = t₂`` is Isabelle's own polymorphic ``=`` over the entity
36
+ type ``e``, lifted with a world binder that is never used —
37
+ ``(\<lambda>_. t₁ = t₂)`` — so it takes **no world argument** and cannot vary by
38
+ world. ``t₁ ≠ t₂`` is lowered to ``¬(t₁ = t₂)`` first, exactly as
39
+ :func:`unicode_logic_kit.fol.qml.qml_translate` does (the lowering is
40
+ :func:`unicode_logic_kit.fol.qml._st_equality` itself, imported rather than
41
+ copied). Consequences, each one a theorem of the emitted theory and the same
42
+ verdict ``qml_is_valid`` gives: ``a = a``, ``a = b → b = a``, transitivity, the
43
+ necessity of identity ``a = b → □(a = b)`` and of distinctness
44
+ ``a ≠ b → □(a ≠ b)`` (valid in every frame, under every ``mode``),
45
+ ``◇(a = b) → a = b``, and Leibniz's law ``a = b → (P(a) ↔ P(b))`` /
46
+ ``a = b → (□P(a) ↔ □P(b))`` and congruence over function symbols.
47
+ ``□(a = b) → a = b`` is **not** valid in ``K`` (a dead-end world makes the box
48
+ vacuously true) and is valid exactly when the frame guarantees a
49
+ successor-or-self (``T``, ``S4``, ``S5``, ``KD``, ``KD45``).
50
+
51
+ Identity ranges over the whole entity type ``e`` and is **not** existence-guarded
52
+ under a varying domain (``qml``'s documented choice): ``a = a`` holds at a world
53
+ where ``a`` does not exist, and ``∃x (x = c)`` — which *does* go through the
54
+ ``existsAt``-guarded ``mexists`` — is valid under ``constant`` / ``possibilist``
55
+ and not under the actualist modes. The ordering atoms ``<`` ``>`` ``≤`` ``≥`` are
56
+ still ordinary uninterpreted world-relativized predicates (``flt`` …), as in
57
+ ``qml``. An ``=`` / ``≠`` atom that is not binary raises ``ValueError``, again as
58
+ in ``qml``.
59
+
60
+ Temporal caveat (henceforth / transitive closure)
61
+ -------------------------------------------------
62
+ ``satisfies_modal`` reads ``Always``/``Eventually`` over the **reflexive-transitive
63
+ closure** of the one-step ``"temporal"`` relation, whereas a plain shallow
64
+ abbreviation can only quantify over the relation it is given. This module emits
65
+ the **box/diamond-over-``t`` reading** and, when ``temporal_closure=True`` (the
66
+ default), additionally constrains ``t`` to be a *reflexive + transitive* relation
67
+ via frame axioms, so ``t`` denotes the henceforth relation directly and the
68
+ embedding matches the closure semantics. ``Next`` is the genuine one-step box over
69
+ the one-step successor relation ``n``. In ``satisfies_modal`` ``Always`` /
70
+ ``Eventually`` close over the **same** one-step ``"temporal"`` relation that
71
+ ``Next`` reads, so every one-step successor is henceforth-reachable and
72
+ ``Always(P) → Next(P)`` is valid. To keep the embedding faithful, when ``Next``
73
+ co-occurs with ``Always`` / ``Eventually`` (both ``n`` and ``t`` declared) this
74
+ module emits the linking axiom ``n_in_t : "⋀w v. n w v ⟹ t w v"`` (one-step
75
+ successor ⟹ henceforth-reachable), so ``Always(P) → Next(P)`` is likewise a
76
+ theorem of the emitted theory.
77
+
78
+ Binary interval operators ``Until`` / ``Since``
79
+ -----------------------------------------------
80
+ ``Until`` and ``Since`` are emitted as **inductive least-fixpoint predicates**
81
+ ``muntil`` / ``msince`` over the one-step relation ``n`` (not as box/diamond
82
+ abbreviations). ``muntil phi psi`` is the least predicate closed under
83
+ ``psi w ⟹ muntil w`` and ``phi w ∧ n w v ∧ muntil v ⟹ muntil w``, so it denotes
84
+ exactly the **finite forward ``n``-paths** with ``psi`` at the endpoint and
85
+ ``phi`` at every earlier point — the faithful counterpart of the depth-first path
86
+ search in :func:`satisfies_modal` (``_until_holds``); ``msince`` is the backward
87
+ mirror over the converse of ``n`` (``_since_holds``). A plain interval reading over
88
+ the closure ``t`` would *not* be faithful on branching / short-cut frames (it asks
89
+ ``phi`` at every ``t``-intermediate world, the path search only along one path); the
90
+ least fixpoint is faithful on **every** frame. Because these read the one-step
91
+ relation ``n``, using ``Until`` / ``Since`` declares ``n`` (as ``Next`` does) and,
92
+ when ``Always`` / ``Eventually`` also occur, links ``n`` into the henceforth ``t``
93
+ so the two agree. The Isabelle-verified characterization (the strong-Until /
94
+ Since fixpoint equation and reachability) lives in ``tests/test_hol_isabelle_modal``
95
+ and the live ``check_theory`` gate.
96
+
97
+ Cross-family bridges (``bridges=``)
98
+ -----------------------------------
99
+ ``frame=`` and ``systems=`` each constrain exactly **one** relation. A *bridge*
100
+ relates **two** relations belonging to different modal families, which is why it
101
+ is a separate, **opt-in** option and not another ``systems=`` entry. Three are
102
+ available (see :data:`BRIDGES`):
103
+
104
+ ====================================== ==================== ==========================
105
+ name schema exact frame correspondent
106
+ ====================================== ==================== ==========================
107
+ ``knowledge_implies_belief`` ``K_a φ → B_a φ`` ``rb ⊆ rk``
108
+ ``sincerity`` ``Say_a φ → B_a φ`` ``rb ⊆ rs``
109
+ ``ought_implies_can`` ``Oφ → ◇φ`` ``∀w. ∃v. d w v ∧ r w v``
110
+ ====================================== ==================== ==========================
111
+
112
+ Each condition is the **exact** correspondent of its schema — necessary as well
113
+ as sufficient — established by brute force over every frame on ≤ 2 worlds against
114
+ :func:`satisfies_modal`, not quoted from the literature. In particular
115
+ ``ought_implies_can`` is deliberately **not** the frequently quoted inclusion
116
+ ``d ⊆ r``: measured, ``d ⊆ r`` alone does not validate ``Oφ → ◇φ`` at all (a world
117
+ with no ``d``-successor makes ``Oφ`` vacuously true while ``◇φ`` is false), and
118
+ ``d ⊆ r`` *together with* ``d``-seriality validates the strictly stronger
119
+ ``□φ → Oφ`` ("whatever is necessary is obligatory") that the caller never asked
120
+ for. The emitted "meet" condition subsumes ``d``-seriality and does *not*
121
+ validate ``□φ → Oφ``. ``d_serial`` is still emitted alongside it — redundant but
122
+ harmless, and dropping it would change :func:`modal_axiom_names`' documented
123
+ output.
124
+
125
+ Bridges are OFF by default. A bridge whose partner family does not occur in the
126
+ formula **raises** ``ValueError``: silently skipping it would emit a weaker logic
127
+ than requested, and declaring the missing relation anyway is not conservative
128
+ (``d_meets_r`` entails seriality of the alethic ``r``, measurably turning
129
+ ``□P → ◇P`` from invalid into valid under ``frame='K'``). See
130
+ :func:`_bridge_axioms` for the full argument.
131
+
132
+ Honesty
133
+ -------
134
+ The toolkit *emits* a theory; it does **not** run Isabelle, Sledgehammer, or any
135
+ prover. First-order modal logic, like FOL, is **undecidable but semi-decidable**
136
+ (validity is recursively enumerable; full second-order logic is *not even
137
+ semi-decidable*), so a successful emission means "here is a sound problem an
138
+ external prover *may* discharge", never "the formula is decided". Propositional
139
+ modal K/T/S4/S5 *are* decidable, but this emitter does not itself decide them.
140
+ The default proof tactic is a Sledgehammer hook followed by an ``oops`` fallback
141
+ so the emitted theory always loads even when no automatic proof is found.
142
+
143
+ Public API
144
+ ----------
145
+ :func:`to_isabelle_modal` (the real ``to_isabelle_modal`` replacement),
146
+ :func:`isabelle_modal_theory` (alias with an explicit ``theory_name``),
147
+ and the constants :data:`ISABELLE_TACTICS` and :data:`BRIDGES`.
148
+ """
149
+
150
+ import re
151
+ from typing import Dict, Iterable, List, Optional, Sequence, Tuple, TypedDict
152
+
153
+ from unicode_logic_kit.fol.nodes import (
154
+ Node, Variable, Constant, Number, Function,
155
+ Atom, Not, And, Or, Xor, Implies, Iff, Quantifier, Contrast,
156
+ Box, Diamond, Knows, Believes, Says, Wants,
157
+ Always, Eventually, Next, Until, Since,
158
+ Historically, Once, Previous, Nominal, At,
159
+ Obligatory, Permitted, SortedQuantifier,
160
+ )
161
+ from unicode_logic_kit.fol._fol_nodes import constant_name_to_ascii
162
+ from unicode_logic_kit.fol._msfl_nodes import nonempty_sort_axioms, sort_membership_axioms
163
+ from unicode_logic_kit.fol._numeral_symbols import numerals_as_constants, prefixed_numeral_name
164
+ from unicode_logic_kit.fol._symbol_names import SymbolNames, dedupe
165
+ from unicode_logic_kit.fol._truth_constants import truth_value
166
+ # Down (the ↓ binder, N1) is not yet re-exported through fol.nodes / fol's
167
+ # public __init__ / the top-level unicode_logic_kit package (that three-file
168
+ # edit is outside this change's file ownership — see the change's own
169
+ # report); imported directly from its defining module in the meantime, the
170
+ # same class object either import path would give.
171
+ from unicode_logic_kit.fol._hybrid_nodes import Down
172
+ from unicode_logic_kit.fol.frames import FRAMES as _SHARED_FRAMES
173
+ # Identity is RIGID here exactly as in the first-order embedding: the lowering rule
174
+ # (``t1 != t2`` -> ``not (t1 = t2)``, binary-only) is qml's own, imported rather than
175
+ # copied so the two routes cannot drift apart on what counts as an identity atom.
176
+ from unicode_logic_kit.fol.qml import _st_equality, _EQUALITY_PREDICATES
177
+
178
+ # --------------------------------------------------------------------------- #
179
+ # Configuration tables (kept in sync with qml.py / satisfies_modal).
180
+ # --------------------------------------------------------------------------- #
181
+
182
+ # Frame systems for the ALETHIC relation r — the shared registry
183
+ # (unicode_logic_kit.fol.frames), so this route understands the same systems as
184
+ # every other one. Being higher-order, it is the route that CAN carry the
185
+ # three conditions with no first-order frame condition (Löb, McKinsey, Grz):
186
+ # it asserts the schema itself, with the proposition universally generalised.
187
+ _FRAMES = _SHARED_FRAMES
188
+
189
+
190
+ _ACTUALIST_MODES = frozenset({"varying", "increasing", "cumulative", "decreasing"})
191
+ _CONSTANT_MODES = frozenset({"constant", "possibilist"})
192
+
193
+ _FORALL = ("∀", "forall")
194
+ _EXISTS = ("∃", "exists")
195
+
196
+ # Available proof-tactic presets for the emitted lemma. Each maps to the Isabelle
197
+ # proof text placed after the lemma statement. ``sledgehammer`` is the honest
198
+ # default: it inserts a Sledgehammer invitation and an ``oops`` so the theory
199
+ # always loads (Isabelle treats ``oops`` as "abandon this goal" — syntactically
200
+ # valid, nothing claimed proved).
201
+ #: Proof-tactic presets for the emitted lemma, keyed by name; the value is the
202
+ #: Isabelle proof text placed after the lemma statement. ``"sledgehammer"`` is
203
+ #: the honest default — see the comment above for why a theory that always
204
+ #: loads matters more here than one that claims a proof.
205
+ ISABELLE_TACTICS = {
206
+ "sledgehammer": " sledgehammer\n oops",
207
+ "auto": " by auto",
208
+ "blast": " by blast",
209
+ "metis": " by (metis (full_types))",
210
+ "smt": " by (smt (verit))",
211
+ "simp": " by simp",
212
+ "force": " by force",
213
+ "oops": " oops",
214
+ "sorry": " sorry",
215
+ }
216
+
217
+ # Tactics whose ``by ...`` proof needs the frame / domain axioms brought into the
218
+ # proof context: a bare ``axiomatization where r_refl: ...`` fact is NOT in the
219
+ # default claset/simpset, so ``by blast`` / ``by auto`` / ``by (metis ...)`` cannot
220
+ # see it and any axiom-dependent validity (T/S4/S5/KD/KD45, temporal closure, a
221
+ # domain regime) fails to discharge even though the formula is valid and the theory
222
+ # sound. For these tactics we emit ``using <axioms> by <tactic>``. ``sledgehammer``
223
+ # finds its own facts; ``oops`` / ``sorry`` prove nothing, so they are excluded.
224
+ _BY_TACTICS = frozenset({"auto", "blast", "metis", "smt", "simp", "force"})
225
+
226
+ _AXIOM_NAME_RE = re.compile(r"axiomatization where (\w+):")
227
+
228
+
229
+ def _axiom_names(axiom_lines: List[str]) -> List[str]:
230
+ """Read the fact names out of emitted ``axiomatization where NAME: ...`` lines."""
231
+ out: List[str] = []
232
+ for ln in axiom_lines:
233
+ m = _AXIOM_NAME_RE.search(ln)
234
+ if m:
235
+ out.append(m.group(1))
236
+ return out
237
+
238
+ # Names for the symbolic predicates that are STILL ordinary uninterpreted world-
239
+ # relativized predicates here (the orderings; same convention as fol.qml). ``=`` / ``≠``
240
+ # are NOT among the predicates this table names any more: identity is rigid, lowered by
241
+ # :func:`_lower_identity` to HOL's own ``=`` and never declared as a constant. The two
242
+ # entries are kept only so that, in a formula that contains identity, they keep claiming
243
+ # ``feq`` / ``fneq`` first (see :class:`_IsaNames`), as they do in the THF resolver --
244
+ # a user predicate literally spelled ``feq`` is then pushed to ``feq_2`` in both.
245
+ _PRED_ALIAS = {"=": "feq", "≠": "fneq", "<": "flt", ">": "fgt", "≤": "fle", "≥": "fge"}
246
+
247
+
248
+ # --------------------------------------------------------------------------- #
249
+ # Rigid identity.
250
+ # --------------------------------------------------------------------------- #
251
+
252
+ def _has_identity(formula: Node) -> bool:
253
+ """Whether ``formula`` contains an ``=`` / ``≠`` atom (of ANY arity)."""
254
+ return any(isinstance(n, Atom) and n.predicate in _EQUALITY_PREDICATES
255
+ for n in formula.walk())
256
+
257
+
258
+ def _lower_identity(formula: Node, who: str) -> Node:
259
+ """Lower every ``≠`` to ``¬(=)`` and refuse a non-binary identity atom.
260
+
261
+ This is :func:`unicode_logic_kit.fol.qml._st_equality` applied to each ``=`` / ``≠``
262
+ atom -- the very rule ``qml_translate`` uses, so the HOL routes and the
263
+ first-order route read the same atoms the same way. ``who`` names the calling
264
+ exporter in the error message (qml's own message names qml). A formula with no
265
+ identity atom is returned UNCHANGED (the same object), so every equality-free
266
+ formula is emitted byte-for-byte as before.
267
+ """
268
+ if not _has_identity(formula):
269
+ return formula
270
+
271
+ def lower(node: Node) -> Node:
272
+ if isinstance(node, Atom) and node.predicate in _EQUALITY_PREDICATES:
273
+ try:
274
+ return _st_equality(node)
275
+ except ValueError as exc:
276
+ msg = str(exc)
277
+ raise ValueError(who + ": " + (msg[5:] if msg.startswith("qml: ")
278
+ else msg)) from None
279
+ return node.map_children(lower)
280
+
281
+ return lower(formula)
282
+
283
+
284
+ # --------------------------------------------------------------------------- #
285
+ # Name sanitising.
286
+ # --------------------------------------------------------------------------- #
287
+
288
+ # Built-in identifiers of the emitted theory — the world type, the accessibility
289
+ # relations / existence predicate, and every lifted-operator abbreviation. A user
290
+ # predicate / constant / function whose sanitised name lands here would shadow the
291
+ # embedding and break loadability, so _safe_name disambiguates it with a trailing "_".
292
+ # NOTE the axiom / bridge FACT names (r_refl, n_in_t, d_serial, rb_in_rk, ...) are
293
+ # deliberately absent: they live in Isabelle's fact namespace, not the consts
294
+ # namespace, so a user predicate sanitising onto one of them cannot shadow it.
295
+ _RESERVED = frozenset({
296
+ "i", "e", "existsAt",
297
+ "r", "rk", "rb", "rs", "rw", "d", "t", "n",
298
+ "mnot", "mand", "mor", "mimp", "miff", "mvalid", "mbox", "mdia",
299
+ "knows", "believes", "says", "wants", "obl", "perm",
300
+ "malways", "meventually", "mnext",
301
+ "mhistorically", "monce", "mprevious", "muntil", "msince",
302
+ "mforall", "mexists",
303
+ })
304
+
305
+ #: The names of the lifted operators (the long entries of :data:`_RESERVED`): a binder spelled
306
+ #: like one would shadow the operator in its own scope, so no bound variable gets one of them.
307
+ _OPERATOR_NAMES = frozenset(name for name in _RESERVED if len(name) > 2)
308
+
309
+
310
+ def _safe_name(name: str) -> str:
311
+ """Turn a predicate/constant/function name into a legal lowercase ASCII Isabelle id.
312
+
313
+ Disambiguates names that would collide with a built-in embedding identifier
314
+ (a relation, the existence predicate, the world type, or a lifted operator).
315
+
316
+ Non-ASCII characters are transliterated FIRST via
317
+ :func:`~unicode_logic_kit.fol._fol_nodes.constant_name_to_ascii` (ASCII passthrough;
318
+ Greek -> conventional name; anything else -> a reversible ``uXXXX`` escape) — an
319
+ Isabelle ``consts`` identifier is ASCII-only, so a raw Unicode letter reaching this
320
+ far (``str.isalnum()`` is ``True`` for almost every one of them, so the old
321
+ alnum-or-underscore filter alone let them straight through) would be silent
322
+ corruption of the loaded theory, not sanitisation. ``constant_name_to_ascii`` is
323
+ the identity on an already-ASCII string, so this changes nothing for a name that
324
+ was already legal — the digit-lead (``c_`` prefix) and reserved-word handling below
325
+ are exactly what they were before.
326
+ """
327
+ if name in _PRED_ALIAS:
328
+ return _PRED_ALIAS[name]
329
+ out = "".join(c if (c.isalnum() or c == "_") else "_"
330
+ for c in constant_name_to_ascii(str(name)))
331
+ if not out:
332
+ out = "p"
333
+ if not (out[0].isalpha() or out[0] == "_"):
334
+ out = "c_" + out
335
+ # Lower-case the leading char so it cannot collide with a bound type/var.
336
+ out = out[0].lower() + out[1:]
337
+ while out in _RESERVED:
338
+ out = out + "_"
339
+ return out
340
+
341
+
342
+ def _var_name(name: str) -> str:
343
+ """Isabelle term-variable name (object variables stay lowercase, legal).
344
+
345
+ Same ASCII-transliteration fix as :func:`_safe_name`, for the same reason: a raw
346
+ non-ASCII variable name previously reached the Isabelle text verbatim (only
347
+ non-alnum/underscore ASCII punctuation was replaced), which is not a legal
348
+ Isabelle identifier. Use :meth:`_IsaNames.variable` for a per-formula unique
349
+ token — ``_var_name`` alone can map two distinct source names onto the same
350
+ token once they are both transliterated (the same class of gap ``_safe_name``
351
+ has, closed there by :class:`_IsaNames`'s ``dedupe``-backed maps).
352
+ """
353
+ out = "".join(c if (c.isalnum() or c == "_") else "_"
354
+ for c in constant_name_to_ascii(str(name)))
355
+ if not out or not (out[0].isalpha() or out[0] == "_"):
356
+ out = "x_" + out
357
+ return out
358
+
359
+
360
+ class _IsaNames(SymbolNames):
361
+ """Per-formula Isabelle constant-name resolver (the shared :class:`SymbolNames` over
362
+ ``_safe_name`` + the equality aliases), so distinct source symbols that sanitise
363
+ alike (``Ab`` / ``ab``) — or a predicate used at two arities — get DISTINCT
364
+ ``consts``. Without it ``□Ab → □ab`` could collapse to a tautology and emit duplicate
365
+ ``consts`` (which Isabelle rejects).
366
+ """
367
+
368
+ def __init__(self, formula: Node, reserved=frozenset()):
369
+ # ``reserved`` holds the free variables of the axioms of this theory
370
+ # (:func:`_axiom_variable_names`): a symbol of the user never has one of those
371
+ # spellings, it gets a numbered variant instead.
372
+ super().__init__(formula, _safe_name, _PRED_ALIAS, reserved=reserved)
373
+ # Identity is rigid HOL ``=`` (see _atom), not a predicate: it gets no
374
+ # ``consts`` line and no entry here. (SymbolNames collected it with every other
375
+ # atom, and let it claim ``feq`` / ``fneq`` -- which only matters to a user
376
+ # predicate literally spelled that way, pushed to ``feq_2``: still unique.)
377
+ for key in [k for k in self.pred if k[0] in _EQUALITY_PREDICATES]:
378
+ del self.pred[key]
379
+ # Nominals become world constants ``nom_<name> :: i`` — the same reserved
380
+ # ``nom_`` prefix the standard translation uses, deduped against every
381
+ # other emitted constant. _safe_name may append "_" to dodge reserved
382
+ # single letters (e.g. the world type "i"), but Isabelle REJECTS
383
+ # identifiers with a trailing underscore — the "nom_" prefix already
384
+ # dodges those names, so strip it; dedupe with digit suffixes (legal).
385
+ taken = (set(self.pred.values()) | set(self.const.values())
386
+ | set(self.func.values()))
387
+ self.nom: Dict[str, str] = {}
388
+ for n in formula.walk():
389
+ if isinstance(n, Nominal) and n.name not in self.nom:
390
+ cand = "nom_" + (_safe_name(n.name).rstrip("_") or "x")
391
+ while cand in taken:
392
+ cand += "0"
393
+ taken.add(cand)
394
+ self.nom[n.name] = cand
395
+ self._var_map: Dict[str, str] = {}
396
+ # A binder shadows every constant of its own name inside its scope, so a bound
397
+ # variable never has the spelling of a constant, predicate, function or nominal
398
+ # constant of the theory, nor of a lifted operator.
399
+ self._var_used: set = taken | _OPERATOR_NAMES
400
+
401
+ def nominal(self, name: str) -> str:
402
+ """The world constant naming nominal ``name``."""
403
+ return self.nom[name]
404
+
405
+ def variable(self, name: str) -> str:
406
+ """Unique Isabelle bound-variable token for object-variable ``name``.
407
+
408
+ Isabelle variables are not case-folded the way THF's are, so the pre-existing
409
+ `x`/`X` case collision THF's variable resolver preserves does not arise here.
410
+ What DOES need de-collision is the same ASCII-transliteration gap
411
+ :func:`_var_name` closed for individual names: two distinct source names could
412
+ still coincide AFTER transliteration (e.g. a literal ``theta`` and the Greek
413
+ ``θ``, both -> ``theta``). Routed through
414
+ :func:`~unicode_logic_kit.fol._symbol_names.dedupe` against a token pool of this
415
+ resolver instance that STARTS with every token of the theory's own constants,
416
+ predicates, functions and nominal constants and with the names of the lifted
417
+ operators. That is not optional: a binder shadows a constant of its own name
418
+ inside its scope, so ``∀x P(x, c)`` with a constant spelled ``x`` would be written
419
+ ``(\\<lambda>x. (p x x))`` -- the constant captured, a different sentence. A variable
420
+ whose natural token is taken gets a numbered variant (``x_2``).
421
+ """
422
+ if name in self._var_map:
423
+ return self._var_map[name]
424
+ cand = dedupe(_var_name(name), self._var_used)
425
+ self._var_map[name] = cand
426
+ return cand
427
+
428
+
429
+ # --------------------------------------------------------------------------- #
430
+ # Term rendering.
431
+ # --------------------------------------------------------------------------- #
432
+
433
+ def _term(node: Node, names: "_IsaNames") -> str:
434
+ """Render an individual term (used as an argument to a lifted predicate)."""
435
+ if isinstance(node, Variable):
436
+ return names.variable(node.name)
437
+ if isinstance(node, Constant):
438
+ return names.constant(node.name)
439
+ if isinstance(node, Number):
440
+ return names.constant(prefixed_numeral_name(node.value))
441
+ if isinstance(node, Function):
442
+ head = names.function(node)
443
+ if not node.args:
444
+ return head
445
+ return "(" + head + " " + " ".join(_term(a, names) for a in node.args) + ")"
446
+ raise NotImplementedError(
447
+ f"to_isabelle_modal: unsupported term {type(node).__name__}.")
448
+
449
+
450
+ # --------------------------------------------------------------------------- #
451
+ # Formula lifting: Node -> Isabelle term of type i => bool.
452
+ # --------------------------------------------------------------------------- #
453
+
454
+ def _atom(node: Atom, names: "_IsaNames") -> str:
455
+ r"""Lift an atom to ``(pred a1 ... an)`` — a value of type ``i => bool``.
456
+
457
+ An identity atom (``=``; ``≠`` was lowered to ``¬(=)`` by :func:`_lower_identity`
458
+ before any atom is lifted) is HOL's own ``=`` over the entity terms, under a
459
+ world binder it never uses: ``(\<lambda>_. a = b)``. No world argument means it
460
+ cannot vary by world (rigid identity, qml's reading), and the binder is the
461
+ anonymous ``_`` rather than a name so a user variable called ``w`` can never be
462
+ captured by it.
463
+ """
464
+ constant = truth_value(node)
465
+ if constant is not None:
466
+ # `$true` / `$false`: HOL's own True / False under the anonymous world binder,
467
+ # so they cannot vary by world.
468
+ return "(\\<lambda>_. True)" if constant else "(\\<lambda>_. False)"
469
+ if node.predicate == "=":
470
+ left, right = node.args
471
+ return f"(\\<lambda>_. {_term(left, names)} = {_term(right, names)})"
472
+ head = names.atom(node)
473
+ if not node.args:
474
+ return head
475
+ return "(" + head + " " + " ".join(_term(a, names) for a in node.args) + ")"
476
+
477
+
478
+ def _lift(node: Node, names: "_IsaNames") -> str:
479
+ r"""Lift a modal formula to an Isabelle term of type ``i \<Rightarrow> bool``.
480
+
481
+ Mirrors the THF ``_thf_lift`` and stays faithful to ``satisfies_modal``: every
482
+ modality becomes a box/diamond over the matching relation, object quantifiers
483
+ are existsAt-guarded, and the agent of Knows/Believes is carried as a real term.
484
+ """
485
+ if isinstance(node, Atom):
486
+ return _atom(node, names)
487
+ if isinstance(node, Not):
488
+ return f"(mnot {_lift(node.formula, names)})"
489
+ if isinstance(node, And):
490
+ return f"(mand {_lift(node.left, names)} {_lift(node.right, names)})"
491
+ if isinstance(node, Or):
492
+ return f"(mor {_lift(node.left, names)} {_lift(node.right, names)})"
493
+ if isinstance(node, Xor):
494
+ # Xor ≡ ¬(φ ↔ ψ); satisfies_modal reads Xor as truth-value inequality.
495
+ return f"(mnot (miff {_lift(node.left, names)} {_lift(node.right, names)}))"
496
+ if isinstance(node, Implies):
497
+ return f"(mimp {_lift(node.left, names)} {_lift(node.right, names)})"
498
+ if isinstance(node, Iff):
499
+ return f"(miff {_lift(node.left, names)} {_lift(node.right, names)})"
500
+
501
+ if isinstance(node, Box):
502
+ return f"(mbox {_lift(node.formula, names)})"
503
+ if isinstance(node, Diamond):
504
+ return f"(mdia {_lift(node.formula, names)})"
505
+
506
+ if isinstance(node, Contrast):
507
+ # Truth-functionally conjunction (Contrast's own contract).
508
+ return f"(mand {_lift(node.left, names)} {_lift(node.right, names)})"
509
+ if isinstance(node, Knows):
510
+ return f"(knows {_term(node.agent, names)} {_lift(node.formula, names)})"
511
+ if isinstance(node, Believes):
512
+ return f"(believes {_term(node.agent, names)} {_lift(node.formula, names)})"
513
+ if isinstance(node, Says):
514
+ return f"(says {_term(node.agent, names)} {_lift(node.formula, names)})"
515
+ if isinstance(node, Wants):
516
+ return f"(wants {_term(node.agent, names)} {_lift(node.formula, names)})"
517
+
518
+ if isinstance(node, Obligatory):
519
+ return f"(obl {_lift(node.formula, names)})"
520
+ if isinstance(node, Permitted):
521
+ return f"(perm {_lift(node.formula, names)})"
522
+
523
+ if isinstance(node, Always):
524
+ return f"(malways {_lift(node.formula, names)})"
525
+ if isinstance(node, Eventually):
526
+ return f"(meventually {_lift(node.formula, names)})"
527
+ if isinstance(node, Next):
528
+ return f"(mnext {_lift(node.formula, names)})"
529
+ if isinstance(node, Historically):
530
+ # "now and at every past point": box over the CONVERSE of the henceforth
531
+ # closure t — matching satisfies_modal's converse-relation reading.
532
+ return f"(mhistorically {_lift(node.formula, names)})"
533
+ if isinstance(node, Once):
534
+ # "now or at some past point": diamond over the converse of t.
535
+ return f"(monce {_lift(node.formula, names)})"
536
+ if isinstance(node, Previous):
537
+ # "at every immediate past point": box over the converse of one-step n.
538
+ return f"(mprevious {_lift(node.formula, names)})"
539
+
540
+ if isinstance(node, Nominal):
541
+ # A nominal is true at exactly the world it names: the world constant
542
+ # nom_<name>, mirroring standard_translation's reserved nom_ prefix and
543
+ # KripkeModel's nominals= assignment.
544
+ return f"(\\<lambda>w. w = {names.nominal(node.name)})"
545
+ if isinstance(node, At):
546
+ # @i φ: evaluate φ AT the named world, regardless of the current one. The world
547
+ # binder is anonymous: it is not used, and a named one would capture an object
548
+ # variable of the body spelled ``w`` (``∀w @i P(w)``).
549
+ return (f"(\\<lambda>_. {_lift(node.formula, names)} "
550
+ f"{names.nominal(node.nominal.name)})")
551
+ if isinstance(node, Down):
552
+ raise NotImplementedError(
553
+ "to_isabelle_modal: the ↓ binder is not supported by this HOL "
554
+ "embedding — H(@,↓) validity is undecidable, and this emitter's "
555
+ "job (a lemma statement for Sledgehammer/a tactic) assumes a goal "
556
+ "shape a human or ATP proof search can be expected to close, not "
557
+ "an open research question. Use "
558
+ "unicode_logic_kit.fol.modal_translation.down_is_valid (Z3, "
559
+ "PROVED-only) or unicode_logic_kit.atp.kripke_enum.KripkeEnumBackend "
560
+ "/ modal_enum_search (bounded search, REFUTED-only) instead.")
561
+
562
+ if isinstance(node, Quantifier):
563
+ x = names.variable(node.variable.name)
564
+ binder = "mforall" if node.type in _FORALL else "mexists"
565
+ return f"({binder} (\\<lambda>{x}. {_lift(node.formula, names)}))"
566
+
567
+ if isinstance(node, Until):
568
+ # Strong "left until right" as the inductive predicate ``muntil`` (least
569
+ # fixpoint over the one-step relation n), matching satisfies_modal's finite
570
+ # forward-path search exactly. See _until_block.
571
+ return f"(muntil {_lift(node.left, names)} {_lift(node.right, names)})"
572
+ if isinstance(node, Since):
573
+ # Past-tense "left since right" as ``msince`` (least fixpoint over the CONVERSE
574
+ # of n), the backward mirror of muntil — matching satisfies_modal's backward
575
+ # path search. See _since_block.
576
+ return f"(msince {_lift(node.left, names)} {_lift(node.right, names)})"
577
+ if isinstance(node, SortedQuantifier):
578
+ raise NotImplementedError(
579
+ "to_isabelle_modal: SortedQuantifier is not supported; use a plain ∀x/∃x.")
580
+ if type(node).__name__ in ("Would", "Might"):
581
+ raise NotImplementedError(
582
+ "to_isabelle_modal: the counterfactuals □→/◇→ read a similarity "
583
+ "ordering (Lewis spheres), not an accessibility relation — use "
584
+ "hol.isabelle_conditional / isabelle_decide_counterfactual.")
585
+ raise NotImplementedError(
586
+ f"to_isabelle_modal: unsupported node type {type(node).__name__}.")
587
+
588
+
589
+ # --------------------------------------------------------------------------- #
590
+ # Signature discovery (which operators / relations / typed consts are needed).
591
+ # --------------------------------------------------------------------------- #
592
+
593
+ class _Sig:
594
+ """Collected signature: predicates, constants, functions, and used modalities."""
595
+
596
+ def __init__(self):
597
+ self.preds = {} # safe_name -> arity (object args)
598
+ self.consts = set() # safe_name
599
+ self.funcs = {} # safe_name -> arity
600
+ self.uses_alethic = False
601
+ self.uses_epistemic = False
602
+ self.uses_doxastic = False
603
+ self.uses_assertive = False # Says (agent-indexed relation rs, plain K)
604
+ self.uses_bouletic = False # Wants (agent-indexed relation rw, plain K)
605
+ self.uses_deontic = False
606
+ self.uses_temporal = False # Always / Eventually / Historically / Once (t)
607
+ self.uses_past = False # Historically / Once (converse-of-t readers)
608
+ self.uses_next = False # Next (relation n)
609
+ self.uses_previous = False # Previous (converse-of-n reader)
610
+ self.uses_until = False # Until (inductive muntil over n)
611
+ self.uses_since = False # Since (inductive msince over converse n)
612
+ self.uses_hybrid = False # Nominal / At (world constants nom_*)
613
+ self.has_quant = False
614
+
615
+ @property
616
+ def needs_next_rel(self) -> bool:
617
+ """Whether the one-step relation ``n`` must be declared.
618
+
619
+ ``n`` is needed by ``Next`` (mnext), ``Previous`` (mprevious — its
620
+ converse), and the inductive ``muntil`` / ``msince`` (one-step forward /
621
+ backward path search).
622
+ """
623
+ return (self.uses_next or self.uses_previous
624
+ or self.uses_until or self.uses_since)
625
+
626
+
627
+ def _scan_term(node: Node, sig: _Sig) -> None:
628
+ if isinstance(node, Constant):
629
+ sig.consts.add(_safe_name(node.name))
630
+ elif isinstance(node, Number):
631
+ sig.consts.add(_safe_name(prefixed_numeral_name(node.value)))
632
+ elif isinstance(node, Function):
633
+ sig.funcs[_safe_name(node.name)] = len(node.args)
634
+ for a in node.args:
635
+ _scan_term(a, sig)
636
+ # Variables contribute nothing to the signature.
637
+
638
+
639
+ def _scan(node: Node, sig: _Sig) -> None:
640
+ """Walk the formula collecting the signature and which modalities are used."""
641
+ if isinstance(node, Atom):
642
+ if node.predicate not in _EQUALITY_PREDICATES and truth_value(node) is None:
643
+ sig.preds[_safe_name(node.predicate)] = len(node.args)
644
+ for a in node.args:
645
+ _scan_term(a, sig)
646
+ return
647
+ if isinstance(node, (Not,)):
648
+ _scan(node.formula, sig)
649
+ return
650
+ if isinstance(node, (And, Or, Xor, Implies, Iff, Contrast)):
651
+ _scan(node.left, sig)
652
+ _scan(node.right, sig)
653
+ return
654
+ if isinstance(node, Box):
655
+ sig.uses_alethic = True
656
+ _scan(node.formula, sig)
657
+ return
658
+ if isinstance(node, Diamond):
659
+ sig.uses_alethic = True
660
+ _scan(node.formula, sig)
661
+ return
662
+ if isinstance(node, Knows):
663
+ sig.uses_epistemic = True
664
+ _scan_term(node.agent, sig)
665
+ _scan(node.formula, sig)
666
+ return
667
+ if isinstance(node, Believes):
668
+ sig.uses_doxastic = True
669
+ _scan_term(node.agent, sig)
670
+ _scan(node.formula, sig)
671
+ return
672
+ if isinstance(node, Says):
673
+ sig.uses_assertive = True
674
+ _scan_term(node.agent, sig)
675
+ _scan(node.formula, sig)
676
+ return
677
+ if isinstance(node, Wants):
678
+ sig.uses_bouletic = True
679
+ _scan_term(node.agent, sig)
680
+ _scan(node.formula, sig)
681
+ return
682
+ if isinstance(node, (Obligatory, Permitted)):
683
+ sig.uses_deontic = True
684
+ _scan(node.formula, sig)
685
+ return
686
+ if isinstance(node, (Always, Eventually)):
687
+ sig.uses_temporal = True
688
+ _scan(node.formula, sig)
689
+ return
690
+ if isinstance(node, (Historically, Once)):
691
+ # Past closure operators read the CONVERSE of the same henceforth t.
692
+ sig.uses_temporal = True
693
+ sig.uses_past = True
694
+ _scan(node.formula, sig)
695
+ return
696
+ if isinstance(node, Next):
697
+ sig.uses_next = True
698
+ _scan(node.formula, sig)
699
+ return
700
+ if isinstance(node, Previous):
701
+ sig.uses_previous = True
702
+ _scan(node.formula, sig)
703
+ return
704
+ if isinstance(node, Nominal):
705
+ sig.uses_hybrid = True
706
+ return
707
+ if isinstance(node, At):
708
+ sig.uses_hybrid = True
709
+ _scan(node.formula, sig)
710
+ return
711
+ if isinstance(node, Down):
712
+ raise NotImplementedError(
713
+ "to_isabelle_modal: the ↓ binder is not supported by this HOL "
714
+ "embedding — H(@,↓) validity is undecidable, and this emitter's "
715
+ "job (a lemma statement for Sledgehammer/a tactic) assumes a goal "
716
+ "shape a human or ATP proof search can be expected to close, not "
717
+ "an open research question. Use "
718
+ "unicode_logic_kit.fol.modal_translation.down_is_valid (Z3, "
719
+ "PROVED-only) or unicode_logic_kit.atp.kripke_enum.KripkeEnumBackend "
720
+ "/ modal_enum_search (bounded search, REFUTED-only) instead.")
721
+ if isinstance(node, Quantifier):
722
+ sig.has_quant = True
723
+ _scan(node.formula, sig)
724
+ return
725
+ if isinstance(node, Until):
726
+ sig.uses_until = True
727
+ _scan(node.left, sig)
728
+ _scan(node.right, sig)
729
+ return
730
+ if isinstance(node, Since):
731
+ sig.uses_since = True
732
+ _scan(node.left, sig)
733
+ _scan(node.right, sig)
734
+ return
735
+ if isinstance(node, SortedQuantifier):
736
+ raise NotImplementedError(
737
+ "to_isabelle_modal: SortedQuantifier is not supported; use a plain ∀x/∃x.")
738
+ if type(node).__name__ in ("Would", "Might"):
739
+ raise NotImplementedError(
740
+ "to_isabelle_modal: the counterfactuals □→/◇→ read a similarity "
741
+ "ordering (Lewis spheres), not an accessibility relation — use "
742
+ "hol.isabelle_conditional / isabelle_decide_counterfactual.")
743
+ raise NotImplementedError(
744
+ f"to_isabelle_modal: unsupported node type {type(node).__name__}.")
745
+
746
+
747
+ # --------------------------------------------------------------------------- #
748
+ # Fixed building blocks of the theory.
749
+ # --------------------------------------------------------------------------- #
750
+
751
+ # World type and the propositional/connective abbreviations are ALWAYS emitted.
752
+ _CORE_ABBREVS = [
753
+ 'abbreviation mnot :: "(i \\<Rightarrow> bool) \\<Rightarrow> (i \\<Rightarrow> bool)" where',
754
+ ' "mnot \\<phi> \\<equiv> \\<lambda>w. \\<not> \\<phi> w"',
755
+ 'abbreviation mand :: "(i \\<Rightarrow> bool) \\<Rightarrow> (i \\<Rightarrow> bool) \\<Rightarrow> (i \\<Rightarrow> bool)" where',
756
+ ' "mand \\<phi> \\<psi> \\<equiv> \\<lambda>w. \\<phi> w \\<and> \\<psi> w"',
757
+ 'abbreviation mor :: "(i \\<Rightarrow> bool) \\<Rightarrow> (i \\<Rightarrow> bool) \\<Rightarrow> (i \\<Rightarrow> bool)" where',
758
+ ' "mor \\<phi> \\<psi> \\<equiv> \\<lambda>w. \\<phi> w \\<or> \\<psi> w"',
759
+ 'abbreviation mimp :: "(i \\<Rightarrow> bool) \\<Rightarrow> (i \\<Rightarrow> bool) \\<Rightarrow> (i \\<Rightarrow> bool)" where',
760
+ ' "mimp \\<phi> \\<psi> \\<equiv> \\<lambda>w. \\<phi> w \\<longrightarrow> \\<psi> w"',
761
+ 'abbreviation miff :: "(i \\<Rightarrow> bool) \\<Rightarrow> (i \\<Rightarrow> bool) \\<Rightarrow> (i \\<Rightarrow> bool)" where',
762
+ ' "miff \\<phi> \\<psi> \\<equiv> \\<lambda>w. \\<phi> w \\<longleftrightarrow> \\<psi> w"',
763
+ ]
764
+
765
+ # validity: truth at every world.
766
+ _VALID_ABBREV = [
767
+ 'abbreviation mvalid :: "(i \\<Rightarrow> bool) \\<Rightarrow> bool" ("\\<lfloor>_\\<rfloor>") where',
768
+ ' "\\<lfloor>\\<phi>\\<rfloor> \\<equiv> \\<forall>w. \\<phi> w"',
769
+ ]
770
+
771
+
772
+ def _alethic_block() -> List[str]:
773
+ return [
774
+ 'consts r :: "i \\<Rightarrow> i \\<Rightarrow> bool" \\<comment> \\<open>alethic accessibility\\<close>',
775
+ 'abbreviation mbox :: "(i \\<Rightarrow> bool) \\<Rightarrow> (i \\<Rightarrow> bool)" where',
776
+ ' "mbox \\<phi> \\<equiv> \\<lambda>w. \\<forall>v. r w v \\<longrightarrow> \\<phi> v"',
777
+ 'abbreviation mdia :: "(i \\<Rightarrow> bool) \\<Rightarrow> (i \\<Rightarrow> bool)" where',
778
+ ' "mdia \\<phi> \\<equiv> \\<lambda>w. \\<exists>v. r w v \\<and> \\<phi> v"',
779
+ ]
780
+
781
+
782
+ def _epistemic_block() -> List[str]:
783
+ return [
784
+ 'consts rk :: "e \\<Rightarrow> i \\<Rightarrow> i \\<Rightarrow> bool" \\<comment> \\<open>agent-indexed epistemic accessibility\\<close>',
785
+ 'abbreviation knows :: "e \\<Rightarrow> (i \\<Rightarrow> bool) \\<Rightarrow> (i \\<Rightarrow> bool)" where',
786
+ ' "knows a \\<phi> \\<equiv> \\<lambda>w. \\<forall>v. rk a w v \\<longrightarrow> \\<phi> v"',
787
+ ]
788
+
789
+
790
+ def _doxastic_block() -> List[str]:
791
+ return [
792
+ 'consts rb :: "e \\<Rightarrow> i \\<Rightarrow> i \\<Rightarrow> bool" \\<comment> \\<open>agent-indexed doxastic accessibility\\<close>',
793
+ 'abbreviation believes :: "e \\<Rightarrow> (i \\<Rightarrow> bool) \\<Rightarrow> (i \\<Rightarrow> bool)" where',
794
+ ' "believes a \\<phi> \\<equiv> \\<lambda>w. \\<forall>v. rb a w v \\<longrightarrow> \\<phi> v"',
795
+ ]
796
+
797
+
798
+ def _assertive_block() -> List[str]:
799
+ return [
800
+ 'consts rs :: "e \\<Rightarrow> i \\<Rightarrow> i \\<Rightarrow> bool" \\<comment> \\<open>agent-indexed assertive accessibility (Says)\\<close>',
801
+ 'abbreviation says :: "e \\<Rightarrow> (i \\<Rightarrow> bool) \\<Rightarrow> (i \\<Rightarrow> bool)" where',
802
+ ' "says a \\<phi> \\<equiv> \\<lambda>w. \\<forall>v. rs a w v \\<longrightarrow> \\<phi> v"',
803
+ ]
804
+
805
+
806
+ def _bouletic_block() -> List[str]:
807
+ return [
808
+ 'consts rw :: "e \\<Rightarrow> i \\<Rightarrow> i \\<Rightarrow> bool" \\<comment> \\<open>agent-indexed bouletic accessibility (Wants)\\<close>',
809
+ 'abbreviation wants :: "e \\<Rightarrow> (i \\<Rightarrow> bool) \\<Rightarrow> (i \\<Rightarrow> bool)" where',
810
+ ' "wants a \\<phi> \\<equiv> \\<lambda>w. \\<forall>v. rw a w v \\<longrightarrow> \\<phi> v"',
811
+ ]
812
+
813
+
814
+ def _deontic_block() -> List[str]:
815
+ return [
816
+ 'consts d :: "i \\<Rightarrow> i \\<Rightarrow> bool" \\<comment> \\<open>deontic accessibility (serial)\\<close>',
817
+ 'abbreviation obl :: "(i \\<Rightarrow> bool) \\<Rightarrow> (i \\<Rightarrow> bool)" where',
818
+ ' "obl \\<phi> \\<equiv> \\<lambda>w. \\<forall>v. d w v \\<longrightarrow> \\<phi> v"',
819
+ 'abbreviation perm :: "(i \\<Rightarrow> bool) \\<Rightarrow> (i \\<Rightarrow> bool)" where',
820
+ ' "perm \\<phi> \\<equiv> \\<lambda>w. \\<exists>v. d w v \\<and> \\<phi> v"',
821
+ ]
822
+
823
+
824
+ def _temporal_block() -> List[str]:
825
+ return [
826
+ 'consts t :: "i \\<Rightarrow> i \\<Rightarrow> bool" \\<comment> \\<open>temporal henceforth relation (refl+trans closure)\\<close>',
827
+ 'abbreviation malways :: "(i \\<Rightarrow> bool) \\<Rightarrow> (i \\<Rightarrow> bool)" where',
828
+ ' "malways \\<phi> \\<equiv> \\<lambda>w. \\<forall>v. t w v \\<longrightarrow> \\<phi> v"',
829
+ 'abbreviation meventually :: "(i \\<Rightarrow> bool) \\<Rightarrow> (i \\<Rightarrow> bool)" where',
830
+ ' "meventually \\<phi> \\<equiv> \\<lambda>w. \\<exists>v. t w v \\<and> \\<phi> v"',
831
+ ]
832
+
833
+
834
+ def _temporal_def_block() -> List[str]:
835
+ r"""Temporal block with ``t`` **defined** as ``rtranclp n`` (the refl-trans closure
836
+ of the one-step relation), not a ``consts`` constrained by axioms.
837
+
838
+ Used by the runner's refute step: with ``t`` determined by ``n``, nitpick searches
839
+ a candidate ``n`` and computes ``t`` itself, so it can actually find a finite
840
+ counter-model to a non-theorem of the Always/Next closure fragment (a ``consts t``
841
+ with an ``rtranclp`` *axiom* leaves nitpick unable to construct the closure).
842
+ Requires the next block (``consts n``) to be emitted first.
843
+ """
844
+ return [
845
+ 'definition t :: "i \\<Rightarrow> i \\<Rightarrow> bool" where "t = rtranclp n" '
846
+ '\\<comment> \\<open>henceforth = reflexive-transitive closure of the one-step n\\<close>',
847
+ 'abbreviation malways :: "(i \\<Rightarrow> bool) \\<Rightarrow> (i \\<Rightarrow> bool)" where',
848
+ ' "malways \\<phi> \\<equiv> \\<lambda>w. \\<forall>v. t w v \\<longrightarrow> \\<phi> v"',
849
+ 'abbreviation meventually :: "(i \\<Rightarrow> bool) \\<Rightarrow> (i \\<Rightarrow> bool)" where',
850
+ ' "meventually \\<phi> \\<equiv> \\<lambda>w. \\<exists>v. t w v \\<and> \\<phi> v"',
851
+ ]
852
+
853
+
854
+ def _next_consts_block() -> List[str]:
855
+ """Declare the one-step successor relation ``n`` (needed by Next / Until / Since)."""
856
+ return [
857
+ 'consts n :: "i \\<Rightarrow> i \\<Rightarrow> bool" \\<comment> \\<open>one-step temporal successor\\<close>',
858
+ ]
859
+
860
+
861
+ def _next_abbrev_block() -> List[str]:
862
+ """The ``mnext`` box-over-``n`` abbreviation (emitted only when Next occurs)."""
863
+ return [
864
+ 'abbreviation mnext :: "(i \\<Rightarrow> bool) \\<Rightarrow> (i \\<Rightarrow> bool)" where',
865
+ ' "mnext \\<phi> \\<equiv> \\<lambda>w. \\<forall>v. n w v \\<longrightarrow> \\<phi> v"',
866
+ ]
867
+
868
+
869
+ def _previous_abbrev_block() -> List[str]:
870
+ r"""``mprevious`` — box over the CONVERSE of the one-step ``n`` (⒴).
871
+
872
+ Faithful to :func:`satisfies_modal`'s Previous: "at every immediate PAST
873
+ point", i.e. every ``v`` with ``n v w``. Requires ``n`` to be declared.
874
+ """
875
+ return [
876
+ 'abbreviation mprevious :: "(i \\<Rightarrow> bool) \\<Rightarrow> (i \\<Rightarrow> bool)" where',
877
+ ' "mprevious \\<phi> \\<equiv> \\<lambda>w. \\<forall>v. n v w \\<longrightarrow> \\<phi> v"',
878
+ ]
879
+
880
+
881
+ def _past_temporal_block() -> List[str]:
882
+ r"""``mhistorically`` / ``monce`` — box/diamond over the CONVERSE of ``t``.
883
+
884
+ Faithful to :func:`satisfies_modal`'s converse-closure readings: ``t`` is
885
+ reflexive, so ``mhistorically`` covers "now and every past point" and
886
+ ``monce`` "now or some past point". The converse of a reflexive+transitive
887
+ relation is reflexive+transitive, so the ``t_refl`` / ``t_trans`` axioms
888
+ constrain the past readings exactly as the future ones — no extra axioms.
889
+ Requires ``t`` to be declared.
890
+ """
891
+ return [
892
+ 'abbreviation mhistorically :: "(i \\<Rightarrow> bool) \\<Rightarrow> (i \\<Rightarrow> bool)" where',
893
+ ' "mhistorically \\<phi> \\<equiv> \\<lambda>w. \\<forall>v. t v w \\<longrightarrow> \\<phi> v"',
894
+ 'abbreviation monce :: "(i \\<Rightarrow> bool) \\<Rightarrow> (i \\<Rightarrow> bool)" where',
895
+ ' "monce \\<phi> \\<equiv> \\<lambda>w. \\<exists>v. t v w \\<and> \\<phi> v"',
896
+ ]
897
+
898
+
899
+ def _hybrid_block(names: "_IsaNames") -> List[str]:
900
+ """``consts nom_<name> :: i`` — one world constant per nominal (H(@))."""
901
+ out = []
902
+ for name, c in sorted(names.nom.items(), key=lambda kv: kv[1]):
903
+ out.append(f'consts {c} :: "i" \\<comment> \\<open>the world named by nominal {name}\\<close>')
904
+ return out
905
+
906
+
907
+ def _until_block() -> List[str]:
908
+ r"""``muntil`` — strong "left until right" as a least fixpoint over ``n``.
909
+
910
+ Defined inductively so it denotes exactly the FINITE forward paths
911
+ ``w = w0 \<rightarrow> ... \<rightarrow> wn`` (one-step ``n`` edges, n \<ge> 0) with ``psi`` at the
912
+ endpoint and ``phi`` at every earlier point — the faithful counterpart of
913
+ :func:`unicode_logic_kit.semantics.kripke._until_holds` (its depth-first path
914
+ search). A plain box/diamond-over-``t`` abbreviation could NOT capture "phi all
915
+ the way along the path" (on a branching/short-cut frame the interval reading
916
+ over the closure ``t`` differs from the path search); the least fixpoint does,
917
+ so this is faithful to ``satisfies_modal`` on every frame, not just linear ones.
918
+ ``n`` (the one-step relation) must be declared first.
919
+ """
920
+ return [
921
+ 'inductive muntil :: "(i \\<Rightarrow> bool) \\<Rightarrow> (i \\<Rightarrow> bool) \\<Rightarrow> i \\<Rightarrow> bool"',
922
+ ' for phi :: "i \\<Rightarrow> bool" and psi :: "i \\<Rightarrow> bool" where',
923
+ ' muntil_base: "psi w \\<Longrightarrow> muntil phi psi w"',
924
+ '| muntil_step: "phi w \\<Longrightarrow> n w v \\<Longrightarrow> muntil phi psi v '
925
+ '\\<Longrightarrow> muntil phi psi w"',
926
+ ]
927
+
928
+
929
+ def _since_block() -> List[str]:
930
+ r"""``msince`` — past-tense "left since right", the backward mirror of ``muntil``.
931
+
932
+ Same least fixpoint but stepping along the CONVERSE of ``n`` (``n v w``: ``v`` is a
933
+ one-step predecessor of ``w``), so it denotes the finite BACKWARD paths matching
934
+ :func:`unicode_logic_kit.semantics.kripke._since_holds`. ``n`` must be declared first.
935
+ """
936
+ return [
937
+ 'inductive msince :: "(i \\<Rightarrow> bool) \\<Rightarrow> (i \\<Rightarrow> bool) \\<Rightarrow> i \\<Rightarrow> bool"',
938
+ ' for phi :: "i \\<Rightarrow> bool" and psi :: "i \\<Rightarrow> bool" where',
939
+ ' msince_base: "psi w \\<Longrightarrow> msince phi psi w"',
940
+ '| msince_step: "phi w \\<Longrightarrow> n v w \\<Longrightarrow> msince phi psi v '
941
+ '\\<Longrightarrow> msince phi psi w"',
942
+ ]
943
+
944
+
945
+ #: The declaration of the existence predicate: ``existsAt x w`` says object ``x`` is in
946
+ #: the domain of world ``w``. Written once, for :func:`_quant_block` and for the
947
+ #: quantifier-free formula whose sort witness (an actualist mode) names it.
948
+ _EXISTS_AT_DECL = ('consts existsAt :: "e \\<Rightarrow> i \\<Rightarrow> bool" '
949
+ '\\<comment> \\<open>object x exists at world w\\<close>')
950
+
951
+
952
+ def _quant_block(mode: str) -> List[str]:
953
+ """existsAt + actualist mforall/mexists (constant mode makes existsAt total)."""
954
+ return [
955
+ _EXISTS_AT_DECL,
956
+ 'abbreviation mforall :: "(e \\<Rightarrow> (i \\<Rightarrow> bool)) \\<Rightarrow> (i \\<Rightarrow> bool)" where',
957
+ ' "mforall \\<Phi> \\<equiv> \\<lambda>w. \\<forall>x. existsAt x w \\<longrightarrow> \\<Phi> x w"',
958
+ 'abbreviation mexists :: "(e \\<Rightarrow> (i \\<Rightarrow> bool)) \\<Rightarrow> (i \\<Rightarrow> bool)" where',
959
+ ' "mexists \\<Phi> \\<equiv> \\<lambda>w. \\<exists>x. existsAt x w \\<and> \\<Phi> x w"',
960
+ ]
961
+
962
+
963
+ # --------------------------------------------------------------------------- #
964
+ # Frame / domain axioms.
965
+ # --------------------------------------------------------------------------- #
966
+
967
+ def _frame_axioms(frame: str) -> List[str]:
968
+ """Alethic-frame axioms on r for the chosen frame system."""
969
+ conds = _FRAMES[frame]
970
+ out = []
971
+ if "refl" in conds:
972
+ out.append('axiomatization where r_refl: "r w w"')
973
+ if "trans" in conds:
974
+ out.append('axiomatization where r_trans: "r w v \\<Longrightarrow> r v u \\<Longrightarrow> r w u"')
975
+ if "sym" in conds:
976
+ out.append('axiomatization where r_sym: "r w v \\<Longrightarrow> r v w"')
977
+ if "serial" in conds:
978
+ out.append('axiomatization where r_serial: "\\<exists>v. r w v"')
979
+ if "eucl" in conds:
980
+ out.append('axiomatization where r_eucl: "r w v \\<Longrightarrow> r w u \\<Longrightarrow> r v u"')
981
+ if "directed" in conds:
982
+ out.append('axiomatization where r_directed: '
983
+ '"r w v \\<Longrightarrow> r w u \\<Longrightarrow> \\<exists>z. r v z \\<and> r u z"')
984
+ if "connected" in conds:
985
+ out.append('axiomatization where r_conn: '
986
+ '"r w v \\<Longrightarrow> r w u \\<Longrightarrow> r v u \\<or> r u v"')
987
+ if "functional" in conds:
988
+ # CD: at most one successor.
989
+ out.append('axiomatization where r_func: '
990
+ '"r w v \\<Longrightarrow> r w u \\<Longrightarrow> v = u"')
991
+ if "dense" in conds:
992
+ out.append('axiomatization where r_dense: '
993
+ '"r w v \\<Longrightarrow> \\<exists>u. r w u \\<and> r u v"')
994
+ if "shift_refl" in conds:
995
+ out.append('axiomatization where r_shift_refl: '
996
+ '"r w v \\<Longrightarrow> r v v"')
997
+ if "empty" in conds:
998
+ out.append('axiomatization where r_empty: "\\<not> r w v"')
999
+ if "mckinsey" in conds:
1000
+ # McKinsey (.1): the schema □◇P → ◇□P, P and w schematic. There is NO
1001
+ # first-order frame condition for it, so the schema IS the axiom —
1002
+ # which is precisely what a higher-order route can state.
1003
+ out.append('axiomatization where r_mckinsey: '
1004
+ '"(\\<forall>v. r w v \\<longrightarrow> (\\<exists>u. r v u \\<and> P u)) \\<Longrightarrow> '
1005
+ '(\\<exists>v. r w v \\<and> (\\<forall>u. r v u \\<longrightarrow> P u))"')
1006
+ if "grz" in conds:
1007
+ # Grzegorczyk: □(□(P → □P) → P) → P, P and w schematic; likewise not
1008
+ # first-order definable.
1009
+ out.append('axiomatization where r_grz: '
1010
+ '"(\\<forall>v. r w v \\<longrightarrow> ((\\<forall>u. r v u \\<longrightarrow> (P u \\<longrightarrow> '
1011
+ '(\\<forall>z. r u z \\<longrightarrow> P z))) \\<longrightarrow> P v)) \\<Longrightarrow> P w"')
1012
+ if "loeb" in conds:
1013
+ # Gödel–Löb provability: the Löb schema □(□P → P) → □P (P, w free, i.e.
1014
+ # universally generalised). Stated over r directly so it needs no abbreviation;
1015
+ # transitivity (4) is derivable from it but emitted too for a faithful frame.
1016
+ out.append('axiomatization where r_loeb: '
1017
+ '"(\\<forall>v. r w v \\<longrightarrow> '
1018
+ '(\\<forall>u. r v u \\<longrightarrow> P u) \\<longrightarrow> P v) '
1019
+ '\\<Longrightarrow> (\\<forall>v. r w v \\<longrightarrow> P v)"')
1020
+ return out
1021
+
1022
+
1023
+ # Conditions expressible as per-agent axiomatization schemas (free vars are
1024
+ # schematic, i.e. universally generalised — including the agent a).
1025
+ _AGENT_CONDS = {
1026
+ "refl": '{tag}_refl: "{rel} a w w"',
1027
+ "trans": '{tag}_trans: "{rel} a w v \\<Longrightarrow> {rel} a v u \\<Longrightarrow> {rel} a w u"',
1028
+ "sym": '{tag}_sym: "{rel} a w v \\<Longrightarrow> {rel} a v w"',
1029
+ "serial": '{tag}_serial: "\\<exists>v. {rel} a w v"',
1030
+ "eucl": '{tag}_eucl: "{rel} a w v \\<Longrightarrow> {rel} a w u \\<Longrightarrow> {rel} a v u"',
1031
+ }
1032
+
1033
+ # The agent-indexed families systems= may constrain: family → (relation const, _Sig flag).
1034
+ _AGENT_SYSTEM_FAMILIES = {
1035
+ "epistemic": ("rk", "uses_epistemic"),
1036
+ "doxastic": ("rb", "uses_doxastic"),
1037
+ "assertive": ("rs", "uses_assertive"),
1038
+ "bouletic": ("rw", "uses_bouletic"),
1039
+ }
1040
+
1041
+
1042
+ def _agent_system_axioms(rel: str, system: str, family: str) -> List[str]:
1043
+ """Per-agent frame axioms constraining an agent-indexed relation (rk/rb/rs/rw).
1044
+
1045
+ Each axiom leaves the agent ``a`` schematic, so the property holds for EVERY
1046
+ agent — matching the ``∀A``-quantified axioms of ``to_thf_modal_full``'s
1047
+ ``systems=`` and the per-agent relations of ``satisfies_modal``. Only systems
1048
+ whose conditions are plain Horn frame properties are accepted; ``GL`` /
1049
+ ``S4.2`` / ``S4.3`` need conditions (Löb / directedness / connectedness) that
1050
+ this schema set does not express, and silently dropping one would emit a
1051
+ WEAKER logic than requested.
1052
+ """
1053
+ if system not in _FRAMES:
1054
+ raise ValueError(
1055
+ f"to_isabelle_modal: unknown system {system!r} for {family} "
1056
+ f"(use one of {sorted(_FRAMES)}).")
1057
+ conds = _FRAMES[system]
1058
+ unsupported = [c for c in conds if c not in _AGENT_CONDS]
1059
+ if unsupported:
1060
+ raise NotImplementedError(
1061
+ f"to_isabelle_modal: system {system!r} for {family} needs the frame "
1062
+ f"condition(s) {unsupported}, which have no per-agent axiom schema "
1063
+ "here; use frame= on the alethic relation for those systems.")
1064
+ return ["axiomatization where "
1065
+ + _AGENT_CONDS[c].format(rel=rel, tag=rel) for c in conds]
1066
+
1067
+
1068
+ # --------------------------------------------------------------------------- #
1069
+ # Cross-family bridge axioms (opt-in, ``bridges=``).
1070
+ # --------------------------------------------------------------------------- #
1071
+ #
1072
+ # WHY A SEPARATE OPTION. ``frame=`` constrains the alethic r and ``systems=``
1073
+ # constrains ONE agent-indexed relation each; both are single-relation frame
1074
+ # properties. A bridge is a principle relating TWO relations of DIFFERENT modal
1075
+ # families (knowledge to belief, assertion to belief, obligation to possibility),
1076
+ # so it cannot be phrased as a ``systems=`` entry — hence its own opt-in option,
1077
+ # defaulting to off.
1078
+ #
1079
+ # WHY THESE CONDITIONS. Each condition below is the EXACT frame correspondent of
1080
+ # its schema — necessary as well as sufficient — measured by brute force over
1081
+ # every frame on <= 2 worlds against semantics.kripke.satisfies_modal, not quoted
1082
+ # from the literature:
1083
+ #
1084
+ # rb subset rk |= K_a phi -> B_a phi and rb subset rk does NOT
1085
+ # validate B_a phi -> K_a phi,
1086
+ # while no condition at all fails
1087
+ # to validate K -> B.
1088
+ # rb subset rs |= Say_a phi -> B_a phi (same two-sided check)
1089
+ # d meets r |= O phi -> <>phi
1090
+ #
1091
+ # THE ``d subset r`` TRAP — do NOT "simplify" d_meets_r into an inclusion. The
1092
+ # frequently quoted ought-implies-can condition ``d subset r`` is measurably the
1093
+ # WRONG axiom: on its own it does not validate O phi -> <>phi at all (a world with
1094
+ # no d-successor makes O phi vacuously true while <>phi is false), and ``d subset
1095
+ # r`` TOGETHER WITH d-seriality validates the strictly stronger [] phi -> O phi
1096
+ # ("whatever is necessary is obligatory"), which the caller never requested. The
1097
+ # existential "meet" condition emitted here is exactly right: it subsumes
1098
+ # d-seriality (every world gets a d-successor) and does not validate [] phi -> O
1099
+ # phi. ``d_serial`` is still emitted alongside it whenever the deontic family
1100
+ # occurs — redundant but harmless, and suppressing it would change the documented
1101
+ # output of :func:`modal_axiom_names`.
1102
+ #
1103
+ # ALL ROUTES AGREE, and that is asserted rather than assumed. The guarded
1104
+ # first-order route ``fol.qml`` emits the same three conditions under the same three
1105
+ # names, including this meet condition for ``ought_implies_can`` (it realised that
1106
+ # one as the inclusion ``D subset R`` in an earlier draft, which validated the
1107
+ # unrequested ``[] phi -> O phi`` and ``P phi -> <>phi``). ``fol.QML_BRIDGES`` and
1108
+ # ``hol.BRIDGES`` stay SEPARATE registries — ``fol`` must not import ``hol`` — so
1109
+ # tests/test_hol_bridges.py and tests/test_qml_bridges.py cross-check the two tables
1110
+ # against each other and against the Kripke oracle, rather than trusting either.
1111
+ #
1112
+ # WHY THE VARIABLES ARE BOUND EXPLICITLY (\<And>a w v. ...) rather than left free
1113
+ # and schematic like _frame_axioms / _AGENT_CONDS. A free variable in an
1114
+ # axiomatization is generalised ONLY if the name is not already a declared
1115
+ # constant. The agent slot is the dangerous one: an agent named ``a`` (the
1116
+ # canonical test agent) emits ``consts a :: "e"``, and a free ``a`` in the axiom
1117
+ # then resolves to THAT CONSTANT and typechecks, so the inclusion would silently
1118
+ # hold for one agent instead of all — a weaker logic than requested, with no
1119
+ # error. A Pure binder shadows the constant inside its scope, so the schema is
1120
+ # universal whatever the formula's symbols are named. (The pre-existing
1121
+ # _AGENT_CONDS schemas have the same exposure; changing them is a separate
1122
+ # change, since their emitted text is pinned by existing tests.)
1123
+
1124
+ # Canonical family name -> the _Sig flag that says its relation is declared.
1125
+ _FAMILY_FLAG = {
1126
+ "alethic": "uses_alethic",
1127
+ "epistemic": "uses_epistemic",
1128
+ "doxastic": "uses_doxastic",
1129
+ "assertive": "uses_assertive",
1130
+ "bouletic": "uses_bouletic",
1131
+ "deontic": "uses_deontic",
1132
+ }
1133
+
1134
+ # Bridge name -> the families it relates (with the operator label to name in an
1135
+ # error message), a description of the relations, and the Isabelle axiom lines.
1136
+ # The fact names ``rb_in_rk`` / ``rb_in_rs`` / ``d_meets_r`` deliberately follow
1137
+ # the existing relation-shaped naming (``n_in_t``, ``t_in_nstar``, ``d_serial``)
1138
+ # and are reused VERBATIM as the THF formula names in hol.thf_modal, so a single
1139
+ # grep for a fact name finds both routes.
1140
+ class _BridgeSpec(TypedDict):
1141
+ """One entry of :data:`_BRIDGES`."""
1142
+
1143
+ needs: Tuple[Tuple[str, str], ...]
1144
+ schema: str
1145
+ rels: str
1146
+ lines: List[str]
1147
+
1148
+
1149
+ _BRIDGES: Dict[str, _BridgeSpec] = {
1150
+ "knowledge_implies_belief": {
1151
+ "needs": (("epistemic", "Knows"), ("doxastic", "Believes")),
1152
+ "schema": "K_a phi -> B_a phi",
1153
+ "rels": "the doxastic relation rb to the epistemic relation rk",
1154
+ "lines": ['axiomatization where rb_in_rk: '
1155
+ '"\\<And>a w v. rb a w v \\<Longrightarrow> rk a w v"'],
1156
+ },
1157
+ "sincerity": {
1158
+ "needs": (("assertive", "Says"), ("doxastic", "Believes")),
1159
+ "schema": "Say_a phi -> B_a phi",
1160
+ "rels": "the doxastic relation rb to the assertive relation rs",
1161
+ "lines": ['axiomatization where rb_in_rs: '
1162
+ '"\\<And>a w v. rb a w v \\<Longrightarrow> rs a w v"'],
1163
+ },
1164
+ "ought_implies_can": {
1165
+ "needs": (("deontic", "Obligatory/Permitted"), ("alethic", "□/◇")),
1166
+ "schema": "O phi -> <>phi",
1167
+ "rels": "the deontic relation d to the alethic relation r",
1168
+ "lines": ['axiomatization where d_meets_r: '
1169
+ '"\\<And>w. \\<exists>v. d w v \\<and> r w v"'],
1170
+ },
1171
+ }
1172
+
1173
+ #: The cross-family bridge names accepted by ``bridges=`` (see :func:`_bridge_axioms`).
1174
+ BRIDGES = tuple(_BRIDGES)
1175
+
1176
+
1177
+ def _validate_bridges(bridges, func: str) -> frozenset:
1178
+ """Normalise and validate a ``bridges=`` request; return it as a frozen set.
1179
+
1180
+ ``None`` / empty means "no bridges" (the default). A bare string is rejected
1181
+ rather than iterated character-wise — ``bridges="sincerity"`` would otherwise
1182
+ silently degrade into a set of unknown one-character names.
1183
+ """
1184
+ if not bridges:
1185
+ return frozenset()
1186
+ if isinstance(bridges, str):
1187
+ raise ValueError(
1188
+ f"{func}: bridges= takes a collection of names, not a single string; "
1189
+ f"use bridges=['{bridges}'].")
1190
+ requested = frozenset(bridges)
1191
+ unknown = sorted(requested - set(_BRIDGES))
1192
+ if unknown:
1193
+ raise ValueError(
1194
+ f"{func}: unknown bridge(s) {unknown} (use one of {sorted(_BRIDGES)}).")
1195
+ return requested
1196
+
1197
+
1198
+ def _bridge_axioms(sig: "_Sig", bridges) -> List[str]:
1199
+ """Axioms for the requested cross-family bridges, in table order.
1200
+
1201
+ Each bridge adds ONE frame condition relating two relations of different
1202
+ modal families; the emitted axiom is the exact correspondent of the schema
1203
+ the option is named after (``knowledge_implies_belief`` → ``rb ⊆ rk``,
1204
+ ``sincerity`` → ``rb ⊆ rs``, ``ought_implies_can`` → ``∀w. ∃v. d w v ∧ r w v``
1205
+ — never the folklore ``d ⊆ r``, see the section comment above). Emission
1206
+ follows :data:`_BRIDGES` order, not the caller's set-iteration order, so the
1207
+ output is deterministic.
1208
+
1209
+ Honest contract — what a request for a bridge whose partner family does NOT
1210
+ occur in the formula does, and why. Such a bridge names a relation that this
1211
+ emitter never declares (every relation block is conditional on its operators
1212
+ occurring), so the axiom would not even load. There are three possible
1213
+ policies and only one of them is honest:
1214
+
1215
+ - *silently skip it*: the caller asked for a logic and would receive a
1216
+ strictly WEAKER one with no signal — the exact "silently weakened logic"
1217
+ failure this module refuses everywhere else;
1218
+ - *declare the relation anyway*: not uniformly conservative, and it fails in
1219
+ the STRENGTHENING direction. Measured: ``d_meets_r`` entails
1220
+ ``∃v. r w v``, i.e. seriality of the ALETHIC relation, so emitting
1221
+ ``ought_implies_can`` for a formula with no ``O``/``P`` but with ``□``/``◇``
1222
+ would turn ``□P → ◇P`` from invalid into valid under ``frame='K'`` — a
1223
+ silent change to the alethic logic the caller *did* select. (The two
1224
+ inclusion bridges happen to be conservative when their partner family is
1225
+ absent, but a rule that sometimes emits and sometimes refuses depending on
1226
+ the axiom's shape is not predictable from the option name.)
1227
+ - *raise*: one uniform rule for every bridge, present and future; the repo's
1228
+ established idiom for "this cannot be expressed here as asked" (see the GL
1229
+ guard in ``fol.qml.qml_axioms`` and :func:`_agent_system_axioms`); and the
1230
+ only policy under which the relation-declaration blocks stay untouched, so
1231
+ a bridge is emitted exactly when both relation blocks already are.
1232
+
1233
+ ``ValueError`` — not ``NotImplementedError`` — because the route *can* express
1234
+ the bridge; the caller's formula simply does not mention one of the families.
1235
+ ``NotImplementedError`` stays reserved for "this route cannot express it".
1236
+ """
1237
+ requested = _validate_bridges(bridges, "to_isabelle_modal")
1238
+ if not requested:
1239
+ return []
1240
+ out: List[str] = []
1241
+ for name, spec in _BRIDGES.items():
1242
+ if name not in requested:
1243
+ continue
1244
+ missing = [op for fam, op in spec["needs"]
1245
+ if not getattr(sig, _FAMILY_FLAG[fam])]
1246
+ if missing:
1247
+ raise ValueError(
1248
+ f"to_isabelle_modal: the bridge {name!r} relates {spec['rels']}, "
1249
+ f"but the formula contains no {' / '.join(missing)} operator, so "
1250
+ "that relation is never declared. Emitting the axiom would not "
1251
+ "load; declaring the relation anyway is not conservative "
1252
+ "(d_meets_r entails seriality of the alethic r, which would "
1253
+ "silently make []P -> <>P valid under frame='K'); and skipping it "
1254
+ "would emit a weaker logic than requested. Drop the bridge, or "
1255
+ "state the formula in both families.")
1256
+ out += spec["lines"]
1257
+ return out
1258
+
1259
+
1260
+ def _temporal_axioms() -> List[str]:
1261
+ """t is the refl+trans (henceforth) relation, matching satisfies_modal's closure."""
1262
+ return [
1263
+ 'axiomatization where t_refl: "t w w"',
1264
+ 'axiomatization where t_trans: "t w v \\<Longrightarrow> t v u \\<Longrightarrow> t w u"',
1265
+ ]
1266
+
1267
+
1268
+ def _next_in_temporal_axiom() -> List[str]:
1269
+ """Link the one-step successor n to the henceforth relation t.
1270
+
1271
+ In ``satisfies_modal`` ``Always`` / ``Eventually`` close over the *same*
1272
+ one-step ``"temporal"`` relation that ``Next`` reads, so every one-step
1273
+ successor is henceforth-reachable and ``Always(P) → Next(P)`` is valid.
1274
+ Without this axiom the emitted ``n`` and ``t`` are decoupled and that
1275
+ entailment fails, breaking faithfulness; the axiom restores it.
1276
+ """
1277
+ return ['axiomatization where n_in_t: "n w v \\<Longrightarrow> t w v"']
1278
+
1279
+
1280
+ def _temporal_next_closure_axiom() -> List[str]:
1281
+ r"""Pin the henceforth relation ``t`` to the reflexive-transitive CLOSURE of ``n``.
1282
+
1283
+ ``n_in_t`` gives ``n ⊆ t`` and the temporal axioms make ``t`` reflexive+transitive,
1284
+ so ``n** ⊆ t``. This adds the converse ``t ⊆ n**`` (``rtranclp n``), so ``t = n**``
1285
+ EXACTLY — matching :func:`satisfies_modal`, which reads ``Always`` / ``Eventually``
1286
+ over the reflexive-transitive *closure* of the one-step ``"temporal"`` relation.
1287
+ Without it ``t`` may be any refl-trans superset of ``n``, and a formula valid only
1288
+ because ``t = closure(n)`` (e.g. the temporal induction ``(p ∧ G(p→Xp)) → Gp``) is
1289
+ spuriously refutable in the embedding — a **false INVALID** under the runner.
1290
+ """
1291
+ return ['axiomatization where t_in_nstar: "t w v \\<Longrightarrow> rtranclp n w v"']
1292
+
1293
+
1294
+ def _deontic_axioms() -> List[str]:
1295
+ """Standard Deontic Logic KD: the deontic relation is serial."""
1296
+ return ['axiomatization where d_serial: "\\<exists>v. d w v"']
1297
+
1298
+
1299
+ def _domain_axioms(mode: str) -> List[str]:
1300
+ """existsAt domain-regime axioms (only meaningful when quantifiers occur)."""
1301
+ out = []
1302
+ if mode in _CONSTANT_MODES:
1303
+ out.append('axiomatization where const_dom: "existsAt x w"')
1304
+ else:
1305
+ # actualist: keep every world's domain non-empty (matches qml nonempty_dom).
1306
+ out.append('axiomatization where nonempty_dom: "\\<exists>x. existsAt x w"')
1307
+ if mode in ("increasing", "cumulative"):
1308
+ out.append('axiomatization where cumul_dom: "existsAt x w \\<Longrightarrow> r w v \\<Longrightarrow> existsAt x v"')
1309
+ elif mode == "decreasing":
1310
+ out.append('axiomatization where decr_dom: "existsAt x v \\<Longrightarrow> r w v \\<Longrightarrow> existsAt x w"')
1311
+ # "varying": no monotonicity axiom.
1312
+ return out
1313
+
1314
+
1315
+ def _sort_consts(original: Node, names: "_IsaNames") -> List[str]:
1316
+ """Isabelle constant names of the sorts ``original`` quantifies over.
1317
+
1318
+ Takes the UNrelativized formula, because after ``_relativize`` a sort is no
1319
+ longer distinguishable from any other unary predicate. Reuses
1320
+ :func:`~unicode_logic_kit.fol._msfl_nodes.nonempty_sort_axioms`'s own scan (as
1321
+ :func:`~unicode_logic_kit.fol.qml._sort_names_used` does) so this route and the
1322
+ classical one can never disagree about which sorts a formula uses, then maps
1323
+ each through the de-colliding resolver so the axiom names the same ``consts``
1324
+ the lifted goal does. A sort the relativized formula never mentions as a
1325
+ unary atom — it has no ``consts`` line — is skipped rather than referenced.
1326
+ """
1327
+ out: List[str] = []
1328
+ for axiom in nonempty_sort_axioms(original):
1329
+ assert isinstance(axiom, Quantifier) and isinstance(axiom.formula, Atom) # ∃x S(x)
1330
+ const = names.pred.get((axiom.formula.predicate, 1))
1331
+ if const is not None and const not in out:
1332
+ out.append(const)
1333
+ return out
1334
+
1335
+
1336
+ def _names_with_sort_facts(relativized: Node, original: Node,
1337
+ reserved=frozenset()) -> "_IsaNames":
1338
+ """The constant-name resolver of ``relativized``, extended by the sort facts of ``original``.
1339
+
1340
+ ``relativized`` is the formula the theory is lifted from; in it a sorted
1341
+ constant ``c:S`` has become the plain ``c`` and the guard ``S`` is mentioned only
1342
+ if some sorted QUANTIFIER used it. A sort that occurs only through a sorted
1343
+ constant would therefore get neither a ``consts`` line nor a name for its
1344
+ axioms. Resolving over ``relativized`` together with the membership atom
1345
+ ``S(c)`` of every sorted constant declares that guard too — the very name the
1346
+ membership axiom and the non-emptiness axiom then use. A formula without a sorted
1347
+ constant is resolved over ``relativized`` itself, so its names and declarations
1348
+ are exactly what they were. ``reserved`` is the set of names no symbol of the formula
1349
+ may take (see :func:`_axiom_variable_names`).
1350
+ """
1351
+ scope = relativized
1352
+ for atom in sort_membership_axioms(original):
1353
+ scope = And(scope, atom)
1354
+ return _IsaNames(scope, reserved)
1355
+
1356
+
1357
+ def _sort_members(original: Node, names: "_IsaNames") -> List[Tuple[str, str]]:
1358
+ """``(guard constant, individual constant)`` of every sorted constant of ``original``.
1359
+
1360
+ One pair per distinct ``c:S``, in first-occurrence order (the order of
1361
+ :func:`~unicode_logic_kit.fol._msfl_nodes.sort_membership_axioms`), each name
1362
+ taken from ``names`` — which must come from :func:`_names_with_sort_facts`, so
1363
+ both are declared and agree with the ones the lifted goal uses.
1364
+ """
1365
+ out: List[Tuple[str, str]] = []
1366
+ for atom in sort_membership_axioms(original):
1367
+ assert isinstance(atom, Atom) and isinstance(atom.args[0], Constant) # ``S(c)``
1368
+ pair = (names.pred[(atom.predicate, 1)], names.constant(atom.args[0].name))
1369
+ if pair not in out:
1370
+ out.append(pair)
1371
+ return out
1372
+
1373
+
1374
+ def _sort_member_axioms(members: Sequence[Tuple[str, str]]) -> List[str]:
1375
+ r"""Membership of every sorted constant in its sort, at EVERY world and unguarded.
1376
+
1377
+ ``c:S`` denotes an element of ``S`` (the many-sorted reading every route of the
1378
+ kit shares, :func:`~unicode_logic_kit.fol._msfl_nodes.sort_membership_axioms`), and
1379
+ a constant is a rigid designator, so ``S c w`` holds at every world ``w`` —
1380
+ ``w`` is free, hence universally quantified by ``axiomatization``, exactly as in
1381
+ the ``nonempty_sort`` lines. It is NOT guarded by ``existsAt c w``, in any
1382
+ mode: a constant may lie outside the local domain (the route's reading of a
1383
+ constant, see :mod:`unicode_logic_kit.fol.qml`), and the guarded form would never
1384
+ fire in the modes where that matters. Without the line a countermodel with
1385
+ ``socrates`` outside ``human`` refutes
1386
+ ``∀x:Human Mortal(x) → Mortal(socrates:Human)`` in the constant-domain mode,
1387
+ which the plain-FOL routes call valid.
1388
+
1389
+ The facts are named ``sort_member<i>``, a family distinct from
1390
+ ``nonempty_sort<i>``, so :func:`modal_axiom_names` lists them separately.
1391
+ """
1392
+ return [f'axiomatization where sort_member{i}: "{sort} {const} w"'
1393
+ for i, (sort, const) in enumerate(members)]
1394
+
1395
+
1396
+ def _nonempty_sort_axioms(sorts: Sequence[str], mode: str) -> List[str]:
1397
+ r"""Per-world non-emptiness for every sort the source formula quantifies over.
1398
+
1399
+ ``_relativize`` turns ``∀x:S φ`` into ``∀x (S(x) → φ)``, which leaves the sort
1400
+ guard an ordinary WORLD-RELATIVE predicate — so without this axiom nothing
1401
+ forces ``S`` to hold of anything at a world, and ``∀x:S P(x) → ∃x:S P(x)``
1402
+ comes out unprovable here while the classical many-sorted routes call it
1403
+ valid (``fol._msfl_nodes.nonempty_sort_axioms`` adds the same convention
1404
+ there, and :func:`~unicode_logic_kit.fol.qml.qml_axioms` the same per-world
1405
+ version). ``w`` is free, i.e. implicitly universally quantified over worlds
1406
+ by ``axiomatization``, so the sort is non-empty at EVERY world, matching the
1407
+ qml route rather than merely fixing the verdict at one.
1408
+
1409
+ Under an actualist ``mode`` the witness must also EXIST at the world: a
1410
+ witness outside the local domain cannot instantiate the ``existsAt``-guarded
1411
+ ``mexists``, so the bare version would not restore the entailment there.
1412
+ """
1413
+ out: List[str] = []
1414
+ for i, sort in enumerate(sorts):
1415
+ body = (f"\\<exists>x. existsAt x w \\<and> {sort} x w"
1416
+ if mode not in _CONSTANT_MODES else f"\\<exists>x. {sort} x w")
1417
+ out.append(f'axiomatization where nonempty_sort{i}: "{body}"')
1418
+ return out
1419
+
1420
+
1421
+ def _collect_axioms(sig: "_Sig", frame: str, mode: str,
1422
+ temporal_closure: bool, temporal_def: bool = False,
1423
+ systems: Optional[dict] = None,
1424
+ bridges: Optional[Iterable[str]] = None,
1425
+ sorts: Sequence[str] = (),
1426
+ members: Sequence[Tuple[str, str]] = ()) -> List[str]:
1427
+ """All ``axiomatization where ...`` lines the theory emits, in emission order.
1428
+
1429
+ Centralised so the proof emitter and :func:`modal_axiom_names` agree on exactly
1430
+ which axioms are in scope (the proof must ``using`` precisely these to discharge
1431
+ an axiom-dependent validity). When ``temporal_def`` makes ``t = rtranclp n`` a
1432
+ definition (Always/Eventually + Next), the temporal/next closure axioms become
1433
+ theorems and are omitted. ``systems`` optionally constrains the agent-indexed
1434
+ relations (see :func:`isabelle_modal_theory`); a family whose operators do not
1435
+ occur in the formula is skipped (its relation is not even declared).
1436
+
1437
+ ``bridges`` optionally adds the opt-in CROSS-family axioms (see
1438
+ :func:`_bridge_axioms`). Unlike a ``systems=`` family, a bridge whose partner
1439
+ family is absent is NOT skipped — it raises, because a bridge constrains two
1440
+ relations and skipping it would silently emit a weaker logic. The bridge block
1441
+ is APPENDED after the temporal axioms, so every pre-existing
1442
+ :func:`modal_axiom_names` result keeps its exact content *and* order.
1443
+ """
1444
+ # t = rtranclp n needs only t to be IN USE: for a formula without Next/
1445
+ # Until/Since the one-step n is simply unconstrained and nitpick constructs
1446
+ # the closure itself — exactly what lets it refute non-theorems of the
1447
+ # pure closure fragment (④/Ⓘ included) instead of quasi_genuine failure.
1448
+ use_temporal_def = temporal_def and sig.uses_temporal
1449
+ axioms: List[str] = []
1450
+ if sig.uses_alethic:
1451
+ axioms += _frame_axioms(frame)
1452
+ for family, system in sorted((systems or {}).items()):
1453
+ if family not in _AGENT_SYSTEM_FAMILIES:
1454
+ raise ValueError(
1455
+ f"to_isabelle_modal: unknown systems family {family!r} "
1456
+ f"(use one of {sorted(_AGENT_SYSTEM_FAMILIES)}).")
1457
+ rel, flag = _AGENT_SYSTEM_FAMILIES[family]
1458
+ if getattr(sig, flag):
1459
+ axioms += _agent_system_axioms(rel, system, family)
1460
+ if sig.uses_deontic:
1461
+ axioms += _deontic_axioms()
1462
+ if not use_temporal_def:
1463
+ if sig.uses_temporal and temporal_closure:
1464
+ axioms += _temporal_axioms()
1465
+ # When the one-step relation n co-occurs with Always/Eventually (t) — because
1466
+ # Next, Until, or Since is present — link n into t so t and the n-based
1467
+ # operators agree (t denotes the closure of the SAME one-step relation the
1468
+ # path-search Until/Since and Next read), keeping the oracle-valid
1469
+ # Always(P) -> Next(P) and the Always/Until interplay theorems of the theory...
1470
+ if sig.uses_temporal and sig.needs_next_rel:
1471
+ axioms += _next_in_temporal_axiom()
1472
+ # ...and, when t is the closure relation, pin t = n** exactly (not merely a
1473
+ # refl-trans superset of n), so satisfies_modal-valid temporal-induction
1474
+ # formulas are not spuriously refuted (false INVALID).
1475
+ if temporal_closure:
1476
+ axioms += _temporal_next_closure_axiom()
1477
+ axioms += _bridge_axioms(sig, bridges)
1478
+ if sig.has_quant:
1479
+ axioms += _domain_axioms(mode)
1480
+ # After the domain axioms, so `existsAt` is already constrained when an
1481
+ # actualist sort witness refers to it, and so every pre-existing
1482
+ # modal_axiom_names result keeps its exact content and order (a formula
1483
+ # with no many-sorted node yields no sorts and no extra line).
1484
+ axioms += _nonempty_sort_axioms(sorts, mode)
1485
+ # Then the sorted constants' membership (a formula without a sorted constant
1486
+ # has none, so its axiom list is unchanged).
1487
+ axioms += _sort_member_axioms(members)
1488
+ return axioms
1489
+
1490
+
1491
+ _ISABELLE_MACRO = re.compile(r"\\<[A-Za-z^]+>")
1492
+ _ONE_LETTER_NAME = re.compile(r"\b[a-z]\b")
1493
+
1494
+
1495
+ def _axiom_variable_names(sig: "_Sig", original: Node, frame: str, mode: str,
1496
+ temporal_closure: bool, temporal_def: bool = False,
1497
+ systems: Optional[dict] = None,
1498
+ bridges: Optional[Iterable[str]] = None) -> frozenset:
1499
+ r"""The one-letter names the axioms and inductive definitions of THIS theory mention.
1500
+
1501
+ The fixed lines of a theory write their variables free and one letter long
1502
+ (``r_eucl: "r w v ⟹ r w u ⟹ r v u"``, ``const_dom: "existsAt x w"``,
1503
+ ``rk_refl: "rk a w w"``, the world ``w`` of the sort facts, the ``w`` and ``v`` of the
1504
+ introduction rules of ``muntil``). ``axiomatization`` generalises a free name only if no
1505
+ constant of that name is declared, so a constant, predicate, function or sort of the
1506
+ user spelled like one of them would be read INSTEAD of the variable: ``consts x ::
1507
+ "e"`` turns ``existsAt x w`` into a statement about that one constant, and the regime it
1508
+ was meant to state (every object exists at every world) is gone. The theory therefore
1509
+ never gives such a name to a symbol of the user.
1510
+
1511
+ The names are read off the text of the axioms the theory is going to emit (the sort
1512
+ facts with placeholders for the user's names), so a line added to the module is
1513
+ covered without being listed here. Only the lines that are in the theory count: a
1514
+ formula without a modality, a quantifier or a sort has no axiom, and its symbols keep
1515
+ their natural spelling.
1516
+ """
1517
+ sorts = ("SORT",) if nonempty_sort_axioms(original) else ()
1518
+ members = (("SORT", "CONST"),) if sort_membership_axioms(original) else ()
1519
+ lines = list(_collect_axioms(sig, frame, mode, temporal_closure, temporal_def, systems,
1520
+ bridges, sorts, members))
1521
+ if sig.uses_until:
1522
+ lines += _until_block()
1523
+ if sig.uses_since:
1524
+ lines += _since_block()
1525
+ found: set = set()
1526
+ for line in lines:
1527
+ found.update(_ONE_LETTER_NAME.findall(_ISABELLE_MACRO.sub(" ", line)))
1528
+ return frozenset(found)
1529
+
1530
+
1531
+ def _proof_lines(tactic: str, axiom_ids: List[str]) -> List[str]:
1532
+ """The proof block for ``tactic``, bringing the frame/domain axioms into scope.
1533
+
1534
+ For a ``by``-style tactic (:data:`_BY_TACTICS`) we prepend ``using <axioms>`` so
1535
+ ``blast`` / ``auto`` / ``metis`` can actually use them; ``sledgehammer`` / ``oops``
1536
+ / ``sorry`` are emitted verbatim.
1537
+ """
1538
+ body = ISABELLE_TACTICS[tactic]
1539
+ if tactic in _BY_TACTICS and axiom_ids:
1540
+ return [" using " + " ".join(axiom_ids), body]
1541
+ return [body]
1542
+
1543
+
1544
+ # --------------------------------------------------------------------------- #
1545
+ # Type declarations for the discovered signature.
1546
+ # --------------------------------------------------------------------------- #
1547
+
1548
+ def _signature_decls(names: "_IsaNames") -> List[str]:
1549
+ """Emit ``consts`` declarations for predicates, object constants, functions.
1550
+
1551
+ Draws each constant's name from the de-colliding resolver, so distinct source
1552
+ symbols never share a ``consts`` declaration (Isabelle rejects duplicate consts).
1553
+ """
1554
+ out = []
1555
+ for (name, arity), c in sorted(names.pred.items(), key=lambda kv: kv[1]):
1556
+ typ = " \\<Rightarrow> ".join(["e"] * arity + ["i \\<Rightarrow> bool"])
1557
+ out.append(f'consts {c} :: "{typ}"')
1558
+ for name, c in sorted(names.const.items(), key=lambda kv: kv[1]):
1559
+ out.append(f'consts {c} :: "e"')
1560
+ for (name, arity), c in sorted(names.func.items(), key=lambda kv: kv[1]):
1561
+ typ = " \\<Rightarrow> ".join(["e"] * (arity + 1))
1562
+ out.append(f'consts {c} :: "{typ}"')
1563
+ return out
1564
+
1565
+
1566
+ # --------------------------------------------------------------------------- #
1567
+ # Public entry points.
1568
+ # --------------------------------------------------------------------------- #
1569
+
1570
+ def _validate(mode: str, frame: str, tactic: str) -> None:
1571
+ if frame not in _FRAMES:
1572
+ raise ValueError(
1573
+ f"to_isabelle_modal: unknown frame {frame!r} (use one of {sorted(_FRAMES)}).")
1574
+ if mode not in _ACTUALIST_MODES and mode not in _CONSTANT_MODES:
1575
+ raise ValueError(
1576
+ f"to_isabelle_modal: unknown mode {mode!r} "
1577
+ f"(use one of {sorted(_ACTUALIST_MODES | _CONSTANT_MODES)}).")
1578
+ if tactic not in ISABELLE_TACTICS:
1579
+ raise ValueError(
1580
+ f"to_isabelle_modal: unknown tactic {tactic!r} "
1581
+ f"(use one of {sorted(ISABELLE_TACTICS)}).")
1582
+
1583
+
1584
+ def isabelle_modal_theory(
1585
+ formula: Node,
1586
+ mode: str = "constant",
1587
+ frame: str = "K",
1588
+ tactic: str = "sledgehammer",
1589
+ theory_name: str = "ModalEmbedding",
1590
+ temporal_closure: bool = True,
1591
+ proof: Optional[str] = None,
1592
+ temporal_def: bool = False,
1593
+ systems: Optional[dict] = None,
1594
+ bridges: Optional[Iterable[str]] = None,
1595
+ ) -> str:
1596
+ """Emit a complete, loadable Isabelle/HOL theory shallow-embedding ``formula``.
1597
+
1598
+ The returned string is a full ``theory <theory_name> imports Main begin ... end``
1599
+ that:
1600
+
1601
+ - declares the world type ``i`` and the accessibility relations actually used
1602
+ (alethic ``r``; **agent-indexed** epistemic ``rk`` / doxastic ``rb`` of type
1603
+ ``'a \\<Rightarrow> i \\<Rightarrow> i \\<Rightarrow> bool``; deontic ``d``; temporal henceforth ``t`` and
1604
+ one-step ``n``);
1605
+ - defines every lifted operator (``mnot mand mor mimp miff mbox mdia``, the
1606
+ agent-indexed ``knows`` / ``believes``, ``obl`` / ``perm``, ``malways`` /
1607
+ ``meventually`` / ``mnext``, the ``existsAt``-guarded ``mforall`` / ``mexists``,
1608
+ and ``mvalid``) as Isabelle abbreviations;
1609
+ - emits the frame axioms for ``frame`` (K/T/S4/S5/KD/KD45), the domain-regime
1610
+ axioms for ``mode``, and (when ``temporal_closure``) the refl+trans axioms on
1611
+ the temporal relation ``t`` so it denotes the henceforth (closure) relation,
1612
+ faithfully to :func:`satisfies_modal`;
1613
+ - states the formula as a **real** ``lemma "\\<lfloor> ... \\<rfloor>"`` whose body is the
1614
+ *lifted* embedding (not a ``to_unicode_str`` dump in a comment), followed by
1615
+ the proof text for ``tactic``;
1616
+ - for a many-sorted formula, states that every sort the formula uses is
1617
+ non-empty (``nonempty_sort<i>``) and that every sorted constant ``c:S`` is an
1618
+ element of ``S`` at EVERY world (``sort_member<i>``: ``S c w``, unguarded by
1619
+ ``existsAt``, because a constant is rigid and may lie outside the domain of a
1620
+ world). A sort that occurs only through a sorted constant gets its guard
1621
+ declared too. A formula without a sorted constant gets no ``sort_member`` line
1622
+ and its text is unchanged.
1623
+
1624
+ Args:
1625
+ formula: the modal AST node to embed.
1626
+ mode: object-quantifier domain regime — ``constant`` / ``possibilist`` or
1627
+ ``varying`` / ``increasing`` / ``cumulative`` / ``decreasing``.
1628
+ frame: alethic frame system in {K, T, S4, S5, KD, KD45}.
1629
+ tactic: a key of :data:`ISABELLE_TACTICS`. The default ``"sledgehammer"``
1630
+ emits a Sledgehammer hook plus ``oops`` so the theory loads without
1631
+ claiming a proof; ``"metis"`` / ``"auto"`` / ``"blast"`` / ``"smt"`` emit a
1632
+ ``by ...`` proof. For these ``by``-style tactics the proof is prefixed
1633
+ with ``using <frame/domain axioms>`` so an axiom-dependent validity
1634
+ (T/S4/S5/KD/KD45, temporal closure, domain regime) can actually
1635
+ discharge — a bare ``axiomatization`` fact is not in the default claset.
1636
+ ``"sorry"`` admits the goal.
1637
+ theory_name: the Isabelle theory name (must match the .thy filename).
1638
+ proof: an explicit proof block to emit verbatim in place of the ``tactic``
1639
+ preset (used by the Isabelle runner to splice in a prove-battery or a
1640
+ ``nitpick`` invocation). When ``None`` the ``tactic`` preset is used.
1641
+ temporal_closure: when True, constrain ``t`` to be reflexive+transitive so
1642
+ ``Always`` / ``Eventually`` denote the henceforth (refl-trans-closure)
1643
+ reading of :func:`satisfies_modal`. ``Next`` always uses the one-step
1644
+ relation ``n`` regardless.
1645
+ systems: optional per-family frame systems for the AGENT-INDEXED relations,
1646
+ e.g. ``{"epistemic": "S5", "doxastic": "KD45"}`` (families:
1647
+ ``epistemic`` rk, ``doxastic`` rb, ``assertive`` rs, ``bouletic`` rw;
1648
+ values from {K, T, B, S4, S5, KD, KD45}). Each axiom leaves the agent
1649
+ schematic, so the property holds per agent — mirroring
1650
+ ``to_thf_modal_full``'s parameter of the same name. A family whose
1651
+ operators do not occur in the formula is skipped.
1652
+ bridges: optional collection of CROSS-family bridge names (see
1653
+ :data:`BRIDGES`), **off by default**:
1654
+
1655
+ - ``"knowledge_implies_belief"`` — schema ``K_a φ → B_a φ``, frame
1656
+ correspondent ``rb ⊆ rk`` (axiom ``rb_in_rk``);
1657
+ - ``"sincerity"`` — schema ``Say_a φ → B_a φ``, correspondent
1658
+ ``rb ⊆ rs`` (axiom ``rb_in_rs``);
1659
+ - ``"ought_implies_can"`` — schema ``Oφ → ◇φ``, correspondent
1660
+ ``∀w. ∃v. d w v ∧ r w v`` (axiom ``d_meets_r``). Deliberately NOT
1661
+ the folklore ``d ⊆ r``, which measurably fails to validate
1662
+ ``Oφ → ◇φ`` on its own and, with seriality, over-validates
1663
+ ``□φ → Oφ``.
1664
+
1665
+ Each condition is the exact correspondent of its schema, so the
1666
+ bridge adds precisely that principle and nothing stronger. Requesting
1667
+ a bridge whose partner family does not occur in the formula raises
1668
+ ``ValueError`` (its relation is never declared; skipping would emit a
1669
+ weaker logic and declaring it anyway is not conservative) — see
1670
+ :func:`_bridge_axioms`.
1671
+
1672
+ Returns:
1673
+ The theory text (newline-terminated).
1674
+
1675
+ Note:
1676
+ The toolkit only *emits* this theory — it does not run Isabelle or
1677
+ Sledgehammer. First-order modal logic is undecidable, so emission proves
1678
+ nothing; an external prover must discharge the lemma.
1679
+ """
1680
+ _validate(mode, frame, tactic)
1681
+ # Reject a bare string / an unknown bridge name BEFORE any emission work, so a
1682
+ # typo fails fast rather than after the formula has been scanned and lifted.
1683
+ requested_bridges = _validate_bridges(bridges, "to_isabelle_modal")
1684
+
1685
+ # A numeral is a constant identified by its value (1 and 1.0 are one), named ``n1``:
1686
+ # a user constant spelled like it is refused, not merged with it.
1687
+ [formula], _ = numerals_as_constants([formula], where="to_isabelle_modal",
1688
+ spell=prefixed_numeral_name)
1689
+
1690
+ # A many-sorted formula (SortedQuantifier / SortedConstant) is relativized
1691
+ # ONCE, here, into the guarded plain FOL fol.to_fol also builds (∀x:S φ ->
1692
+ # ∀x (S(x) -> φ), etc.) — before _scan/_lift ever see it, rather than
1693
+ # adding a recursive case to either: the resulting sort-guard atom is an
1694
+ # ordinary unary predicate, which _scan/_lift already handle like any
1695
+ # other atom (see fol.qml's module docstring for the identical choice and
1696
+ # why "once, up front" also catches a SortedConstant anywhere in the
1697
+ # formula, not only directly under a SortedQuantifier).
1698
+ original = formula
1699
+ formula = formula._relativize([])
1700
+ # Rigid identity (qml's reading): `≠` -> `¬(=)`, a non-binary `=` refused. A formula
1701
+ # without an identity atom is returned unchanged.
1702
+ formula = _lower_identity(formula, "to_isabelle_modal")
1703
+
1704
+ sig = _Sig()
1705
+ _scan(formula, sig) # which modalities occur (relation/operator blocks)
1706
+ # de-colliding constant names (decls + usages agree); extended by the guard of a
1707
+ # sort that occurs only through a sorted constant, so that guard is declared.
1708
+ names = _names_with_sort_facts(
1709
+ formula, original,
1710
+ _axiom_variable_names(sig, original, frame, mode, temporal_closure, temporal_def,
1711
+ systems, bridges))
1712
+ sort_consts = _sort_consts(original, names)
1713
+ body = _lift(formula, names) # may raise on an unsupported node — do before emitting.
1714
+
1715
+ lines: List[str] = []
1716
+ lines.append(f"theory {theory_name}")
1717
+ lines.append(" imports Main")
1718
+ lines.append("begin")
1719
+ lines.append("")
1720
+ lines.append("(* Shallow semantical embedding (Benzmüller-style) of a quantified modal *)")
1721
+ lines.append(f"(* formula. mode={mode}, frame={frame}, tactic={tactic}. *)")
1722
+ lines.append("(* The toolkit EMITS this theory; it does not run Isabelle/Sledgehammer. *)")
1723
+ lines.append("(* First-order modal logic is undecidable: an external prover must close *)")
1724
+ lines.append("(* the lemma. Equality =/<> is RIGID identity: HOL's own =, no world *)")
1725
+ lines.append("(* argument, as in the first-order embedding (fol.qml). *)")
1726
+ if requested_bridges:
1727
+ # Comment only — the bridge AXIOMS are emitted with the other axioms below.
1728
+ named = ", ".join(n for n in _BRIDGES if n in requested_bridges)
1729
+ lines.append(f"(* cross-family bridges: {named} *)")
1730
+ lines.append("")
1731
+ lines.append("typedecl i \\<comment> \\<open>the type of worlds\\<close>")
1732
+ lines.append("typedecl e \\<comment> \\<open>the type of entities (objects and agents)\\<close>")
1733
+ # NOTE the fixed entity type: with a POLYMORPHIC 'a, Isabelle gives every
1734
+ # occurrence of an agent constant its own type instance, so the two
1735
+ # ``says a`` in ``Say_a(P→Q) → (Say_a P → Say_a Q)`` would read two
1736
+ # INDEPENDENT relation instances and nitpick could "genuinely" refute the
1737
+ # K axiom — a false INVALID observed live. One monomorphic e removes it.
1738
+ lines.append("")
1739
+
1740
+ # Relations + lifted operators for the modalities that actually occur.
1741
+ if sig.uses_alethic:
1742
+ lines += _alethic_block()
1743
+ lines.append("")
1744
+ if sig.uses_epistemic:
1745
+ lines += _epistemic_block()
1746
+ lines.append("")
1747
+ if sig.uses_doxastic:
1748
+ lines += _doxastic_block()
1749
+ lines.append("")
1750
+ if sig.uses_assertive:
1751
+ lines += _assertive_block()
1752
+ lines.append("")
1753
+ if sig.uses_bouletic:
1754
+ lines += _bouletic_block()
1755
+ lines.append("")
1756
+ if sig.uses_deontic:
1757
+ lines += _deontic_block()
1758
+ lines.append("")
1759
+ # When ``temporal_def`` and BOTH Always/Eventually (t) and Next (n) occur, the
1760
+ # henceforth relation is DEFINED as the reflexive-transitive closure of n
1761
+ # (``t = rtranclp n``) rather than a ``consts`` constrained by axioms — so nitpick
1762
+ # can determine t from a candidate n and actually refute non-theorems of the
1763
+ # closure fragment. n must be declared before t's definition references it.
1764
+ use_temporal_def = temporal_def and sig.uses_temporal
1765
+ # The one-step relation n is declared once when Next/Previous/Until/Since
1766
+ # needs it — or when the temporal definition t = rtranclp n references it;
1767
+ # the mnext/mprevious abbreviations only when their operator occurs.
1768
+ if sig.needs_next_rel or use_temporal_def:
1769
+ lines += _next_consts_block()
1770
+ if sig.uses_next:
1771
+ lines += _next_abbrev_block()
1772
+ if sig.uses_previous:
1773
+ lines += _previous_abbrev_block()
1774
+ lines.append("")
1775
+ if sig.uses_temporal:
1776
+ lines += _temporal_def_block() if use_temporal_def else _temporal_block()
1777
+ if sig.uses_past:
1778
+ lines += _past_temporal_block()
1779
+ lines.append("")
1780
+ if sig.uses_hybrid:
1781
+ lines += _hybrid_block(names)
1782
+ lines.append("")
1783
+ # Inductive least-fixpoint definitions for the binary interval operators; they
1784
+ # step over n (declared above), so they follow it.
1785
+ if sig.uses_until:
1786
+ lines += _until_block()
1787
+ lines.append("")
1788
+ if sig.uses_since:
1789
+ lines += _since_block()
1790
+ lines.append("")
1791
+
1792
+ # Connectives + validity (always present).
1793
+ lines += _CORE_ABBREVS
1794
+ lines.append("")
1795
+ if sig.has_quant:
1796
+ lines += _quant_block(mode)
1797
+ lines.append("")
1798
+ elif sort_consts and mode in _ACTUALIST_MODES:
1799
+ # An actualist sort witness (``_nonempty_sort_axioms``) names ``existsAt``;
1800
+ # a formula with no quantifier has no ``_quant_block`` to declare it.
1801
+ lines.append(_EXISTS_AT_DECL)
1802
+ lines.append("")
1803
+ lines += _VALID_ABBREV
1804
+ lines.append("")
1805
+
1806
+ # Signature of the user's predicates / constants / functions.
1807
+ decls = _signature_decls(names)
1808
+ if decls:
1809
+ lines += decls
1810
+ lines.append("")
1811
+
1812
+ # Axioms.
1813
+ axioms = _collect_axioms(sig, frame, mode, temporal_closure, temporal_def,
1814
+ systems, bridges, sort_consts,
1815
+ _sort_members(original, names))
1816
+ if axioms:
1817
+ lines += axioms
1818
+ lines.append("")
1819
+
1820
+ # The real lemma. Its proof brings the frame/domain axioms into scope (see
1821
+ # _proof_lines) so an axiom-dependent validity actually discharges; an explicit
1822
+ # ``proof`` override (used by the runner's prove-battery / nitpick) wins.
1823
+ lines.append(f'lemma modal_goal: "\\<lfloor> {body} \\<rfloor>"')
1824
+ if proof is not None:
1825
+ lines.append(proof.rstrip("\n"))
1826
+ else:
1827
+ lines += _proof_lines(tactic, _axiom_names(axioms))
1828
+ lines.append("")
1829
+ lines.append("end")
1830
+ return "\n".join(lines) + "\n"
1831
+
1832
+
1833
+ def modal_axiom_names(
1834
+ formula: Node,
1835
+ mode: str = "constant",
1836
+ frame: str = "K",
1837
+ temporal_closure: bool = True,
1838
+ systems: Optional[dict] = None,
1839
+ bridges: Optional[Iterable[str]] = None,
1840
+ ) -> List[str]:
1841
+ """The names of the ``axiomatization`` facts the emitted theory would declare.
1842
+
1843
+ These are exactly the frame / domain / temporal-link / agent-system / bridge
1844
+ axioms in scope for the proof of ``modal_goal``. The Isabelle runner needs them
1845
+ to build a ``using <axioms> by <method>`` proof (or to know none are required),
1846
+ so this exposes the same computation :func:`isabelle_modal_theory` uses
1847
+ internally.
1848
+
1849
+ Passing ``bridges`` here is load-bearing, not cosmetic: an ``axiomatization``
1850
+ fact is not in the default claset/simpset, so a validity that holds only
1851
+ because of a bridge (``K_a P → B_a P`` under ``knowledge_implies_belief``, say)
1852
+ cannot be discharged by a ``by blast`` that does not ``using rb_in_rk`` — the
1853
+ same failure mode the :data:`_BY_TACTICS` comment documents for the frame
1854
+ axioms.
1855
+ """
1856
+ _validate(mode, frame, "oops")
1857
+ # The same numeral rule as isabelle_modal_theory: a numeral spelled like a constant is refused.
1858
+ [formula], _ = numerals_as_constants([formula], where="modal_axiom_names",
1859
+ spell=prefixed_numeral_name)
1860
+ sig = _Sig()
1861
+ # Relativize first, same as isabelle_modal_theory — _scan's has_quant flag
1862
+ # only needs to know a quantifier occurs, sorted or not, and a sort guard
1863
+ # is an ordinary atom to it either way; see that function's comment.
1864
+ relativized = _lower_identity(formula._relativize([]), "modal_axiom_names")
1865
+ _scan(relativized, sig)
1866
+ names = _names_with_sort_facts(
1867
+ relativized, formula,
1868
+ _axiom_variable_names(sig, formula, frame, mode, temporal_closure, False,
1869
+ systems, bridges))
1870
+ return _axiom_names(_collect_axioms(
1871
+ sig, frame, mode, temporal_closure, systems=systems, bridges=bridges,
1872
+ sorts=_sort_consts(formula, names),
1873
+ members=_sort_members(formula, names)))
1874
+
1875
+
1876
+ def to_isabelle_modal(
1877
+ formula: Node,
1878
+ mode: str = "constant",
1879
+ frame: str = "K",
1880
+ tactic: str = "sledgehammer",
1881
+ temporal_closure: bool = True,
1882
+ proof: Optional[str] = None,
1883
+ systems: Optional[dict] = None,
1884
+ bridges: Optional[Iterable[str]] = None,
1885
+ ) -> str:
1886
+ """Emit a complete, loadable Isabelle/HOL theory embedding ``formula`` (real lemma).
1887
+
1888
+ This is the genuine replacement for the old skeleton stub: it returns a full
1889
+ ``theory ModalEmbedding imports Main begin ... end`` with all lifted operators
1890
+ defined as abbreviations and the formula stated as a **real** ``lemma`` (not a
1891
+ comment). See :func:`isabelle_modal_theory` for the parameters; this thin
1892
+ wrapper fixes ``theory_name="ModalEmbedding"`` to match the historical
1893
+ signature ``to_isabelle_modal(formula, mode, frame)``.
1894
+
1895
+ ``bridges`` is the opt-in cross-family option (``knowledge_implies_belief`` /
1896
+ ``sincerity`` / ``ought_implies_can``, see :data:`BRIDGES`); it is off by
1897
+ default and raises ``ValueError`` when the formula does not mention both of a
1898
+ requested bridge's families.
1899
+
1900
+ Honesty: the toolkit emits the theory but never runs Isabelle/Sledgehammer.
1901
+ First-order modal logic is undecidable, so emission proves nothing — an
1902
+ external prover must discharge the lemma. Propositional modal K/T/S4/S5 are
1903
+ decidable, but this emitter does not itself decide them.
1904
+ """
1905
+ return isabelle_modal_theory(
1906
+ formula, mode=mode, frame=frame, tactic=tactic,
1907
+ theory_name="ModalEmbedding", temporal_closure=temporal_closure, proof=proof,
1908
+ systems=systems, bridges=bridges)