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,1743 @@
1
+ r"""Third-order MODAL logic → HOL: the shallow embedding, worlds made explicit.
2
+
3
+ Third-order modal logic is where a formula can say ``Positive(G)`` — a predicate
4
+ of a PROPERTY — inside a ``□``. Nothing first-order carries it and no
5
+ propositional modal calculus reaches it, but a higher-order prover does, through
6
+ the same shallow (Benzmüller-style) embedding the rest of this package uses: a
7
+ proposition is not a truth value but a FUNCTION FROM WORLDS to truth values, and
8
+ every connective is lifted to act pointwise on those functions. The modal
9
+ operators then become ordinary quantifiers over the accessibility relation, and
10
+ what a HOL prover sees is plain higher-order logic.
11
+
12
+ The three levels get three types::
13
+
14
+ i individuals
15
+ world worlds
16
+ sigma = world => bool propositions (a "truth value" per world)
17
+ i => sigma properties (the third order's argument type)
18
+ (i => sigma) => sigma predicates OF properties -- e.g. Positive
19
+
20
+ and the lifted vocabulary is emitted as Isabelle ``abbreviation``\ s rather than
21
+ ``definition``\ s **on purpose**: an abbreviation is unfolded by the parser, so
22
+ `blast`/`metis`/`auto` see through the embedding to plain HOL without being told
23
+ to unfold anything. A ``definition`` would hide the goal behind a constant and
24
+ turn every proof into an unfolding exercise.
25
+
26
+ ``mall`` / ``mex`` are polymorphic (``('a => sigma) => sigma``), so ONE pair of
27
+ binders serves individual quantification and property quantification alike —
28
+ which is the embedding's own reason for existing: the object logic's orders are
29
+ distinguished by the TYPE at the binder, not by separate machinery.
30
+
31
+ **What this module emits, and what it does not.** It emits a self-contained
32
+ Isabelle theory (or a THF problem) — types, lifted vocabulary, the frame axioms
33
+ for the chosen system, the signature read off the formulas, the axioms, and the
34
+ goals with whatever proof text the caller supplies. It does not run a prover;
35
+ :func:`unicode_logic_kit.hol.isabelle_runner.check_theory` does that.
36
+
37
+ **Fragment.** Alethic ``□`` / ``◇``, agent-indexed epistemic/doxastic/assertive/
38
+ bouletic (``K_a`` / ``B_a`` / ``Say_a`` / ``Want_a``), deontic (``O`` / ``P``,
39
+ serial), temporal (``Always`` / ``Eventually`` / ``Next`` / ``Until`` / ``Since``
40
+ and their past mirrors ``Historically`` / ``Once`` / ``Previous``) and hybrid
41
+ (nominals / ``@``) — the same non-counterfactual family
42
+ :mod:`unicode_logic_kit.hol.thf_modal` / :mod:`unicode_logic_kit.hol.isabelle_modal`
43
+ carry at first order, ported here to the third-order shallow embedding rather
44
+ than reinvented. Two things stay refused BY NAME rather than approximated: the
45
+ Lewis counterfactuals ``Would`` / ``Might`` (they read a similarity ordering of
46
+ worlds, not an accessibility relation — see
47
+ :mod:`unicode_logic_kit.hol.isabelle_conditional`), and the GROUP-epistemic
48
+ operators ``EverybodyKnows`` / ``DistributedKnowledge`` / ``CommonKnowledge``
49
+ (``C_G`` needs a transitive closure this embedding does not attempt).
50
+
51
+ **Equality is rigid, or refused.** ``t₁ = t₂`` / ``t₁ ≠ t₂`` between INDIVIDUALS is
52
+ read exactly as :func:`unicode_logic_kit.fol.qml.qml_is_valid`,
53
+ :mod:`unicode_logic_kit.hol.thf_modal` and :mod:`unicode_logic_kit.hol.isabelle_modal`
54
+ read it: HOL's own ``=`` over the individual type ``i`` with NO world argument
55
+ (Isabelle ``(\<lambda>_. a = b)``, THF the ``meq`` macro of ``thf_modal``), so it
56
+ cannot vary with the world. ``a = a`` and ``a = b → □(a = b)`` are theorems,
57
+ ``□(a = b) → a = b`` holds exactly on a SERIAL frame (every world has a successor,
58
+ which every reflexive frame does), and ``a = a`` holds under every ``mode=``
59
+ (identity is not existence-guarded). ``≠`` is lowered to
60
+ ``¬(=)`` first, by :func:`unicode_logic_kit.hol.isabelle_modal._lower_identity` — which is
61
+ :func:`unicode_logic_kit.fol.qml._st_equality` — and nothing is declared for either: no
62
+ ``feq`` / ``fneq`` exists in the emitted theory or problem. Two cases are refused, both
63
+ loudly, both before anything is rendered:
64
+
65
+ * a ``=`` / ``≠`` atom that does not have exactly two terms raises ``ValueError``
66
+ (``qml``'s rule: the glyphs are reserved for identity and are never a world-relative
67
+ predicate);
68
+ * identity at a PROPERTY type — a predicate name or a λ-abstraction as a term of ``=`` —
69
+ raises ``NotImplementedError`` through
70
+ :func:`unicode_logic_kit.semantics._modal_reject.reject_equality`. That is not rigid
71
+ OBJECT identity but a different question. The grammar cannot even write it, the
72
+ signature analysis types the two slots of ``=`` independently (so ``G = H`` with ``G``
73
+ unary and ``H`` binary would pass and be ill-typed), and HOL's ``=`` at ``i ⇒ σ`` is
74
+ one particular answer — necessary coextension — to a question the kit has no oracle
75
+ for. State the relation you mean (``∀x □(P(x) ↔ Q(x))``) instead.
76
+
77
+ The ordering atoms ``<`` ``>`` ``≤`` ``≥`` stay ordinary uninterpreted relations,
78
+ world-relativised, as in ``qml``.
79
+
80
+ **Constants and free variables.** A constant is a particular individual that its name
81
+ stands for; a free variable is a parameter of the problem, one unknown element shared by
82
+ every formula (see :mod:`unicode_logic_kit.fol._free_parameters`). The two are different
83
+ symbols even when they are spelled alike, so ``P(x)`` with a free ``x`` and ``P('x')`` are
84
+ two statements: each is declared under a name of its own (``x`` for the variable, ``x_2``
85
+ for the constant) and a constant is never looked up among the bound variables, so no binder
86
+ of any name captures it. A constant of any name has a legal identifier in both targets (a
87
+ name that is an Isabelle identifier is written as it is, any other name under the kit's ASCII
88
+ stem of it; THF always uses the stem), and every symbol of the problem — predicate, function,
89
+ free variable, constant, and the embedding's own vocabulary (``R``, ``mall``, ``Rk``, ``nom_i``,
90
+ …) — gets a name of its own, so two names that share a stem are still two symbols.
91
+
92
+ **Domains — two independent axes.** ``mode=`` (default ``"constant"``, i.e.
93
+ possibilist) accepts the same domain-regime vocabulary as
94
+ :mod:`unicode_logic_kit.hol.isabelle_modal`'s ``_ACTUALIST_MODES``: ``"varying"``,
95
+ ``"increasing"``, ``"cumulative"``, ``"decreasing"``, on top of the
96
+ constant/possibilist default. The **one genuinely new judgment call this port
97
+ makes** (first order has no property quantifier to decide about): the
98
+ ``existsAt``-guard an actualist mode adds applies ONLY to the
99
+ INDIVIDUAL-typed use of the polymorphic binder (a :class:`~unicode_logic_kit.fol.nodes.Quantifier`
100
+ node, ranging over type ``i``) — a PROPERTY quantifier
101
+ (:class:`~unicode_logic_kit.fol.nodes.SecondOrderQuantifier`, type ``i ⇒ sigma``)
102
+ always stays ``mall``/``mex``, unguarded, constant across worlds, matching how
103
+ the ontological-argument literature and Fitting-style actualist QML treat
104
+ properties (an object may cease to exist; the property "being golden" does not
105
+ along with it). Under the default constant mode, individual quantification is
106
+ `mall`/`mex` too — textually and semantically identical to today, so
107
+ ``hol.goedel`` and this module's own docs example are unaffected; only an
108
+ actualist ``mode=`` pays for the ``existsAt :: i ⇒ world ⇒ bool`` guard's
109
+ extra vocabulary and axioms, reusing the pattern of isabelle_modal.py's
110
+ ``_quant_block``/``_domain_axioms`` (Isabelle abbreviations) and thf_modal.py's
111
+ ``mforall``/``mexists`` THF definitions plus their domain axioms.
112
+ """
113
+
114
+ from dataclasses import dataclass
115
+ from typing import Dict, List, Optional, Sequence
116
+
117
+ from ..fol.nodes import (
118
+ Node, Atom, Not, And, Or, Xor, Implies, Iff, Quantifier,
119
+ SecondOrderQuantifier, PredicateTerm,
120
+ Variable, Constant, Function, LambdaVar, Lambda,
121
+ Box, Diamond,
122
+ Knows, Believes, Says, Wants,
123
+ Obligatory, Permitted,
124
+ Always, Eventually, Next, Until, Since,
125
+ Historically, Once, Previous,
126
+ Nominal, At,
127
+ analyse_signatures,
128
+ )
129
+ # Down (the ↓ binder, N1) is not yet re-exported through fol.nodes / fol's
130
+ # public __init__ / the top-level unicode_logic_kit package (that three-file
131
+ # edit is outside this change's file ownership — see the change's own
132
+ # report); imported directly from its defining module in the meantime, the
133
+ # same class object either import path would give.
134
+ from ..fol._hybrid_nodes import Down
135
+ from ..fol._ho_nodes import INDIVIDUAL
136
+ from ..fol._truth_constants import truth_value
137
+ from ..fol.frames import FRAMES, resolve_frame, UnsupportedFrameCondition
138
+ from ._ho_common import (
139
+ UnsupportedHigherOrderNode, ORDERING, FREE_VARIABLE, CONSTANT, ISABELLE_BUILT_IN,
140
+ peel_lambdas, rename_apart, bound_pred_names, atom_predicates,
141
+ function_symbols, individual_symbols, ThfNames, IsabelleNames, bound_token,
142
+ )
143
+ from ._isabelle_binders import (
144
+ PREDICATE, VARIABLE, BinderScope, binder_tokens, collect_binders, declared_names,
145
+ )
146
+ from ..semantics._modal_reject import (
147
+ EQUALITY_PREDICATES, is_equality_atom, reject_equality,
148
+ )
149
+ # Single-sourced domain-regime vocabulary: importing the frozensets (not the
150
+ # axiom TEXT, which stays local -- see the module docstring) keeps this
151
+ # module's ``mode=`` meaning the same set of names as the first-order sibling
152
+ # it ports the actualist guard from, so the two can never drift apart about
153
+ # what "varying" or "constant" means.
154
+ from .isabelle_modal import _ACTUALIST_MODES, _CONSTANT_MODES
155
+ # Rigid identity is single-sourced the same way: the lowering (``≠`` -> ``¬(=)``,
156
+ # a non-binary atom refused) is fol.qml._st_equality reached through
157
+ # isabelle_modal._lower_identity, and the THF macro that lifts HOL's own ``=``
158
+ # is thf_modal's -- so the first-order, second-order and third-order modal routes
159
+ # cannot disagree about what an identity atom is.
160
+ from .isabelle_modal import _has_identity, _lower_identity
161
+ from .thf_modal import _THF_RIGID_EQ, _THF_RIGID_EQ_DEF
162
+
163
+
164
+ # Isabelle's ASCII escapes, spelled once.
165
+ _ALL = r"\<forall>"
166
+ _EX = r"\<exists>"
167
+ _AND = r"\<and>"
168
+ _OR = r"\<or>"
169
+ _IMP = r"\<longrightarrow>"
170
+ _NOT = r"\<not>"
171
+ _LAM = r"\<lambda>"
172
+ _FUN = r"\<Rightarrow>"
173
+ _EQUIV = r"\<equiv>"
174
+ _NEQ = r"\<noteq>"
175
+
176
+
177
+ # --------------------------------------------------------------------------
178
+ # The lifted vocabulary
179
+ # --------------------------------------------------------------------------
180
+
181
+ _DEFINITIONS = [
182
+ 'typedecl i \\<comment> \\<open>individuals\\<close>',
183
+ 'typedecl world \\<comment> \\<open>worlds\\<close>',
184
+ f'type_synonym sigma = "world {_FUN} bool"'
185
+ ' \\<comment> \\<open>a proposition: one truth value per world\\<close>',
186
+ '',
187
+ f'consts R :: "world {_FUN} world {_FUN} bool"'
188
+ ' \\<comment> \\<open>accessibility\\<close>',
189
+ '',
190
+ '\\<comment> \\<open>Abbreviations, not definitions: the automation must see '
191
+ 'THROUGH the embedding.\\<close>',
192
+ f'abbreviation mnot :: "sigma {_FUN} sigma"'
193
+ f' where "mnot p {_EQUIV} {_LAM}v. {_NOT} p v"',
194
+ f'abbreviation mand :: "sigma {_FUN} sigma {_FUN} sigma"'
195
+ f' where "mand p q {_EQUIV} {_LAM}v. p v {_AND} q v"',
196
+ f'abbreviation mor :: "sigma {_FUN} sigma {_FUN} sigma"'
197
+ f' where "mor p q {_EQUIV} {_LAM}v. p v {_OR} q v"',
198
+ f'abbreviation mimp :: "sigma {_FUN} sigma {_FUN} sigma"'
199
+ f' where "mimp p q {_EQUIV} {_LAM}v. p v {_IMP} q v"',
200
+ f'abbreviation miff :: "sigma {_FUN} sigma {_FUN} sigma"'
201
+ f' where "miff p q {_EQUIV} {_LAM}v. p v = q v"',
202
+ f'abbreviation mxor :: "sigma {_FUN} sigma {_FUN} sigma"'
203
+ f' where "mxor p q {_EQUIV} {_LAM}v. p v {_NEQ} q v"',
204
+ f'abbreviation mbox :: "sigma {_FUN} sigma"'
205
+ f' where "mbox p {_EQUIV} {_LAM}v. {_ALL}u. R v u {_IMP} p u"',
206
+ f'abbreviation mdia :: "sigma {_FUN} sigma"'
207
+ f' where "mdia p {_EQUIV} {_LAM}v. {_EX}u. R v u {_AND} p u"',
208
+ f'abbreviation mall :: "(\'a {_FUN} sigma) {_FUN} sigma"'
209
+ f' where "mall P {_EQUIV} {_LAM}v. {_ALL}x. P x v"',
210
+ f'abbreviation mex :: "(\'a {_FUN} sigma) {_FUN} sigma"'
211
+ f' where "mex P {_EQUIV} {_LAM}v. {_EX}x. P x v"',
212
+ f'abbreviation mvalid :: "sigma {_FUN} bool"'
213
+ f' where "mvalid p {_EQUIV} {_ALL}v. p v"',
214
+ ]
215
+
216
+
217
+ def ho_modal_definitions() -> str:
218
+ """Return the Isabelle preamble: the three types, ``R``, and the lifted vocabulary.
219
+
220
+ Emitted verbatim by :func:`isabelle_ho_modal_theory`; exposed separately so a
221
+ caller writing its own theory around the same embedding can reuse exactly
222
+ the vocabulary the kit's emitted goals are stated in.
223
+ """
224
+ return "\n".join(_DEFINITIONS)
225
+
226
+
227
+ # --------------------------------------------------------------------------
228
+ # Modal-family usage scan
229
+ # --------------------------------------------------------------------------
230
+ #
231
+ # What relations/vocabulary a theory needs is a property of the FORMULAS, not
232
+ # of a caller-supplied flag -- so it is read off the AST once, the same way
233
+ # hol.isabelle_modal's ``_Sig``/``_scan`` do at first order. Unlike that
234
+ # scanner this one does not ALSO collect the free signature (analyse_signatures
235
+ # / _signature_lines already do that, over the SAME third-order AST both HOL
236
+ # exporters share); it only answers "which modal families occur", which is
237
+ # what decides which relation consts / abbreviations / axioms this theory
238
+ # needs.
239
+
240
+ class _FamilyUsage:
241
+ """Which non-alethic modal families a formula set uses, and whether it quantifies."""
242
+
243
+ def __init__(self):
244
+ self.epistemic = False # Knows -- relation Rk
245
+ self.doxastic = False # Believes -- relation Rb
246
+ self.assertive = False # Says -- relation Rs
247
+ self.bouletic = False # Wants -- relation Rw
248
+ self.deontic = False # Obligatory / Permitted -- relation Rd (serial)
249
+ self.temporal = False # Always / Eventually / Historically / Once -- Rt
250
+ self.next_ = False # Next -- Rn
251
+ self.previous = False # Previous (converse-of-Rn reader)
252
+ self.until = False # Until (inductive muntil over Rn)
253
+ self.since = False # Since (inductive msince over converse Rn)
254
+ self.hybrid = False # Nominal / At -- world constants Nom_<name>
255
+ self.has_quant = False # a plain individual Quantifier occurs
256
+
257
+ @property
258
+ def needs_next_rel(self) -> bool:
259
+ """Whether the one-step relation ``Rn`` must be declared.
260
+
261
+ Needed by ``Next`` (mnext), ``Previous`` (mprevious -- its converse),
262
+ and the inductive ``muntil`` / ``msince`` (one-step forward/backward
263
+ path search) -- exactly mirroring
264
+ :attr:`unicode_logic_kit.hol.isabelle_modal._Sig.needs_next_rel`.
265
+ """
266
+ return self.next_ or self.previous or self.until or self.since
267
+
268
+
269
+ # The four agent-indexed families a ``systems=`` dict may name -- each is
270
+ # also the exact attribute name on _FamilyUsage that reports whether the
271
+ # formula set uses it (see _validate_systems).
272
+ _AGENT_FAMILIES = frozenset({"epistemic", "doxastic", "assertive", "bouletic"})
273
+
274
+
275
+ def _isa_family_rel(family: str) -> str:
276
+ """The Isabelle relation constant for an agent-indexed family."""
277
+ return {"epistemic": "Rk", "doxastic": "Rb",
278
+ "assertive": "Rs", "bouletic": "Rw"}[family]
279
+
280
+
281
+ def _scan_family_usage(formulas: Sequence[Node]) -> _FamilyUsage:
282
+ """Walk ``formulas`` and report which modal families / quantifiers occur.
283
+
284
+ Uses the generic :meth:`~unicode_logic_kit.fol._fol_nodes.Node.walk`, which
285
+ reaches every structural child regardless of node type (including a
286
+ :class:`~unicode_logic_kit.fol.nodes.Knows`/``Obligatory``/... node's own
287
+ ``agent``/``formula`` fields) -- so a new node type nested anywhere in the
288
+ tree is still visited; only the classification below is by hand.
289
+ """
290
+ usage = _FamilyUsage()
291
+ for formula in formulas:
292
+ for node in formula.walk():
293
+ if isinstance(node, Down):
294
+ # Checked FIRST (ahead of even the generic walk classification
295
+ # below) so a ↓-formula is refused before any vocab/signature
296
+ # work happens on it — never let it fall through to the
297
+ # generic "hybrid" bucket (True for Down too, for free, since
298
+ # Down.variable is itself a Nominal) unnoticed.
299
+ raise UnsupportedHigherOrderNode(_NO_DOWN)
300
+ if isinstance(node, Knows):
301
+ usage.epistemic = True
302
+ elif isinstance(node, Believes):
303
+ usage.doxastic = True
304
+ elif isinstance(node, Says):
305
+ usage.assertive = True
306
+ elif isinstance(node, Wants):
307
+ usage.bouletic = True
308
+ elif isinstance(node, (Obligatory, Permitted)):
309
+ usage.deontic = True
310
+ elif isinstance(node, (Always, Eventually)):
311
+ usage.temporal = True
312
+ elif isinstance(node, (Historically, Once)):
313
+ usage.temporal = True
314
+ elif isinstance(node, Next):
315
+ usage.next_ = True
316
+ elif isinstance(node, Previous):
317
+ usage.previous = True
318
+ elif isinstance(node, Until):
319
+ usage.until = True
320
+ elif isinstance(node, Since):
321
+ usage.since = True
322
+ elif isinstance(node, (Nominal, At)):
323
+ usage.hybrid = True
324
+ elif isinstance(node, Quantifier):
325
+ usage.has_quant = True
326
+ return usage
327
+
328
+
329
+ # --------------------------------------------------------------------------
330
+ # Agent-indexed per-system frame axioms (K_a / B_a / Say_a / Want_a)
331
+ # --------------------------------------------------------------------------
332
+ #
333
+ # Ported verbatim (same five Horn schemas, same "leave the agent schematic"
334
+ # reading) from hol.isabelle_modal's _AGENT_CONDS -- kept as its own local
335
+ # copy rather than imported so the schema text stays self-contained in this
336
+ # file like everything else here, the same choice thf_modal.py makes for its
337
+ # own _agent_frame_axioms rather than importing isabelle_modal's Isabelle text.
338
+ _ISA_AGENT_CONDS = {
339
+ "refl": '{tag}_refl: "{_ALL}a w. {rel} a w w"',
340
+ "trans": ('{tag}_trans: "{_ALL}a w v u. {rel} a w v {_IMP} '
341
+ '{rel} a v u {_IMP} {rel} a w u"'),
342
+ "sym": '{tag}_sym: "{_ALL}a w v. {rel} a w v {_IMP} {rel} a v w"',
343
+ "serial": '{tag}_serial: "{_ALL}a w. {_EX}v. {rel} a w v"',
344
+ "eucl": ('{tag}_eucl: "{_ALL}a w v u. {rel} a w v {_IMP} '
345
+ '{rel} a w u {_IMP} {rel} a v u"'),
346
+ }
347
+
348
+
349
+ def _agent_system_axioms(family: str, system: str) -> List[str]:
350
+ """Per-agent frame axioms constraining ``family``'s relation under ``system``.
351
+
352
+ Each axiom universally quantifies the agent explicitly (``\\<forall>a``), so
353
+ the property holds for EVERY agent -- matching
354
+ :func:`unicode_logic_kit.hol.isabelle_modal._agent_system_axioms`'s reading
355
+ and, semantically, the per-agent relations of
356
+ :func:`unicode_logic_kit.semantics.kripke.satisfies_modal`. Only systems
357
+ whose conditions are plain Horn frame properties are accepted; a system
358
+ needing Löb / directedness / connectedness has no per-agent schema here
359
+ and raises rather than silently emitting a WEAKER logic than requested --
360
+ the same refusal :func:`~unicode_logic_kit.hol.isabelle_modal._agent_system_axioms`
361
+ makes.
362
+ """
363
+ if system not in FRAMES:
364
+ raise ValueError(
365
+ f"isabelle_ho_modal_theory: unknown system {system!r} for "
366
+ f"{family} (use one of {sorted(FRAMES)})."
367
+ )
368
+ conditions = resolve_frame(system)
369
+ unsupported = [c for c in conditions if c not in _ISA_AGENT_CONDS]
370
+ if unsupported:
371
+ raise NotImplementedError(
372
+ f"isabelle_ho_modal_theory: system {system!r} for {family} needs "
373
+ f"the frame condition(s) {unsupported}, which have no per-agent "
374
+ f"axiom schema here; use frame= on the alethic relation for those "
375
+ f"systems."
376
+ )
377
+ rel = _isa_family_rel(family)
378
+ return [
379
+ "axiomatization where " + _ISA_AGENT_CONDS[c].format(
380
+ rel=rel, tag=rel, _ALL=_ALL, _EX=_EX, _IMP=_IMP)
381
+ for c in conditions
382
+ ]
383
+
384
+
385
+ def _validate_systems(usage: _FamilyUsage, systems: Optional[dict],
386
+ caller: str = "isabelle_ho_modal_theory") -> dict:
387
+ """Validate ``systems=`` and return it (``{}`` if ``None``).
388
+
389
+ Every key must be one of the four agent-indexed families; a family named
390
+ that the formula set never actually uses is refused (the axiom would be
391
+ dead weight the caller never asked to constrain, and silently accepting
392
+ it risks a typo -- e.g. ``"epistemc"`` -- going unnoticed). Shared between
393
+ :func:`isabelle_ho_modal_theory` and :func:`to_thf_ho_modal` (``caller``
394
+ only changes what the error names), so the two HOL routes can never
395
+ accept a different ``systems=`` for the same input.
396
+ """
397
+ if not systems:
398
+ return {}
399
+ unknown = sorted(set(systems) - set(_AGENT_FAMILIES))
400
+ if unknown:
401
+ raise ValueError(
402
+ f"{caller}: unknown systems= famil{'y' if len(unknown) == 1 else 'ies'} "
403
+ f"{unknown} (use any of {sorted(_AGENT_FAMILIES)})."
404
+ )
405
+ unused = [fam for fam in systems if not getattr(usage, fam)]
406
+ if unused:
407
+ raise ValueError(
408
+ f"{caller}: systems= names {sorted(unused)}, but "
409
+ f"the formula set contains no matching operator "
410
+ f"(K_a / B_a / Say_a / Want_a respectively)."
411
+ )
412
+ return dict(systems)
413
+
414
+
415
+ # --------------------------------------------------------------------------
416
+ # Extended vocabulary: agent-indexed / deontic / temporal / hybrid families
417
+ # --------------------------------------------------------------------------
418
+ #
419
+ # Emitted CONDITIONALLY (only the relations/abbreviations a formula set
420
+ # actually needs), matching hol.isabelle_modal's own conditional emission --
421
+ # unlike this module's THF side (see _THF_FAMILY_DEFS below), which follows
422
+ # thf_modal.py's convention of declaring every relation unconditionally.
423
+ # Isabelle keeps the conditional discipline the ALETHIC frame axioms already
424
+ # use here (``if frame_lines: ...``), so a theory that never mentions K_a
425
+ # never declares Rk either.
426
+
427
+ def _family_vocab_lines(usage: _FamilyUsage) -> List[str]:
428
+ """``consts``/``abbreviation`` lines for every family ``usage`` reports used."""
429
+ out: List[str] = []
430
+ for family in ("epistemic", "doxastic", "assertive", "bouletic"):
431
+ if not getattr(usage, family):
432
+ continue
433
+ rel = _isa_family_rel(family)
434
+ macro = {"epistemic": "mknows", "doxastic": "mbelieves",
435
+ "assertive": "msays", "bouletic": "mwants"}[family]
436
+ out.append(f'consts {rel} :: "i {_FUN} world {_FUN} world {_FUN} bool"')
437
+ out.append(f'abbreviation {macro} :: "i {_FUN} sigma {_FUN} sigma" where')
438
+ out.append(f' "{macro} a p {_EQUIV} {_LAM}v. {_ALL}u. {rel} a v u {_IMP} p u"')
439
+ if usage.deontic:
440
+ out += [
441
+ f'consts Rd :: "world {_FUN} world {_FUN} bool"',
442
+ f'abbreviation mobl :: "sigma {_FUN} sigma"'
443
+ f' where "mobl p {_EQUIV} {_LAM}v. {_ALL}u. Rd v u {_IMP} p u"',
444
+ f'abbreviation mperm :: "sigma {_FUN} sigma"'
445
+ f' where "mperm p {_EQUIV} {_LAM}v. {_EX}u. Rd v u {_AND} p u"',
446
+ ]
447
+ if usage.temporal:
448
+ out += [
449
+ f'consts Rt :: "world {_FUN} world {_FUN} bool"'
450
+ ' \\<comment> \\<open>henceforth (reflexive-transitive) reader for Always/Eventually\\<close>',
451
+ f'abbreviation malways :: "sigma {_FUN} sigma"'
452
+ f' where "malways p {_EQUIV} {_LAM}v. {_ALL}u. Rt v u {_IMP} p u"',
453
+ f'abbreviation meventually :: "sigma {_FUN} sigma"'
454
+ f' where "meventually p {_EQUIV} {_LAM}v. {_EX}u. Rt v u {_AND} p u"',
455
+ f'abbreviation mhistorically :: "sigma {_FUN} sigma"'
456
+ f' where "mhistorically p {_EQUIV} {_LAM}v. {_ALL}u. Rt u v {_IMP} p u"',
457
+ f'abbreviation monce :: "sigma {_FUN} sigma"'
458
+ f' where "monce p {_EQUIV} {_LAM}v. {_EX}u. Rt u v {_AND} p u"',
459
+ ]
460
+ if usage.needs_next_rel:
461
+ out.append(
462
+ f'consts Rn :: "world {_FUN} world {_FUN} bool"'
463
+ ' \\<comment> \\<open>one-step reader for Next/Previous/Until/Since\\<close>')
464
+ if usage.next_:
465
+ out.append(f'abbreviation mnext :: "sigma {_FUN} sigma"'
466
+ f' where "mnext p {_EQUIV} {_LAM}v. {_ALL}u. Rn v u {_IMP} p u"')
467
+ if usage.previous:
468
+ out.append(f'abbreviation mprevious :: "sigma {_FUN} sigma"'
469
+ f' where "mprevious p {_EQUIV} {_LAM}v. {_ALL}u. Rn u v {_IMP} p u"')
470
+ if usage.until:
471
+ # Strong Until as a least-fixpoint INDUCTIVE predicate over Rn -- not
472
+ # an abbreviation, because it is a genuine recursive definition, the
473
+ # same reason hol.isabelle_modal's own ``inductive muntil`` is not one
474
+ # either (see that module's _until_block). ``phi``/``psi`` are
475
+ # world-indexed (type sigma, i.e. exactly isabelle_modal's "i =>
476
+ # bool" reading at THAT module's world type), so muntil phi psi
477
+ # partially applied already has type sigma.
478
+ out += [
479
+ 'inductive muntil :: "sigma \\<Rightarrow> sigma \\<Rightarrow> world \\<Rightarrow> bool"',
480
+ ' for phi :: sigma and psi :: sigma where',
481
+ ' muntil_base: "psi w \\<Longrightarrow> muntil phi psi w"',
482
+ '| muntil_step: "phi w \\<Longrightarrow> Rn w v \\<Longrightarrow> muntil phi psi v '
483
+ '\\<Longrightarrow> muntil phi psi w"',
484
+ ]
485
+ if usage.since:
486
+ out += [
487
+ 'inductive msince :: "sigma \\<Rightarrow> sigma \\<Rightarrow> world \\<Rightarrow> bool"',
488
+ ' for phi :: sigma and psi :: sigma where',
489
+ ' msince_base: "psi w \\<Longrightarrow> msince phi psi w"',
490
+ '| msince_step: "phi w \\<Longrightarrow> Rn v w \\<Longrightarrow> msince phi psi v '
491
+ '\\<Longrightarrow> msince phi psi w"',
492
+ ]
493
+ return out
494
+
495
+
496
+ def _hybrid_vocab_lines(nominal_consts: Sequence[str]) -> List[str]:
497
+ """``consts nom_<name> :: world`` -- one world constant per nominal (H(@))."""
498
+ return [f'consts {c} :: "world"'
499
+ ' \\<comment> \\<open>the world named by a hybrid nominal\\<close>'
500
+ for c in nominal_consts]
501
+
502
+
503
+ def _family_axiom_lines(usage: _FamilyUsage, systems: dict,
504
+ temporal_closure: bool) -> List[str]:
505
+ """``axiomatization`` lines for every family/system ``usage``/``systems`` need.
506
+
507
+ Order mirrors :func:`unicode_logic_kit.hol.isabelle_modal._collect_axioms`:
508
+ per-agent systems, then deontic seriality, then the temporal
509
+ closure/linking axioms -- so the two HOL routes read the same way.
510
+ """
511
+ lines: List[str] = []
512
+ for family in sorted(systems):
513
+ lines += _agent_system_axioms(family, systems[family])
514
+ if usage.deontic:
515
+ lines.append('axiomatization where Rd_serial: "{0}v. {1}u. Rd v u"'
516
+ .format(_ALL, _EX))
517
+ if usage.temporal and temporal_closure:
518
+ lines += [
519
+ f'axiomatization where Rt_refl: "{_ALL}w. Rt w w"',
520
+ f'axiomatization where Rt_trans: "{_ALL}w v u. Rt w v {_IMP} '
521
+ f'Rt v u {_IMP} Rt w u"',
522
+ ]
523
+ # tnext ⊆ t*: every one-step Rn-successor (what Next/Previous/Until/Since
524
+ # read) is henceforth-reachable (what Always/Eventually read) -- without
525
+ # this link Always(P) -> Next(P), valid for satisfies_modal (X and G/F
526
+ # both close over the SAME one-step "temporal" relation there), would be
527
+ # a non-theorem of the embedding (Rt and Rn otherwise unconstrained
528
+ # relative to each other). Mirrors isabelle_modal.py's n_in_t /
529
+ # thf_modal.py's tnext_in_t exactly.
530
+ if usage.temporal and usage.needs_next_rel:
531
+ lines.append(f'axiomatization where Rn_in_Rt: "{_ALL}w v. Rn w v '
532
+ f'{_IMP} Rt w v"')
533
+ return lines
534
+
535
+
536
+ # Frame condition -> its Isabelle axiom over R, universally quantified so the
537
+ # axiom is a schema about R and cannot be narrowed by a constant that happens to
538
+ # share a variable's name (see hol/isabelle_modal.py's note on that trap).
539
+ #
540
+ # "loeb"/"mckinsey"/"grz" are schemas over PROPOSITIONS, not conditions on R
541
+ # alone (there is no first-order frame correspondence for any of the three),
542
+ # so each states the Löb / McKinsey / Grzegorczyk schema directly, exactly as
543
+ # hol.isabelle_modal's r_loeb/r_mckinsey/r_grz do at first order -- with ONE
544
+ # difference in how the schema predicate P is written: it is bound EXPLICITLY
545
+ # here (`\<forall>P::sigma.`) rather than left a free/schematic variable the
546
+ # way isabelle_modal's first-order port leaves it. That first-order module
547
+ # gets away with a free P only via a side convention (_safe_name lower-cases
548
+ # every user predicate's leading character, so a free schematic `P` can never
549
+ # collide with a user symbol); this module carries no such convention, and
550
+ # does not need one for a different reason: isabelle_ho_modal_theory always
551
+ # emits the frame axioms (this dict) BEFORE the signature's `consts`
552
+ # declarations (see its body), so at the point a free P in, say, R_loeb would
553
+ # be elaborated, no user-declared `consts P` exists yet for it to resolve to
554
+ # -- Isar turns a free identifier with no existing constant of that name into
555
+ # a schematic variable of the statement, which is exactly the generalisation
556
+ # a bound `\<forall>P` gives explicitly. (Reversing that emission order would
557
+ # reintroduce a real capture: verified live against local Isabelle by moving
558
+ # `consts P` before a free-variable R_loeb and watching a proof for a
559
+ # DIFFERENT proposition Q, which should need R_loeb's full generality, stall
560
+ # instead of discharging.) The explicit binder is kept anyway, as
561
+ # defense-in-depth against that emission order ever changing and because it
562
+ # makes the schema's universal scope visible in the source text rather than
563
+ # resting on an unstated fact about Isar's free-variable elaboration. The THF
564
+ # side keeps the same explicit binder for the same reason; there a user's own
565
+ # ``P`` is a constant, spelled as the lower word ``p`` (see
566
+ # _ho_common.ThfNames), so it could not meet the schema variable in any case.
567
+ _FRAME_AXIOMS = {
568
+ "refl": f'"{_ALL}x. R x x"',
569
+ "sym": f'"{_ALL}x y. R x y {_IMP} R y x"',
570
+ "trans": f'"{_ALL}x y z. R x y {_IMP} R y z {_IMP} R x z"',
571
+ "serial": f'"{_ALL}x. {_EX}y. R x y"',
572
+ "eucl": f'"{_ALL}x y z. R x y {_IMP} R x z {_IMP} R y z"',
573
+ "directed": f'"{_ALL}x y z. R x y {_IMP} R x z {_IMP} ({_EX}u. R y u {_AND} R z u)"',
574
+ "connected": f'"{_ALL}x y z. R x y {_IMP} R x z {_IMP} (R y z {_OR} R z y)"',
575
+ "functional": f'"{_ALL}x y z. R x y {_IMP} R x z {_IMP} y = z"',
576
+ "dense": f'"{_ALL}x y. R x y {_IMP} ({_EX}z. R x z {_AND} R z y)"',
577
+ "shift_refl": f'"{_ALL}x y. R x y {_IMP} R y y"',
578
+ "empty": f'"{_ALL}x y. {_NOT} R x y"',
579
+ # Löb: box(box P -> P) -> box P, i.e. (∀y. Rxy -> ((∀z. Ryz -> Pz) -> Py))
580
+ # -> (∀y. Rxy -> Py), for every world x and every proposition P.
581
+ "loeb": (f'"{_ALL}P::sigma. {_ALL}x. ({_ALL}y. R x y {_IMP} '
582
+ f'(({_ALL}z. R y z {_IMP} P z) {_IMP} P y)) {_IMP} '
583
+ f'({_ALL}y. R x y {_IMP} P y)"'),
584
+ # McKinsey: box(diamond P) -> diamond(box P).
585
+ "mckinsey": (f'"{_ALL}P::sigma. {_ALL}x. ({_ALL}y. R x y {_IMP} '
586
+ f'({_EX}z. R y z {_AND} P z)) {_IMP} '
587
+ f'({_EX}y. R x y {_AND} ({_ALL}z. R y z {_IMP} P z))"'),
588
+ # Grzegorczyk: box(box(P -> box P) -> P) -> P.
589
+ "grz": (f'"{_ALL}P::sigma. {_ALL}x. ({_ALL}y. R x y {_IMP} '
590
+ f'(({_ALL}z. R y z {_IMP} (P z {_IMP} ({_ALL}u. R z u {_IMP} P u))) '
591
+ f'{_IMP} P y)) {_IMP} P x"'),
592
+ }
593
+
594
+
595
+ def _frame_axiom_lines(frame: str) -> List[str]:
596
+ """Return the ``axiomatization`` lines constraining ``R`` for ``frame``.
597
+
598
+ Reads :data:`unicode_logic_kit.fol.frames.FRAMES` like every other modal route
599
+ in the kit, so a system means the same thing here as it does there. Every
600
+ condition the registry lists has an entry in ``_FRAME_AXIOMS`` — including
601
+ Löb, McKinsey and Grz, which have no first-order correspondence and are
602
+ instead stated as schemas over propositions, each with its schema variable
603
+ bound explicitly (see the comment above ``_FRAME_AXIOMS``). A name the
604
+ registry itself does not know is caught earlier, by ``resolve_frame``; this
605
+ loop's own raise is a defence against ``_FRAME_AXIOMS`` and the registry
606
+ drifting apart, not a live refusal path for any currently-registered
607
+ condition.
608
+ """
609
+ conditions = resolve_frame(frame)
610
+ lines = []
611
+ for condition in conditions:
612
+ if condition not in _FRAME_AXIOMS:
613
+ raise UnsupportedFrameCondition(
614
+ f"ho_modal: frame condition {condition!r} (system {frame!r}) has no "
615
+ f"axiom registered in this embedding's _FRAME_AXIOMS; "
616
+ f"fol.frames.resolve_frame accepted it but this module does not "
617
+ f"yet state it."
618
+ )
619
+ lines.append(f'axiomatization where R_{condition}: {_FRAME_AXIOMS[condition]}')
620
+ return lines
621
+
622
+
623
+ # --------------------------------------------------------------------------
624
+ # Rigid identity
625
+ # --------------------------------------------------------------------------
626
+ #
627
+ # Object identity is NOT a world-relativised predicate (fol.qml's "Equality is
628
+ # rigid"). It is HOL's own ``=`` over the individual type ``i``, lifted to the
629
+ # type of a proposition by a world binder it never uses, so it cannot vary with the
630
+ # world. Both exporters read an identity atom through the two helpers below and
631
+ # nowhere else, so a formula is lowered and checked ONCE, up front -- never lazily
632
+ # at the atom, where a route that short-circuits could skip it.
633
+
634
+ #: How the refusal of a PROPERTY-typed identity speaks (see :func:`_rigid_identity`).
635
+ _PROPERTY_IDENTITY_ROUTE = "the third-order modal embedding at a property type"
636
+ _PROPERTY_IDENTITY_ATOM_READING = (
637
+ "'=' is read between INDIVIDUALS only, as HOL's own identity over the type i; "
638
+ "a property is a world-indexed function i => ... => sigma, and whether two of "
639
+ "them are 'identical' (necessarily coextensive? coextensive at one world?) is a "
640
+ "choice this embedding does not make for you"
641
+ )
642
+ #: Not the propositional routes' failure. They key an atom by its rendered form
643
+ #: and so get 'a = a' FALSE — a wrong answer. Here there is no single right
644
+ #: answer to give: HOL's own ``=`` at a property type IS one reading (necessary
645
+ #: coextension), and picking it silently would answer a question the caller did
646
+ #: not ask rather than the one they did.
647
+ _PROPERTY_IDENTITY_CONSEQUENCE = (
648
+ "so reading it as HOL's own identity at that type would silently pick "
649
+ "necessary coextension as what 'the same property' means"
650
+ )
651
+
652
+ _PROPERTY_IDENTITY_INSTEAD = (
653
+ "State the relation you mean between the properties instead: necessary "
654
+ "coextension is ∀x □(P(x) ↔ Q(x)), coextension at the current world is "
655
+ "∀x (P(x) ↔ Q(x)). Identity of INDIVIDUALS (a = b) is supported, and rigid."
656
+ )
657
+
658
+
659
+ def _rigid_identity(formulas: Sequence[Node], caller: str) -> List[Node]:
660
+ """Read every identity atom of ``formulas`` as rigid identity, or refuse it by name.
661
+
662
+ Two steps, both BEFORE any signature analysis or rendering:
663
+
664
+ 1. An identity atom with a PROPERTY-typed term (a predicate name or a
665
+ λ-abstraction, i.e. identity at type ``i ⇒ … ⇒ σ`` rather than ``i``) is
666
+ refused by name through
667
+ :func:`~unicode_logic_kit.semantics._modal_reject.reject_equality`. This is not
668
+ rigid identity gone missing but a different question: the grammar cannot even
669
+ write it (``G = H`` does not parse), :func:`~unicode_logic_kit.fol.nodes.analyse_signatures`
670
+ types the two slots of ``=`` independently (so ``G = H`` with ``G`` unary and
671
+ ``H`` binary would be accepted and ill-typed in HOL), and HOL's ``=`` at a
672
+ function type is one particular answer -- necessary coextension -- to a question
673
+ the kit has no oracle for. Rather than guess, the case is refused. It is checked
674
+ BEFORE the lowering so the message names the atom as the caller wrote it
675
+ (``≠``, not the ``¬(=)`` it would have become).
676
+ 2. :func:`~unicode_logic_kit.hol.isabelle_modal._lower_identity` -- the very rule
677
+ :func:`unicode_logic_kit.fol.qml.qml_translate` applies (``t₁ ≠ t₂`` becomes
678
+ ``¬(t₁ = t₂)``) -- and a non-binary ``=`` / ``≠`` raises ``ValueError``, as in
679
+ ``qml``. A formula without an identity atom is returned unchanged.
680
+
681
+ The returned list is parallel to ``formulas``.
682
+ """
683
+ for formula in formulas:
684
+ for node in formula.walk():
685
+ # A non-binary atom is step 2's ValueError, whatever its terms are.
686
+ if is_equality_atom(node) and len(node.args) == 2 and any(
687
+ isinstance(arg, (PredicateTerm, Lambda)) for arg in node.args):
688
+ reject_equality(node, caller, _PROPERTY_IDENTITY_ROUTE,
689
+ atom_reading=_PROPERTY_IDENTITY_ATOM_READING,
690
+ consequence=_PROPERTY_IDENTITY_CONSEQUENCE,
691
+ instead=_PROPERTY_IDENTITY_INSTEAD)
692
+ return [_lower_identity(f, caller) for f in formulas]
693
+
694
+
695
+ def _identity_sides(node: Atom):
696
+ """The two individual terms of an identity atom that :func:`_rigid_identity` accepted.
697
+
698
+ The renderers call this on every ``=`` atom. An atom that is not a binary ``=``
699
+ has bypassed the lowering (``≠`` becomes ``¬(=)`` there, a non-binary atom is
700
+ refused there); rendering it anyway would route it through the generic predicate
701
+ path, where THF's name resolver turns ``≠`` into ``fneq`` -- an uninterpreted
702
+ reading of identity, which is exactly what this embedding refuses. So it raises.
703
+ """
704
+ if node.predicate != "=" or len(node.args) != 2:
705
+ raise UnsupportedHigherOrderNode(
706
+ f"ho_modal: the identity atom {node.to_unicode_str()!r} reached the renderer "
707
+ f"without being lowered (≠ becomes ¬(=), and a non-binary =/≠ is refused, "
708
+ f"before any rendering). Render through isabelle_ho_modal_theory / "
709
+ f"to_thf_ho_modal, which do that; identity is never rendered as a "
710
+ f"world-relative predicate."
711
+ )
712
+ return node.args
713
+
714
+
715
+ # --------------------------------------------------------------------------
716
+ # Isabelle rendering
717
+ # --------------------------------------------------------------------------
718
+
719
+
720
+ def _prop_type(arity: int) -> str:
721
+ """Return the Isabelle type of a property of ``arity`` arguments: ``i => … => sigma``."""
722
+ return "".join(f"i {_FUN} " for _ in range(arity)) + "sigma"
723
+
724
+
725
+ def _nominal_const(name: str) -> str:
726
+ """The Isabelle ``world`` constant naming a hybrid nominal (``i`` -> ``nom_i``).
727
+
728
+ A plain prefix, not a full de-collision registry: nominal names are
729
+ already legal (lowercase) identifiers, so two distinct source names stay
730
+ distinct after prefixing, and ``nom_<name>`` cannot coincide with any of
731
+ this module's own fixed identifiers (``R``, ``Rk``, ``mall``, a bound
732
+ ``w``/``v``/``u``/``x``, …) or with a free individual/predicate constant
733
+ (a DIFFERENT Isabelle type, ``world`` rather than ``i``/``sigma``, so even
734
+ a same-spelled clash would be a type error caught at load time, not a
735
+ silent misreading).
736
+ """
737
+ return f"nom_{name}"
738
+
739
+
740
+ def _domain_vocab_lines() -> List[str]:
741
+ """``existsAt`` + the existsAt-guarded ``mforall``/``mexists`` (actualist modes only).
742
+
743
+ Ported from hol.isabelle_modal.py's ``_quant_block`` (see the module
744
+ docstring's "Domains" section for the one judgment call this makes): the
745
+ guard is typed ``i => world => bool`` (an INDIVIDUAL exists at a WORLD),
746
+ matching ho_modal.py's own type names rather than isabelle_modal.py's
747
+ (there ``i``/``e`` mean world/individual, the reverse of here). Emitted
748
+ only when an actualist ``mode=`` is combined with an actual individual
749
+ ``Quantifier`` — never for the default constant/possibilist mode, which
750
+ keeps using the existing, unguarded ``mall``/``mex`` exactly as before.
751
+ """
752
+ return [
753
+ f'consts existsAt :: "i {_FUN} world {_FUN} bool"'
754
+ ' \\<comment> \\<open>individual x exists at world w\\<close>',
755
+ f'abbreviation mforall :: "(i {_FUN} sigma) {_FUN} sigma" where',
756
+ f' "mforall P {_EQUIV} {_LAM}v. {_ALL}x. existsAt x v {_IMP} P x v"',
757
+ f'abbreviation mexists :: "(i {_FUN} sigma) {_FUN} sigma" where',
758
+ f' "mexists P {_EQUIV} {_LAM}v. {_EX}x. existsAt x v {_AND} P x v"',
759
+ ]
760
+
761
+
762
+ def _domain_axiom_lines(mode: str) -> List[str]:
763
+ """``existsAt`` domain-regime axioms for an actualist ``mode`` — ported from
764
+ :func:`unicode_logic_kit.hol.isabelle_modal._domain_axioms`, over ``R`` (this
765
+ module's alethic relation) rather than ``r``. Never called for a constant
766
+ mode (see :func:`_domain_vocab_lines`): there, quantification stays plain
767
+ ``mall``/``mex`` and no ``existsAt`` axiom is needed at all.
768
+ """
769
+ out = [f'axiomatization where nonempty_dom: "{_EX}x. existsAt x w"']
770
+ if mode in ("increasing", "cumulative"):
771
+ out.append(f'axiomatization where cumul_dom: "existsAt x w {_IMP} '
772
+ f'R w v {_IMP} existsAt x v"')
773
+ elif mode == "decreasing":
774
+ out.append(f'axiomatization where decr_dom: "existsAt x v {_IMP} '
775
+ f'R w v {_IMP} existsAt x w"')
776
+ # "varying": nonempty_dom only, no monotonicity axiom.
777
+ return out
778
+
779
+
780
+ def _isa_arg(node: Node, bound_arity: Dict[str, int],
781
+ display: Dict[str, str], mode: str,
782
+ scope: Optional[BinderScope] = None,
783
+ symbols: Optional[IsabelleNames] = None) -> str:
784
+ """Render a node standing in ARGUMENT position — an individual or a property.
785
+
786
+ ``scope`` holds the binders that enclose the node and the names they are printed under: a
787
+ variable or a lambda variable bound by one of them is printed under its binder's name. A
788
+ free variable and a constant are not bound by any binder, whatever their spelling: each is
789
+ printed under the name ``symbols`` gives it, and the two have two names. Without a ``scope``
790
+ every binder is printed under its own name.
791
+ """
792
+ if scope is None:
793
+ scope = BinderScope({})
794
+ if symbols is None:
795
+ symbols = IsabelleNames(ISABELLE_BUILT_IN)
796
+ if isinstance(node, (Variable, LambdaVar)):
797
+ # a binder's token is never empty, so "" means: no binder of this name encloses the node
798
+ return scope.token(VARIABLE, node.name, default="") or symbols.symbol(FREE_VARIABLE, node.name)
799
+ if isinstance(node, Constant):
800
+ return symbols.symbol(CONSTANT, node.name)
801
+ if isinstance(node, PredicateTerm):
802
+ return display.get(node.name) or symbols.symbol("predicate", node.name)
803
+ if isinstance(node, Lambda):
804
+ names, body = peel_lambdas(node)
805
+ binders = []
806
+ for name in names:
807
+ token, scope = scope.enter(VARIABLE, name)
808
+ binders.append(f"{_LAM}{token}::i.")
809
+ return f"({' '.join(binders)} {_isa_sigma(body, bound_arity, display, mode, scope, symbols)})"
810
+ if isinstance(node, Function):
811
+ head = symbols.symbol("function", node.name)
812
+ args = " ".join(_isa_arg(a, bound_arity, display, mode, scope, symbols) for a in node.args)
813
+ return f"({head} {args})" if args else head
814
+ raise UnsupportedHigherOrderNode(
815
+ f"ho_modal: {type(node).__name__} cannot stand in argument position; an "
816
+ f"argument is an individual term, a predicate name, or a λ-abstraction."
817
+ )
818
+
819
+
820
+ _BINARY_ISA = {And: "mand", Or: "mor", Implies: "mimp", Iff: "miff", Xor: "mxor"}
821
+
822
+ #: Node types this embedding EXPLICITLY refuses by name rather than folding
823
+ #: into the generic "no reading for ..." fallback -- each needs its own
824
+ #: pointed message naming where the construct DOES belong.
825
+ _NO_COUNTERFACTUAL = (
826
+ "ho_modal: the counterfactuals □→/◇→ (Would/Might) read a similarity "
827
+ "ordering of worlds (Lewis spheres), not an accessibility relation, and "
828
+ "are not shallow-embeddable the way □/◇/K_a/... are. Use "
829
+ "hol.isabelle_conditional / isabelle_decide_counterfactual instead."
830
+ )
831
+
832
+ #: N1: the ↓ binder is not supported by this third-order HOL embedding —
833
+ #: H(@,↓) validity is undecidable, and this emitter's job (a theory/theorem
834
+ #: for a human or ATP proof search) assumes a goal shape that search can be
835
+ #: expected to close, not an open research question.
836
+ _NO_DOWN = (
837
+ "ho_modal: the ↓ binder is not supported by this third-order HOL "
838
+ "embedding — H(@,↓) validity is undecidable. Use "
839
+ "unicode_logic_kit.fol.modal_translation.down_is_valid (propositional "
840
+ "H(@,↓), Z3, PROVED-only) or unicode_logic_kit.atp.kripke_enum.KripkeEnumBackend "
841
+ "/ modal_enum_search (bounded search, REFUTED-only) instead."
842
+ )
843
+
844
+
845
+ def _isa_sigma(node: Node, bound_arity: Dict[str, int],
846
+ display: Dict[str, str], mode: str,
847
+ scope: Optional[BinderScope] = None,
848
+ symbols: Optional[IsabelleNames] = None) -> str:
849
+ """Render ``node`` as an Isabelle term of type ``sigma`` (a world-indexed proposition).
850
+
851
+ ``display`` gives each bound predicate variable the name it is printed under, ``scope`` each
852
+ enclosing object binder (see :mod:`unicode_logic_kit.hol._isabelle_binders`); without a
853
+ ``scope`` every binder is printed under its own name. ``symbols`` names the free symbols
854
+ (predicates, functions, free variables, constants) and is the one the declarations were
855
+ written under."""
856
+ if scope is None:
857
+ scope = BinderScope({})
858
+ if symbols is None:
859
+ symbols = IsabelleNames(ISABELLE_BUILT_IN)
860
+ if isinstance(node, Not):
861
+ return f"(mnot {_isa_sigma(node.formula, bound_arity, display, mode, scope, symbols)})"
862
+ op = _BINARY_ISA.get(type(node))
863
+ if op is not None:
864
+ left = _isa_sigma(node.left, bound_arity, display, mode, scope, symbols)
865
+ right = _isa_sigma(node.right, bound_arity, display, mode, scope, symbols)
866
+ return f"({op} {left} {right})"
867
+ if isinstance(node, Box):
868
+ return f"(mbox {_isa_sigma(node.formula, bound_arity, display, mode, scope, symbols)})"
869
+ if isinstance(node, Diamond):
870
+ return f"(mdia {_isa_sigma(node.formula, bound_arity, display, mode, scope, symbols)})"
871
+ if isinstance(node, Quantifier):
872
+ # The one judgment call this port makes (see the module docstring):
873
+ # ONLY the individual-typed binder is existsAt-guarded under an
874
+ # actualist mode; a property binder (SecondOrderQuantifier, below)
875
+ # never is.
876
+ if mode in _ACTUALIST_MODES:
877
+ binder = "mforall" if node.type == "∀" else "mexists"
878
+ else:
879
+ binder = "mall" if node.type == "∀" else "mex"
880
+ token, inner = scope.enter(VARIABLE, node.variable.name)
881
+ body = _isa_sigma(node.formula, bound_arity, display, mode, inner, symbols)
882
+ return f"({binder} ({_LAM}{token}::i. {body}))"
883
+ if isinstance(node, SecondOrderQuantifier):
884
+ binder = "mall" if node.type == "∀" else "mex"
885
+ arity = bound_arity.get(node.predicate, node.arity)
886
+ name = display.get(node.predicate, node.predicate)
887
+ body = _isa_sigma(node.formula, bound_arity, display, mode, scope, symbols)
888
+ return (f"({binder} ({_LAM}{name}::{_prop_type(arity)}. {body}))")
889
+ if isinstance(node, (Knows, Believes, Says, Wants)):
890
+ macro = {Knows: "mknows", Believes: "mbelieves",
891
+ Says: "msays", Wants: "mwants"}[type(node)]
892
+ agent = _isa_arg(node.agent, bound_arity, display, mode, scope, symbols)
893
+ body = _isa_sigma(node.formula, bound_arity, display, mode, scope, symbols)
894
+ return f"({macro} {agent} {body})"
895
+ if isinstance(node, Obligatory):
896
+ return f"(mobl {_isa_sigma(node.formula, bound_arity, display, mode, scope, symbols)})"
897
+ if isinstance(node, Permitted):
898
+ return f"(mperm {_isa_sigma(node.formula, bound_arity, display, mode, scope, symbols)})"
899
+ if isinstance(node, Always):
900
+ return f"(malways {_isa_sigma(node.formula, bound_arity, display, mode, scope, symbols)})"
901
+ if isinstance(node, Eventually):
902
+ return f"(meventually {_isa_sigma(node.formula, bound_arity, display, mode, scope, symbols)})"
903
+ if isinstance(node, Next):
904
+ return f"(mnext {_isa_sigma(node.formula, bound_arity, display, mode, scope, symbols)})"
905
+ if isinstance(node, Historically):
906
+ return f"(mhistorically {_isa_sigma(node.formula, bound_arity, display, mode, scope, symbols)})"
907
+ if isinstance(node, Once):
908
+ return f"(monce {_isa_sigma(node.formula, bound_arity, display, mode, scope, symbols)})"
909
+ if isinstance(node, Previous):
910
+ return f"(mprevious {_isa_sigma(node.formula, bound_arity, display, mode, scope, symbols)})"
911
+ if isinstance(node, Until):
912
+ left = _isa_sigma(node.left, bound_arity, display, mode, scope, symbols)
913
+ right = _isa_sigma(node.right, bound_arity, display, mode, scope, symbols)
914
+ return f"(muntil {left} {right})"
915
+ if isinstance(node, Since):
916
+ left = _isa_sigma(node.left, bound_arity, display, mode, scope, symbols)
917
+ right = _isa_sigma(node.right, bound_arity, display, mode, scope, symbols)
918
+ return f"(msince {left} {right})"
919
+ if isinstance(node, Nominal):
920
+ return f"({_LAM}v::world. v = {_nominal_const(node.name)})"
921
+ if isinstance(node, At):
922
+ # The world binder is anonymous: ``body`` holds the caller's own symbols, and a binder
923
+ # named ``v`` would capture a constant, a function or a predicate called ``v`` there.
924
+ body = _isa_sigma(node.formula, bound_arity, display, mode, scope, symbols)
925
+ return f"({_LAM}_. {body} {_nominal_const(node.nominal.name)})"
926
+ if isinstance(node, Down):
927
+ raise UnsupportedHigherOrderNode(_NO_DOWN)
928
+ if isinstance(node, Atom):
929
+ constant = truth_value(node)
930
+ if constant is not None:
931
+ # `$true` / `$false`: HOL's True / False under the anonymous world binder.
932
+ return f"({_LAM}_. True)" if constant else f"({_LAM}_. False)"
933
+ if is_equality_atom(node):
934
+ # Rigid identity: HOL's own ``=`` under a world binder it never uses, so
935
+ # it cannot vary by world. The binder is the anonymous ``_`` rather than a
936
+ # name so a user variable called ``w`` can never be captured by it. The
937
+ # same text isabelle_modal emits for the same atom.
938
+ left, right = _identity_sides(node)
939
+ return (f"({_LAM}_. {_isa_arg(left, bound_arity, display, mode, scope, symbols)} = "
940
+ f"{_isa_arg(right, bound_arity, display, mode, scope, symbols)})")
941
+ if node.predicate in ORDERING:
942
+ name = ORDERING[node.predicate]
943
+ else:
944
+ name = display.get(node.predicate) or symbols.symbol("predicate", node.predicate)
945
+ if not node.args:
946
+ return name
947
+ args = " ".join(_isa_arg(a, bound_arity, display, mode, scope, symbols) for a in node.args)
948
+ return f"({name} {args})"
949
+ if type(node).__name__ in ("Would", "Might"):
950
+ raise UnsupportedHigherOrderNode(_NO_COUNTERFACTUAL)
951
+ raise UnsupportedHigherOrderNode(
952
+ f"ho_modal: no reading for {type(node).__name__}. This embedding covers "
953
+ f"alethic □/◇, agent-indexed epistemic/doxastic/assertive/bouletic "
954
+ f"(K_a/B_a/Say_a/Want_a), deontic O/P, temporal (Always/Eventually/Next/"
955
+ f"Until/Since and their past mirrors) and hybrid (nominals/@) over "
956
+ f"third-order syntax. The Lewis counterfactuals (Would/Might) and the "
957
+ f"GROUP-epistemic operators (EverybodyKnows/DistributedKnowledge/"
958
+ f"CommonKnowledge) are not carried by any HOL route in this kit yet."
959
+ )
960
+
961
+
962
+ def _used_ordering(apart: Sequence[Node]) -> List[str]:
963
+ """The names of the ordering relations (``flt``, ...) that the formulas use, sorted."""
964
+ return sorted({ORDERING[p] for f in apart for p in atom_predicates(f) if p in ORDERING})
965
+
966
+
967
+ def _signature_lines(apart: Sequence[Node], signatures, symbols: IsabelleNames) -> List[str]:
968
+ """Return the ``consts`` declarations for every free symbol of ``apart``.
969
+
970
+ ``apart`` are the formulas with their bound predicate variables renamed apart and
971
+ ``signatures`` is the analysis over them TOGETHER, because that is the scope on which a
972
+ free predicate's argument types are determined: ``Positive(G)`` in one axiom and ``G(x)``
973
+ in another jointly say that ``Positive`` takes a property of arity 1.
974
+
975
+ Every symbol is declared under the name ``symbols`` gives it, and ``symbols`` is the one
976
+ the formulas are rendered with. A theory has one namespace for its constants, so a
977
+ predicate, a function, a free variable and a constant that are spelled alike are four
978
+ constants with four names, and none takes a name of the embedding's own vocabulary.
979
+ """
980
+ bound = set()
981
+ for formula in apart:
982
+ bound |= bound_pred_names(formula)
983
+ predicates = [pred for pred in sorted(signatures.slots)
984
+ if not (pred in bound or pred in EQUALITY_PREDICATES or pred in ORDERING
985
+ or pred in ORDERING.values())]
986
+ # Free individual symbols: a bare NAME parses to a Constant, and a variable left unbound
987
+ # by any quantifier denotes a particular individual too. Both are individuals of type i,
988
+ # but a free variable is a parameter and a constant a particular element: two symbols.
989
+ individuals = individual_symbols(apart)
990
+ functions = sorted(function_symbols(apart).items())
991
+ symbols.claim([("predicate", pred) for pred in predicates]
992
+ + [("function", name) for name, _ in functions] + individuals)
993
+
994
+ lines: List[str] = []
995
+ for pred in predicates:
996
+ parts = []
997
+ for kind in signatures.slots[pred]:
998
+ parts.append("i" if kind == INDIVIDUAL else f"({_prop_type(kind[1])})")
999
+ arrow = "".join(f"{p} {_FUN} " for p in parts)
1000
+ lines.append(f'consts {symbols.symbol("predicate", pred)} :: "{arrow}sigma"')
1001
+
1002
+ for kind, name in individuals:
1003
+ lines.append(f'consts {symbols.symbol(kind, name)} :: "i"')
1004
+
1005
+ # Ordering predicates actually used, as world-relativised relations. Identity is
1006
+ # NOT among them: it is HOL's own ``=`` (see _rigid_identity) and is declared
1007
+ # nowhere.
1008
+ for name in _used_ordering(apart):
1009
+ lines.append(f'consts {name} :: "i {_FUN} i {_FUN} sigma"')
1010
+
1011
+ # Function symbols in term position.
1012
+ for name, arity in functions:
1013
+ arrow = "".join(f"i {_FUN} " for _ in range(arity))
1014
+ lines.append(f'consts {symbols.symbol("function", name)} :: "{arrow}i"')
1015
+ return lines
1016
+
1017
+
1018
+ # --------------------------------------------------------------------------
1019
+ # Theory assembly
1020
+ # --------------------------------------------------------------------------
1021
+
1022
+
1023
+ @dataclass(frozen=True)
1024
+ class HoAxiom:
1025
+ """One named axiom of an emitted theory.
1026
+
1027
+ ``comment`` is printed above it, so a theory can carry the argument's own
1028
+ numbering (``A1``, ``D2``, …) and say what each line is meant to assert.
1029
+ """
1030
+
1031
+ name: str
1032
+ formula: Node
1033
+ comment: str = ""
1034
+
1035
+
1036
+ @dataclass(frozen=True)
1037
+ class HoGoal:
1038
+ """One named goal, with the proof text to attempt it with.
1039
+
1040
+ ``proof`` is Isabelle proof text (``by blast``, ``using A1 A2 by metis``,
1041
+ ``nitpick [expect = genuine]``, …). The kit does not invent proofs: what is
1042
+ written here is what the theory will try. ``oops`` leaves the goal
1043
+ explicitly unproved, which is what :func:`to_isabelle_ho_modal` emits when
1044
+ no proof is supplied.
1045
+
1046
+ A goal is stated EITHER as a ``formula`` of the object logic — asserted
1047
+ valid, i.e. ``mvalid φ`` — OR, for the one thing the object logic cannot
1048
+ say about itself, as a raw Isabelle ``statement`` such as ``"False"``. That
1049
+ second form is what lets a theory record that an axiom set is INCONSISTENT:
1050
+ "these axioms prove falsity" is a claim about the theory, not a formula in
1051
+ it. Exactly one of the two must be given.
1052
+ """
1053
+
1054
+ name: str
1055
+ formula: Optional[Node] = None
1056
+ proof: str = "oops"
1057
+ comment: str = ""
1058
+ kind: str = "theorem"
1059
+ statement: Optional[str] = None
1060
+
1061
+ def __post_init__(self):
1062
+ """Reject a goal that states nothing, or states itself twice over."""
1063
+ if (self.formula is None) == (self.statement is None):
1064
+ raise ValueError(
1065
+ f"HoGoal({self.name!r}): give exactly one of formula= (an object-logic "
1066
+ f"formula, asserted valid) or statement= (a raw Isabelle proposition)."
1067
+ )
1068
+
1069
+
1070
+ def isabelle_ho_modal_theory(name: str,
1071
+ axioms: Sequence[HoAxiom] = (),
1072
+ goals: Sequence[HoGoal] = (),
1073
+ frame: str = "K",
1074
+ preamble: Sequence[str] = (),
1075
+ mode: str = "constant",
1076
+ systems: Optional[dict] = None,
1077
+ temporal_closure: bool = True) -> str:
1078
+ """Emit a complete Isabelle theory for a third-order modal axiom set and its goals.
1079
+
1080
+ ``axioms`` are asserted as ``axiomatization where <name>: "mvalid φ"`` — valid at
1081
+ every world, which is what an axiom of the object logic means. ``goals`` are
1082
+ stated the same way and followed by their own proof text.
1083
+
1084
+ ``frame`` names a system from :data:`unicode_logic_kit.fol.frames.FRAMES` and
1085
+ constrains ``R`` accordingly; ``"K"`` leaves ``R`` arbitrary. ``preamble``
1086
+ lines are inserted after the vocabulary and before the axioms, for anything
1087
+ the caller wants to add in Isabelle's own syntax.
1088
+
1089
+ ``mode`` selects the domain regime for INDIVIDUAL quantification (see the
1090
+ module docstring): the default ``"constant"``/``"possibilist"`` behaves
1091
+ exactly as before this parameter existed; ``"varying"``/``"increasing"``/
1092
+ ``"cumulative"``/``"decreasing"`` add the ``existsAt``-guarded reading.
1093
+ ``systems`` optionally constrains one or more of the agent-indexed
1094
+ families -- ``{"epistemic": "S5", "doxastic": "KD45", ...}`` -- a family
1095
+ named that the formula set never uses is refused, and an unnamed one
1096
+ stays unconstrained (plain K). ``temporal_closure`` (default ``True``)
1097
+ adds the reflexive+transitive axioms that make ``Rt`` the henceforth
1098
+ relation ``Always``/``Eventually`` need, exactly mirroring
1099
+ :func:`unicode_logic_kit.hol.isabelle_modal.isabelle_modal_theory`'s own
1100
+ parameter of the same name.
1101
+
1102
+ The signature is read off the axioms and goals together, so a symbol used in
1103
+ a goal but not in any axiom is still declared. Property-argument slots whose
1104
+ arity nothing in the theory determines are reported as a comment rather than
1105
+ silently defaulted — see :mod:`unicode_logic_kit.fol._ho_nodes`.
1106
+
1107
+ Identity ``=`` / ``≠`` between individuals is RIGID HOL equality (``≠`` is ``¬(=)``),
1108
+ declared nowhere; a ``=`` / ``≠`` atom without exactly two terms raises
1109
+ ``ValueError`` and identity between PROPERTIES raises ``NotImplementedError`` — the
1110
+ module docstring's "Equality is rigid, or refused" has the reasons.
1111
+ """
1112
+ if frame not in FRAMES:
1113
+ raise ValueError(
1114
+ f"isabelle_ho_modal_theory: unknown frame {frame!r}; "
1115
+ f"known systems are {sorted(FRAMES)}."
1116
+ )
1117
+ if mode not in _ACTUALIST_MODES and mode not in _CONSTANT_MODES:
1118
+ raise ValueError(
1119
+ f"isabelle_ho_modal_theory: unknown mode {mode!r} "
1120
+ f"(use one of {sorted(_ACTUALIST_MODES | _CONSTANT_MODES)})."
1121
+ )
1122
+ # A goal stated raw (statement=) contributes no formula to type-check.
1123
+ typed_goals = [g for g in goals if g.formula is not None]
1124
+ # Identity is read ONCE, up front, as rigid HOL ``=`` (``≠`` -> ``¬(=)``, a
1125
+ # non-binary atom or a property-typed one refused by name) -- before the signature
1126
+ # analysis, so nothing downstream ever sees a ``≠``.
1127
+ formulas = _rigid_identity(
1128
+ [a.formula for a in axioms] + [g.formula for g in typed_goals],
1129
+ "isabelle_ho_modal_theory")
1130
+ apart, display = rename_apart(formulas)
1131
+ signatures = analyse_signatures(apart)
1132
+ # Arities come from the theory-wide analysis, not from each node's own
1133
+ # parse-time field: a binder whose arity only the OTHER axioms determine is
1134
+ # exactly the case a per-formula answer gets wrong.
1135
+ bound_arity = signatures.arity
1136
+ axiom_bodies = apart[:len(axioms)]
1137
+ typed_bodies = dict(zip((g.name for g in typed_goals), apart[len(axioms):]))
1138
+ usage = _scan_family_usage(apart)
1139
+ resolved_systems = _validate_systems(usage, systems)
1140
+ nominal_consts = sorted({_nominal_const(n.name) for f in apart for n in f.walk()
1141
+ if isinstance(n, Nominal)})
1142
+
1143
+ lines = [
1144
+ "(* Third-order modal logic in HOL: shallow (Benzmuller-style) embedding. *)",
1145
+ f"(* Frame: {frame}. Domain mode: {mode}. *)",
1146
+ f"theory {name}",
1147
+ " imports Main",
1148
+ "begin",
1149
+ "",
1150
+ ho_modal_definitions(),
1151
+ ]
1152
+ extra_vocab = (_family_vocab_lines(usage) + _hybrid_vocab_lines(nominal_consts))
1153
+ if mode in _ACTUALIST_MODES and usage.has_quant:
1154
+ extra_vocab = _domain_vocab_lines() + extra_vocab
1155
+ if extra_vocab:
1156
+ lines.append("")
1157
+ lines += extra_vocab
1158
+ lines.append("")
1159
+ # The symbols of the formulas share one namespace with the vocabulary above (R, mall,
1160
+ # mnot, Rk, existsAt, nom_i, ...): none of them may take one of those names, and none
1161
+ # takes the name of another symbol, whatever its kind.
1162
+ fixed = declared_names([ho_modal_definitions()] + extra_vocab)
1163
+ symbols = IsabelleNames((fixed - declared_names(())) | ISABELLE_BUILT_IN
1164
+ | set(_used_ordering(apart)))
1165
+ signature = _signature_lines(apart, signatures, symbols)
1166
+ frame_lines = _frame_axiom_lines(frame)
1167
+ frame_lines += _family_axiom_lines(usage, resolved_systems, temporal_closure)
1168
+ if mode in _ACTUALIST_MODES and usage.has_quant:
1169
+ frame_lines += _domain_axiom_lines(mode)
1170
+ if frame_lines:
1171
+ lines.append(f"\\<comment> \\<open>frame/domain conditions for {frame}/{mode}\\<close>")
1172
+ lines += frame_lines
1173
+ lines.append("")
1174
+ if signature:
1175
+ lines += signature
1176
+ lines.append("")
1177
+ if signatures.defaulted:
1178
+ pairs = ", ".join(f"{p}[{i}]" for p, i in sorted(signatures.defaulted))
1179
+ lines.append(
1180
+ f"\\<comment> \\<open>arity defaulted to 1 (nothing in the theory "
1181
+ f"fixes it): {pairs}\\<close>")
1182
+ lines.append("")
1183
+ lines += list(preamble)
1184
+ if preamble:
1185
+ lines.append("")
1186
+
1187
+ # A binder shadows a constant of its own spelling inside its scope, and the lifted
1188
+ # operators (mand, mall, ...) are constants too: every binder (object quantifier, lambda
1189
+ # parameter, bound predicate variable) is printed under a name that nothing declared above
1190
+ # has. ``display`` gives each renamed-apart predicate variable that name.
1191
+ tokens = binder_tokens(collect_binders(formulas), declared_names(lines))
1192
+ display = {fresh: tokens[(PREDICATE, original)] for fresh, original in display.items()}
1193
+ scope = BinderScope(tokens)
1194
+
1195
+ for axiom, renamed in zip(axioms, axiom_bodies):
1196
+ if axiom.comment:
1197
+ lines.append(f"\\<comment> \\<open>{axiom.comment}\\<close>")
1198
+ body = _isa_sigma(renamed, bound_arity, display, mode, scope, symbols)
1199
+ lines.append(f'axiomatization where {axiom.name}: "mvalid {body}"')
1200
+ if axioms:
1201
+ lines.append("")
1202
+
1203
+ for goal in goals:
1204
+ if goal.comment:
1205
+ lines.append(f"\\<comment> \\<open>{goal.comment}\\<close>")
1206
+ if goal.statement is not None:
1207
+ proposition = goal.statement
1208
+ else:
1209
+ body = _isa_sigma(typed_bodies[goal.name], bound_arity, display, mode, scope, symbols)
1210
+ proposition = f"mvalid {body}"
1211
+ lines.append(f'{goal.kind} {goal.name}: "{proposition}"')
1212
+ lines.append(f" {goal.proof}")
1213
+ lines.append("")
1214
+
1215
+ lines.append("end")
1216
+ return "\n".join(lines) + "\n"
1217
+
1218
+
1219
+ def to_isabelle_ho_modal(formula: Node, name: str = "HO_Modal_Goal",
1220
+ frame: str = "K", proof: Optional[str] = None,
1221
+ mode: str = "constant",
1222
+ systems: Optional[dict] = None,
1223
+ temporal_closure: bool = True) -> str:
1224
+ """Emit a theory whose single goal is ``formula``, valid on every world.
1225
+
1226
+ The one-formula convenience over :func:`isabelle_ho_modal_theory`. Without a
1227
+ ``proof`` the goal is left ``oops`` — the kit states the problem, it does not
1228
+ invent a proof for it. ``mode``/``systems``/``temporal_closure`` pass
1229
+ through unchanged; see :func:`isabelle_ho_modal_theory`.
1230
+ """
1231
+ goal = HoGoal("goal", formula, proof or "oops")
1232
+ return isabelle_ho_modal_theory(name, (), (goal,), frame=frame, mode=mode,
1233
+ systems=systems,
1234
+ temporal_closure=temporal_closure)
1235
+
1236
+
1237
+ # --------------------------------------------------------------------------
1238
+ # THF
1239
+ # --------------------------------------------------------------------------
1240
+
1241
+ _THF_PRELUDE = [
1242
+ "% Third-order modal logic in THF: shallow (Benzmuller-style) embedding.",
1243
+ "% mu = worlds; $i = individuals; (mu > $o) = propositions;",
1244
+ "% ($i > mu > $o) = properties; (($i > mu > $o) > mu > $o) = predicates of properties.",
1245
+ "thf(mu_type, type, ( mu : $tType )).",
1246
+ "thf(r_type, type, ( r : mu > mu > $o )).",
1247
+ "thf(mnot_type, type, ( mnot : ( mu > $o ) > mu > $o )).",
1248
+ "thf(mnot_def, definition, ( mnot = ( ^ [P: mu > $o, W: mu] : ( ~ ( P @ W ) ) ) )).",
1249
+ "thf(mand_type, type, ( mand : ( mu > $o ) > ( mu > $o ) > mu > $o )).",
1250
+ "thf(mand_def, definition, ( mand = ( ^ [P: mu > $o, Q: mu > $o, W: mu] : "
1251
+ "( ( P @ W ) & ( Q @ W ) ) ) )).",
1252
+ "thf(mor_type, type, ( mor : ( mu > $o ) > ( mu > $o ) > mu > $o )).",
1253
+ "thf(mor_def, definition, ( mor = ( ^ [P: mu > $o, Q: mu > $o, W: mu] : "
1254
+ "( ( P @ W ) | ( Q @ W ) ) ) )).",
1255
+ "thf(mimp_type, type, ( mimp : ( mu > $o ) > ( mu > $o ) > mu > $o )).",
1256
+ "thf(mimp_def, definition, ( mimp = ( ^ [P: mu > $o, Q: mu > $o, W: mu] : "
1257
+ "( ( P @ W ) => ( Q @ W ) ) ) )).",
1258
+ "thf(miff_type, type, ( miff : ( mu > $o ) > ( mu > $o ) > mu > $o )).",
1259
+ "thf(miff_def, definition, ( miff = ( ^ [P: mu > $o, Q: mu > $o, W: mu] : "
1260
+ "( ( P @ W ) <=> ( Q @ W ) ) ) )).",
1261
+ "thf(mbox_type, type, ( mbox : ( mu > $o ) > mu > $o )).",
1262
+ "thf(mbox_def, definition, ( mbox = ( ^ [P: mu > $o, W: mu] : "
1263
+ "( ! [V: mu] : ( ( r @ W @ V ) => ( P @ V ) ) ) ) )).",
1264
+ "thf(mdia_type, type, ( mdia : ( mu > $o ) > mu > $o )).",
1265
+ "thf(mdia_def, definition, ( mdia = ( ^ [P: mu > $o, W: mu] : "
1266
+ "( ? [V: mu] : ( ( r @ W @ V ) & ( P @ V ) ) ) ) )).",
1267
+ "thf(mvalid_type, type, ( mvalid : ( mu > $o ) > $o )).",
1268
+ "thf(mvalid_def, definition, ( mvalid = "
1269
+ "( ^ [P: mu > $o] : ( ! [W: mu] : ( P @ W ) ) ) )).",
1270
+ ]
1271
+
1272
+ _THF_FRAME = {
1273
+ "refl": "( ! [W: mu] : ( r @ W @ W ) )",
1274
+ "sym": "( ! [W: mu, V: mu] : ( ( r @ W @ V ) => ( r @ V @ W ) ) )",
1275
+ "trans": "( ! [W: mu, V: mu, U: mu] : ( ( ( r @ W @ V ) & ( r @ V @ U ) ) "
1276
+ "=> ( r @ W @ U ) ) )",
1277
+ "serial": "( ! [W: mu] : ( ? [V: mu] : ( r @ W @ V ) ) )",
1278
+ "eucl": "( ! [W: mu, V: mu, U: mu] : ( ( ( r @ W @ V ) & ( r @ W @ U ) ) "
1279
+ "=> ( r @ V @ U ) ) )",
1280
+ "directed": "( ! [W: mu, V: mu, U: mu] : ( ( ( r @ W @ V ) & ( r @ W @ U ) ) "
1281
+ "=> ( ? [Z: mu] : ( ( r @ V @ Z ) & ( r @ U @ Z ) ) ) ) )",
1282
+ "connected": "( ! [W: mu, V: mu, U: mu] : ( ( ( r @ W @ V ) & ( r @ W @ U ) ) "
1283
+ "=> ( ( r @ V @ U ) | ( r @ U @ V ) ) ) )",
1284
+ "functional": "( ! [W: mu, V: mu, U: mu] : ( ( ( r @ W @ V ) & ( r @ W @ U ) ) "
1285
+ "=> ( V = U ) ) )",
1286
+ "dense": "( ! [W: mu, V: mu] : ( ( r @ W @ V ) "
1287
+ "=> ( ? [U: mu] : ( ( r @ W @ U ) & ( r @ U @ V ) ) ) ) )",
1288
+ "shift_refl": "( ! [W: mu, V: mu] : ( ( r @ W @ V ) => ( r @ V @ V ) ) )",
1289
+ "empty": "( ! [W: mu, V: mu] : ( ~ ( r @ W @ V ) ) )",
1290
+ # Löb / McKinsey / Grz: schemas over propositions, with the schema
1291
+ # variable P bound explicitly by a THF `! [P: mu > $o]`, matching the
1292
+ # explicit `\<forall>P::sigma.` on the Isabelle side (see the comment above
1293
+ # _FRAME_AXIOMS). A user symbol named P is a constant spelled `p`, so it
1294
+ # cannot capture the binder.
1295
+ "loeb": "( ! [P: mu > $o] : ( ! [W: mu] : ( ( ! [V: mu] : ( ( r @ W @ V ) => "
1296
+ "( ( ! [U: mu] : ( ( r @ V @ U ) => ( P @ U ) ) ) => ( P @ V ) ) ) ) "
1297
+ "=> ( ! [V: mu] : ( ( r @ W @ V ) => ( P @ V ) ) ) ) ) )",
1298
+ "mckinsey": "( ! [P: mu > $o] : ( ! [W: mu] : ( ( ! [V: mu] : ( ( r @ W @ V ) => "
1299
+ "( ? [U: mu] : ( ( r @ V @ U ) & ( P @ U ) ) ) ) ) "
1300
+ "=> ( ? [V: mu] : ( ( r @ W @ V ) & ( ! [U: mu] : ( ( r @ V @ U ) "
1301
+ "=> ( P @ U ) ) ) ) ) ) ) )",
1302
+ "grz": "( ! [P: mu > $o] : ( ! [W: mu] : ( ( ! [V: mu] : ( ( r @ W @ V ) => "
1303
+ "( ( ! [U: mu] : ( ( r @ V @ U ) => ( ( P @ U ) => "
1304
+ "( ! [Z: mu] : ( ( r @ U @ Z ) => ( P @ Z ) ) ) ) ) ) => ( P @ V ) ) ) ) "
1305
+ "=> ( P @ W ) ) ) )",
1306
+ }
1307
+
1308
+ # The extended vocabulary (epistemic/doxastic/assertive/bouletic, deontic,
1309
+ # temporal, hybrid) -- unconditionally declared, matching thf_modal.py's own
1310
+ # ``_THF_DEFS_FULL`` convention (a bridge/family whose relation is never used
1311
+ # stays merely unconstrained, not absent; see that module's own comment on
1312
+ # why "declared unconditionally" is the honest, THF-idiomatic choice) rather
1313
+ # than this module's own conditional Isabelle side. ``existsAt``/``mforall``/
1314
+ # ``mexists`` are the ONE exception (see _thf_domain_lines below): they stay
1315
+ # conditional, so the DEFAULT constant-mode THF output is unchanged.
1316
+ _THF_FAMILY_PRELUDE = [
1317
+ "thf(rk_type, type, ( rk : $i > mu > mu > $o )).",
1318
+ "thf(mknows_type, type, ( mknows : $i > ( mu > $o ) > mu > $o )).",
1319
+ "thf(mknows_def, definition, ( mknows = ( ^ [A: $i, P: mu > $o, W: mu] : "
1320
+ "( ! [V: mu] : ( ( rk @ A @ W @ V ) => ( P @ V ) ) ) ) )).",
1321
+ "thf(rb_type, type, ( rb : $i > mu > mu > $o )).",
1322
+ "thf(mbelieves_type, type, ( mbelieves : $i > ( mu > $o ) > mu > $o )).",
1323
+ "thf(mbelieves_def, definition, ( mbelieves = ( ^ [A: $i, P: mu > $o, W: mu] : "
1324
+ "( ! [V: mu] : ( ( rb @ A @ W @ V ) => ( P @ V ) ) ) ) )).",
1325
+ "thf(rs_type, type, ( rs : $i > mu > mu > $o )).",
1326
+ "thf(msays_type, type, ( msays : $i > ( mu > $o ) > mu > $o )).",
1327
+ "thf(msays_def, definition, ( msays = ( ^ [A: $i, P: mu > $o, W: mu] : "
1328
+ "( ! [V: mu] : ( ( rs @ A @ W @ V ) => ( P @ V ) ) ) ) )).",
1329
+ "thf(rw_type, type, ( rw : $i > mu > mu > $o )).",
1330
+ "thf(mwants_type, type, ( mwants : $i > ( mu > $o ) > mu > $o )).",
1331
+ "thf(mwants_def, definition, ( mwants = ( ^ [A: $i, P: mu > $o, W: mu] : "
1332
+ "( ! [V: mu] : ( ( rw @ A @ W @ V ) => ( P @ V ) ) ) ) )).",
1333
+ "thf(d_type, type, ( d : mu > mu > $o )).",
1334
+ "thf(mobl_type, type, ( mobl : ( mu > $o ) > mu > $o )).",
1335
+ "thf(mobl_def, definition, ( mobl = ( ^ [P: mu > $o, W: mu] : "
1336
+ "( ! [V: mu] : ( ( d @ W @ V ) => ( P @ V ) ) ) ) )).",
1337
+ "thf(mperm_type, type, ( mperm : ( mu > $o ) > mu > $o )).",
1338
+ "thf(mperm_def, definition, ( mperm = ( ^ [P: mu > $o, W: mu] : "
1339
+ "( ? [V: mu] : ( ( d @ W @ V ) & ( P @ V ) ) ) ) )).",
1340
+ "thf(t_type, type, ( t : mu > mu > $o )).",
1341
+ "thf(malways_type, type, ( malways : ( mu > $o ) > mu > $o )).",
1342
+ "thf(malways_def, definition, ( malways = ( ^ [P: mu > $o, W: mu] : "
1343
+ "( ! [V: mu] : ( ( t @ W @ V ) => ( P @ V ) ) ) ) )).",
1344
+ "thf(meventually_type, type, ( meventually : ( mu > $o ) > mu > $o )).",
1345
+ "thf(meventually_def, definition, ( meventually = ( ^ [P: mu > $o, W: mu] : "
1346
+ "( ? [V: mu] : ( ( t @ W @ V ) & ( P @ V ) ) ) ) )).",
1347
+ "thf(mhistorically_type, type, ( mhistorically : ( mu > $o ) > mu > $o )).",
1348
+ "thf(mhistorically_def, definition, ( mhistorically = ( ^ [P: mu > $o, W: mu] : "
1349
+ "( ! [V: mu] : ( ( t @ V @ W ) => ( P @ V ) ) ) ) )).",
1350
+ "thf(monce_type, type, ( monce : ( mu > $o ) > mu > $o )).",
1351
+ "thf(monce_def, definition, ( monce = ( ^ [P: mu > $o, W: mu] : "
1352
+ "( ? [V: mu] : ( ( t @ V @ W ) & ( P @ V ) ) ) ) )).",
1353
+ "thf(tnext_type, type, ( tnext : mu > mu > $o )).",
1354
+ "thf(mnext_type, type, ( mnext : ( mu > $o ) > mu > $o )).",
1355
+ "thf(mnext_def, definition, ( mnext = ( ^ [P: mu > $o, W: mu] : "
1356
+ "( ! [V: mu] : ( ( tnext @ W @ V ) => ( P @ V ) ) ) ) )).",
1357
+ "thf(mprevious_type, type, ( mprevious : ( mu > $o ) > mu > $o )).",
1358
+ "thf(mprevious_def, definition, ( mprevious = ( ^ [P: mu > $o, W: mu] : "
1359
+ "( ! [V: mu] : ( ( tnext @ V @ W ) => ( P @ V ) ) ) ) )).",
1360
+ "thf(muntil_type, type, ( muntil : ( mu > $o ) > ( mu > $o ) > mu > $o )).",
1361
+ "thf(muntil_def, definition, ( muntil = ( ^ [Phi: mu>$o, Psi: mu>$o, W: mu] : "
1362
+ "! [S: mu>$o] : ( ( ( ! [V: mu] : ( ( Psi @ V ) => ( S @ V ) ) ) & "
1363
+ "( ! [V: mu, U: mu] : ( ( ( Phi @ V ) & ( tnext @ V @ U ) & ( S @ U ) ) "
1364
+ "=> ( S @ V ) ) ) ) => ( S @ W ) ) ) )).",
1365
+ "thf(msince_type, type, ( msince : ( mu > $o ) > ( mu > $o ) > mu > $o )).",
1366
+ "thf(msince_def, definition, ( msince = ( ^ [Phi: mu>$o, Psi: mu>$o, W: mu] : "
1367
+ "! [S: mu>$o] : ( ( ( ! [V: mu] : ( ( Psi @ V ) => ( S @ V ) ) ) & "
1368
+ "( ! [V: mu, U: mu] : ( ( ( Phi @ V ) & ( tnext @ U @ V ) & ( S @ U ) ) "
1369
+ "=> ( S @ V ) ) ) ) => ( S @ W ) ) ) )).",
1370
+ ]
1371
+
1372
+ # Per-agent frame axioms for the four agent-indexed relations, keyed the same
1373
+ # way as _ISA_AGENT_CONDS's Isabelle counterpart (ported from the same five
1374
+ # Horn schemas thf_modal.py's own _agent_frame_axioms states in THF).
1375
+ _THF_AGENT_CONDS = {
1376
+ "refl": "thf({tag}_refl, axiom, ( ! [A: $i, W: mu] : ( {rel} @ A @ W @ W ) )).",
1377
+ "trans": ("thf({tag}_trans, axiom, ( ! [A: $i, W: mu, V: mu, U: mu] : "
1378
+ "( ( ( {rel} @ A @ W @ V ) & ( {rel} @ A @ V @ U ) ) "
1379
+ "=> ( {rel} @ A @ W @ U ) ) ))."),
1380
+ "sym": ("thf({tag}_sym, axiom, ( ! [A: $i, W: mu, V: mu] : "
1381
+ "( ( {rel} @ A @ W @ V ) => ( {rel} @ A @ V @ W ) ) ))."),
1382
+ "serial": ("thf({tag}_serial, axiom, ( ! [A: $i, W: mu] : "
1383
+ "( ? [V: mu] : ( {rel} @ A @ W @ V ) ) ))."),
1384
+ "eucl": ("thf({tag}_eucl, axiom, ( ! [A: $i, W: mu, V: mu, U: mu] : "
1385
+ "( ( ( {rel} @ A @ W @ V ) & ( {rel} @ A @ W @ U ) ) "
1386
+ "=> ( {rel} @ A @ V @ U ) ) )).")
1387
+ }
1388
+
1389
+ _THF_FAMILY_REL = {"epistemic": "rk", "doxastic": "rb",
1390
+ "assertive": "rs", "bouletic": "rw"}
1391
+
1392
+
1393
+ def _thf_agent_system_axioms(family: str, system: str) -> List[str]:
1394
+ """THF counterpart of :func:`_agent_system_axioms` -- same systems, same refusals."""
1395
+ if system not in FRAMES:
1396
+ raise ValueError(
1397
+ f"to_thf_ho_modal: unknown system {system!r} for {family} "
1398
+ f"(use one of {sorted(FRAMES)})."
1399
+ )
1400
+ conditions = resolve_frame(system)
1401
+ unsupported = [c for c in conditions if c not in _THF_AGENT_CONDS]
1402
+ if unsupported:
1403
+ raise NotImplementedError(
1404
+ f"to_thf_ho_modal: system {system!r} for {family} needs the frame "
1405
+ f"condition(s) {unsupported}, which have no per-agent axiom schema "
1406
+ f"here; use frame= on the alethic relation for those systems."
1407
+ )
1408
+ rel = _THF_FAMILY_REL[family]
1409
+ return [_THF_AGENT_CONDS[c].format(rel=rel, tag=rel) for c in conditions]
1410
+
1411
+
1412
+ def _thf_family_axiom_lines(usage: "_FamilyUsage", systems: dict,
1413
+ temporal_closure: bool) -> List[str]:
1414
+ """THF ``axiom`` lines for every family/system ``usage``/``systems`` need.
1415
+
1416
+ Mirrors :func:`_family_axiom_lines`'s order and gating exactly, so the
1417
+ Isabelle and THF routes constrain the same relations under the same call.
1418
+ """
1419
+ lines: List[str] = []
1420
+ for family in sorted(systems):
1421
+ lines += _thf_agent_system_axioms(family, systems[family])
1422
+ if usage.deontic:
1423
+ lines.append("thf(d_serial, axiom, ( ! [W: mu] : ( ? [V: mu] : "
1424
+ "( d @ W @ V ) ) )).")
1425
+ if usage.temporal and temporal_closure:
1426
+ lines += [
1427
+ "thf(t_refl, axiom, ( ! [W: mu] : ( t @ W @ W ) )).",
1428
+ "thf(t_trans, axiom, ( ! [W: mu, V: mu, U: mu] : "
1429
+ "( ( ( t @ W @ V ) & ( t @ V @ U ) ) => ( t @ W @ U ) ) )).",
1430
+ ]
1431
+ if usage.temporal and usage.needs_next_rel:
1432
+ lines.append("thf(tnext_in_t, axiom, ( ! [W: mu, V: mu] : "
1433
+ "( ( tnext @ W @ V ) => ( t @ W @ V ) ) )).")
1434
+ return lines
1435
+
1436
+
1437
+ def _thf_domain_lines() -> List[str]:
1438
+ """``existsAt`` typed, unconditionally -- the ONLY family piece kept
1439
+ conditional in this module's THF export (see the module docstring and
1440
+ _domain_vocab_lines's Isabelle counterpart): emitted only for an
1441
+ actualist ``mode=`` combined with an actual individual quantifier, so the
1442
+ default constant-mode THF problem is unchanged from before this port.
1443
+ """
1444
+ return ["thf(existsat_type, type, ( existsat : $i > mu > $o ))."]
1445
+
1446
+
1447
+ def _thf_domain_axiom_lines(mode: str) -> List[str]:
1448
+ """THF counterpart of :func:`_domain_axiom_lines`, over the alethic ``r``."""
1449
+ out = ["thf(nonempty_dom, axiom, ( ! [W: mu] : "
1450
+ "( ? [X: $i] : ( existsat @ X @ W ) ) ))."]
1451
+ if mode in ("increasing", "cumulative"):
1452
+ out.append("thf(cumul_dom, axiom, ( ! [X: $i, W: mu, V: mu] : "
1453
+ "( ( ( existsat @ X @ W ) & ( r @ W @ V ) ) "
1454
+ "=> ( existsat @ X @ V ) ) )).")
1455
+ elif mode == "decreasing":
1456
+ out.append("thf(decr_dom, axiom, ( ! [X: $i, W: mu, V: mu] : "
1457
+ "( ( ( existsat @ X @ V ) & ( r @ W @ V ) ) "
1458
+ "=> ( existsat @ X @ W ) ) )).")
1459
+ return out
1460
+
1461
+
1462
+ def _thf_type(kind) -> str:
1463
+ """Return the THF type of an argument slot: ``$i`` or a property type."""
1464
+ if kind == INDIVIDUAL:
1465
+ return "$i"
1466
+ return "( " + " > ".join(["$i"] * kind[1] + ["mu", "$o"]) + " )"
1467
+
1468
+
1469
+ def _thf_arg(node: Node, upper: Dict[str, str], display: Dict[str, str],
1470
+ depth: int, names: ThfNames, mode: str) -> str:
1471
+ """Render an argument-position node in THF — an individual or a property.
1472
+
1473
+ ``upper`` maps each source name bound in scope to its THF variable token;
1474
+ a variable that is not in it is free and spelled through ``names``. A constant
1475
+ is never looked up in ``upper``: no binder captures it, whatever its spelling.
1476
+ """
1477
+ if isinstance(node, (Variable, LambdaVar)):
1478
+ return upper.get(node.name) or names.functor(FREE_VARIABLE, node.name)
1479
+ if isinstance(node, Constant):
1480
+ return names.functor(CONSTANT, node.name)
1481
+ if isinstance(node, PredicateTerm):
1482
+ return upper.get(node.name) or names.functor("predicate", node.name)
1483
+ if isinstance(node, Lambda):
1484
+ params, body = peel_lambdas(node)
1485
+ fresh = dict(upper)
1486
+ for n in params:
1487
+ fresh[n] = bound_token(n, "_V", fresh)
1488
+ world = f"W{depth}"
1489
+ binders = ", ".join(f"{fresh[n]}: $i" for n in params)
1490
+ return (f"( ^ [{binders}, {world}: mu] : "
1491
+ f"( {_thf(body, fresh, display, depth + 1, names, mode)} @ {world} ) )")
1492
+ if isinstance(node, Function):
1493
+ functor = names.functor("function", node.name)
1494
+ args = " @ ".join(_thf_arg(a, upper, display, depth, names, mode)
1495
+ for a in node.args)
1496
+ return f"( {functor} @ {args} )" if node.args else functor
1497
+ raise UnsupportedHigherOrderNode(
1498
+ f"ho_modal: {type(node).__name__} cannot stand in argument position.")
1499
+
1500
+
1501
+ _BINARY_THF = {And: "mand", Or: "mor", Implies: "mimp", Iff: "miff"}
1502
+
1503
+ #: The embedding's own functors, pre-claimed so no user symbol can take them.
1504
+ _THF_RESERVED = ("mu", "r", "mnot", "mand", "mor", "mimp", "miff", "mbox", "mdia",
1505
+ "mvalid", "rk", "rb", "rs", "rw", "d", "t", "tnext",
1506
+ "mknows", "mbelieves", "msays", "mwants", "mobl", "mperm",
1507
+ "malways", "meventually", "mnext", "mhistorically", "monce",
1508
+ "mprevious", "muntil", "msince",
1509
+ "existsat", "mforall", "mexists", "feq", "fneq") + tuple(ORDERING.values())
1510
+ # ``feq`` / ``fneq`` name nothing in this embedding any more (identity is rigid: the
1511
+ # ``meq`` macro below, not an uninterpreted functor), but they stay claimed so a user
1512
+ # predicate that sanitises onto one of them is still pushed to ``feq_2`` -- the same
1513
+ # choice hol.thf_modal makes -- rather than reading as the old identity alias. ``meq``
1514
+ # is claimed only when the problem contains identity (see to_thf_ho_modal), so an
1515
+ # identity-free problem names its symbols exactly as before.
1516
+
1517
+ #: The identity macro (emitted only when a formula contains identity): the name and the
1518
+ #: DEFINITION are hol.thf_modal's, verbatim -- ``meq = ^ [A: $i, B: $i, W: mu] : ( A = B )``,
1519
+ #: THF's own ``=`` over the individual sort with a world binder the body never mentions
1520
+ #: -- plus the type line every other macro of this prelude carries.
1521
+ _THF_RIGID_EQ_LINES = [
1522
+ f"thf({_THF_RIGID_EQ}_type, type, ( {_THF_RIGID_EQ} : $i > $i > mu > $o )).",
1523
+ _THF_RIGID_EQ_DEF,
1524
+ ]
1525
+
1526
+ #: Refusal text for the counterfactuals, THF side (see the module docstring).
1527
+ _NO_THF_COUNTERFACTUAL = (
1528
+ "to_thf_ho_modal: the counterfactuals □→/◇→ (Would/Might) "
1529
+ "read a similarity ordering of worlds (Lewis spheres), not an "
1530
+ "accessibility relation. Use hol.isabelle_conditional instead."
1531
+ )
1532
+
1533
+ #: N1: ↓, THF side (see _NO_DOWN above for the Isabelle-side twin).
1534
+ _NO_THF_DOWN = (
1535
+ "to_thf_ho_modal: the ↓ binder is not supported by this third-order THF "
1536
+ "embedding — H(@,↓) validity is undecidable. Use "
1537
+ "unicode_logic_kit.fol.modal_translation.down_is_valid (propositional "
1538
+ "H(@,↓), Z3, PROVED-only) or unicode_logic_kit.atp.kripke_enum.KripkeEnumBackend "
1539
+ "/ modal_enum_search (bounded search, REFUTED-only) instead."
1540
+ )
1541
+
1542
+
1543
+ def _thf(node: Node, upper: Dict[str, str], display: Dict[str, str],
1544
+ depth: int, names: ThfNames, mode: str) -> str:
1545
+ """Render ``node`` as a THF term of type ``mu > $o``.
1546
+
1547
+ ``depth`` numbers the world binders this rendering introduces (``W0``,
1548
+ ``W1``, …). Shadowing would be sound — each application picks up its own
1549
+ innermost binder — but a distinct name per level is what makes the emitted
1550
+ problem readable, and readable is what gets a mis-embedding noticed. User
1551
+ variable tokens always carry a ``_V``/``_P`` suffix, so they never meet a
1552
+ world binder.
1553
+ """
1554
+ if isinstance(node, Not):
1555
+ return f"( mnot @ {_thf(node.formula, upper, display, depth, names, mode)} )"
1556
+ op = _BINARY_THF.get(type(node))
1557
+ if op is not None:
1558
+ left = _thf(node.left, upper, display, depth, names, mode)
1559
+ right = _thf(node.right, upper, display, depth, names, mode)
1560
+ return f"( {op} @ {left} @ {right} )"
1561
+ if isinstance(node, Box):
1562
+ return f"( mbox @ {_thf(node.formula, upper, display, depth, names, mode)} )"
1563
+ if isinstance(node, Diamond):
1564
+ return f"( mdia @ {_thf(node.formula, upper, display, depth, names, mode)} )"
1565
+ if isinstance(node, Quantifier):
1566
+ var = bound_token(node.variable.name, "_V", upper)
1567
+ fresh = dict(upper, **{node.variable.name: var})
1568
+ quant = "!" if node.type == "∀" else "?"
1569
+ world = f"W{depth}"
1570
+ body = f"( {_thf(node.formula, fresh, display, depth + 1, names, mode)} @ {world} )"
1571
+ if mode in _ACTUALIST_MODES:
1572
+ # The individual binder becomes existsAt-guarded; a property
1573
+ # binder (below) never is -- the one judgment call this port
1574
+ # makes, see the module docstring.
1575
+ guard = f"( existsat @ {var} @ {world} )"
1576
+ connective = "=>" if quant == "!" else "&"
1577
+ inner = f"( {quant} [{var}: $i] : ( {guard} {connective} {body} ) )"
1578
+ else:
1579
+ inner = f"( {quant} [{var}: $i] : {body} )"
1580
+ return f"( ^ [{world}: mu] : {inner} )"
1581
+ if isinstance(node, SecondOrderQuantifier):
1582
+ var = bound_token(display.get(node.predicate, node.predicate), "_P", upper)
1583
+ fresh = dict(upper, **{node.predicate: var})
1584
+ quant = "!" if node.type == "∀" else "?"
1585
+ ptype = " > ".join(["$i"] * node.arity + ["mu", "$o"])
1586
+ world = f"W{depth}"
1587
+ return (f"( ^ [{world}: mu] : ( {quant} [{var}: {ptype}] : "
1588
+ f"( {_thf(node.formula, fresh, display, depth + 1, names, mode)} @ {world} ) ) )")
1589
+ if isinstance(node, (Knows, Believes, Says, Wants)):
1590
+ macro = {Knows: "mknows", Believes: "mbelieves",
1591
+ Says: "msays", Wants: "mwants"}[type(node)]
1592
+ agent = _thf_arg(node.agent, upper, display, depth, names, mode)
1593
+ body = _thf(node.formula, upper, display, depth, names, mode)
1594
+ return f"( {macro} @ {agent} @ {body} )"
1595
+ if isinstance(node, Obligatory):
1596
+ return f"( mobl @ {_thf(node.formula, upper, display, depth, names, mode)} )"
1597
+ if isinstance(node, Permitted):
1598
+ return f"( mperm @ {_thf(node.formula, upper, display, depth, names, mode)} )"
1599
+ if isinstance(node, Always):
1600
+ return f"( malways @ {_thf(node.formula, upper, display, depth, names, mode)} )"
1601
+ if isinstance(node, Eventually):
1602
+ return f"( meventually @ {_thf(node.formula, upper, display, depth, names, mode)} )"
1603
+ if isinstance(node, Next):
1604
+ return f"( mnext @ {_thf(node.formula, upper, display, depth, names, mode)} )"
1605
+ if isinstance(node, Historically):
1606
+ return f"( mhistorically @ {_thf(node.formula, upper, display, depth, names, mode)} )"
1607
+ if isinstance(node, Once):
1608
+ return f"( monce @ {_thf(node.formula, upper, display, depth, names, mode)} )"
1609
+ if isinstance(node, Previous):
1610
+ return f"( mprevious @ {_thf(node.formula, upper, display, depth, names, mode)} )"
1611
+ if isinstance(node, Until):
1612
+ left = _thf(node.left, upper, display, depth, names, mode)
1613
+ right = _thf(node.right, upper, display, depth, names, mode)
1614
+ return f"( muntil @ {left} @ {right} )"
1615
+ if isinstance(node, Since):
1616
+ left = _thf(node.left, upper, display, depth, names, mode)
1617
+ right = _thf(node.right, upper, display, depth, names, mode)
1618
+ return f"( msince @ {left} @ {right} )"
1619
+ if isinstance(node, Nominal):
1620
+ return f"( ^ [W: mu] : ( W = {names.functor('nominal', node.name)} ) )"
1621
+ if isinstance(node, At):
1622
+ body = _thf(node.formula, upper, display, depth, names, mode)
1623
+ return f"( ^ [W: mu] : ( {body} @ {names.functor('nominal', node.nominal.name)} ) )"
1624
+ if isinstance(node, Down):
1625
+ raise UnsupportedHigherOrderNode(_NO_THF_DOWN)
1626
+ if isinstance(node, Atom):
1627
+ constant = truth_value(node)
1628
+ if constant is not None:
1629
+ # `$true` / `$false`: the proposition true (false) at every world.
1630
+ return "( ^ [W: mu] : $true )" if constant else "( ^ [W: mu] : $false )"
1631
+ if is_equality_atom(node):
1632
+ _identity_sides(node)
1633
+ name = _THF_RIGID_EQ
1634
+ elif node.predicate in ORDERING:
1635
+ name = ORDERING[node.predicate]
1636
+ else:
1637
+ name = upper.get(node.predicate) or names.functor("predicate", node.predicate)
1638
+ if not node.args:
1639
+ return name
1640
+ args = " @ ".join(_thf_arg(a, upper, display, depth, names, mode)
1641
+ for a in node.args)
1642
+ return f"( {name} @ {args} )"
1643
+ if type(node).__name__ in ("Would", "Might"):
1644
+ raise UnsupportedHigherOrderNode(_NO_THF_COUNTERFACTUAL)
1645
+ raise UnsupportedHigherOrderNode(
1646
+ f"ho_modal: no THF reading for {type(node).__name__}; this embedding "
1647
+ f"covers alethic, agent-indexed epistemic/doxastic/assertive/bouletic, "
1648
+ f"deontic, temporal and hybrid families over third-order syntax -- not "
1649
+ f"the Lewis counterfactuals or the group-epistemic operators.")
1650
+
1651
+
1652
+ def to_thf_ho_modal(formula: Node, frame: str = "K",
1653
+ axioms: Sequence[HoAxiom] = (), mode: str = "constant",
1654
+ systems: Optional[dict] = None,
1655
+ temporal_closure: bool = True) -> str:
1656
+ """Emit a THF (TH0) problem: ``axioms`` as hypotheses, ``formula`` as the conjecture.
1657
+
1658
+ The THF counterpart of :func:`isabelle_ho_modal_theory`, for a higher-order
1659
+ ATP (Leo-III, Satallax) rather than Isabelle. Same embedding, same fragment,
1660
+ same refusals, same ``mode=``/``systems=``/``temporal_closure=`` meaning;
1661
+ the toolkit emits the problem and does not run a prover.
1662
+
1663
+ Identity ``=`` / ``≠`` between individuals is the rigid ``meq`` macro (HOL's own ``=``,
1664
+ no world argument; ``≠`` is ``¬(=)``), emitted only when a formula contains identity;
1665
+ a non-binary atom raises ``ValueError`` and identity between PROPERTIES raises
1666
+ ``NotImplementedError`` — see the module docstring's "Equality is rigid, or refused".
1667
+ """
1668
+ if frame not in FRAMES:
1669
+ raise ValueError(f"to_thf_ho_modal: unknown frame {frame!r}.")
1670
+ if mode not in _ACTUALIST_MODES and mode not in _CONSTANT_MODES:
1671
+ raise ValueError(
1672
+ f"to_thf_ho_modal: unknown mode {mode!r} "
1673
+ f"(use one of {sorted(_ACTUALIST_MODES | _CONSTANT_MODES)})."
1674
+ )
1675
+ # Identity is read ONCE, up front, as rigid ``=`` (see _rigid_identity).
1676
+ formulas = _rigid_identity([a.formula for a in axioms] + [formula], "to_thf_ho_modal")
1677
+ apart, display = rename_apart(formulas)
1678
+ signatures = analyse_signatures(apart)
1679
+ bound = set()
1680
+ for f in apart:
1681
+ bound |= bound_pred_names(f)
1682
+ usage = _scan_family_usage(apart)
1683
+ resolved_systems = _validate_systems(usage, systems, caller="to_thf_ho_modal")
1684
+
1685
+ identity = any(_has_identity(f) for f in apart)
1686
+ names = ThfNames(reserved=_THF_RESERVED + ((_THF_RIGID_EQ,) if identity else ()))
1687
+ lines = list(_THF_PRELUDE) + list(_THF_FAMILY_PRELUDE)
1688
+ if identity:
1689
+ lines += _THF_RIGID_EQ_LINES
1690
+ if mode in _ACTUALIST_MODES and usage.has_quant:
1691
+ lines += _thf_domain_lines()
1692
+ for pred in sorted(signatures.slots):
1693
+ if pred in bound or pred in EQUALITY_PREDICATES or pred in ORDERING:
1694
+ continue
1695
+ parts = [_thf_type(k) for k in signatures.slots[pred]]
1696
+ thf_type = " > ".join(parts + ["mu", "$o"]) if parts else "mu > $o"
1697
+ functor = names.functor("predicate", pred)
1698
+ lines.append(f"thf({functor}_type, type, ( {functor} : {thf_type} )).")
1699
+ for kind, name in individual_symbols(apart):
1700
+ functor = names.functor(kind, name)
1701
+ lines.append(f"thf({functor}_type, type, ( {functor} : $i )).")
1702
+ # An ordering atom is a world-dependent uninterpreted relation here, as in qml.
1703
+ # Identity is not: it is the ``meq`` macro above, declared nowhere else.
1704
+ for symbol in sorted({ORDERING[p] for f in apart for p in atom_predicates(f)
1705
+ if p in ORDERING}):
1706
+ lines.append(f"thf({symbol}_type, type, ( {symbol} : $i > $i > mu > $o )).")
1707
+ for name, arity in sorted(function_symbols(apart).items()):
1708
+ functor = names.functor("function", name)
1709
+ ftype = " > ".join(["$i"] * (arity + 1))
1710
+ lines.append(f"thf({functor}_type, type, ( {functor} : {ftype} )).")
1711
+ # Hybrid nominals: one `mu`-typed constant per distinct nominal NAME across
1712
+ # the whole problem, declared up front (like every other free symbol) so
1713
+ # every later reference -- in an axiom or in the goal -- resolves to the
1714
+ # SAME world constant.
1715
+ nominal_names = sorted({n.name for f in apart for n in f.walk()
1716
+ if isinstance(n, Nominal)})
1717
+ for name in nominal_names:
1718
+ functor = names.functor("nominal", name)
1719
+ lines.append(f"thf({functor}_type, type, ( {functor} : mu )).")
1720
+
1721
+ for condition in resolve_frame(frame):
1722
+ if condition not in _THF_FRAME:
1723
+ raise UnsupportedFrameCondition(
1724
+ f"to_thf_ho_modal: frame condition {condition!r} (system "
1725
+ f"{frame!r}) has no axiom registered in this embedding's "
1726
+ f"_THF_FRAME.")
1727
+ lines.append(f"thf(frame_{condition}, axiom, {_THF_FRAME[condition]}).")
1728
+ lines += _thf_family_axiom_lines(usage, resolved_systems, temporal_closure)
1729
+ if mode in _ACTUALIST_MODES and usage.has_quant:
1730
+ lines += _thf_domain_axiom_lines(mode)
1731
+
1732
+ # Annotated-formula names share one namespace; an axiom called "A1" is not
1733
+ # a lower word, and one called "goal" or "mu_type" would clash.
1734
+ for line in lines:
1735
+ if line.startswith("thf("):
1736
+ names.unit(line[4:line.index(",")])
1737
+ for axiom, renamed in zip(axioms, apart):
1738
+ lines.append(f"thf({names.unit(axiom.name)}, axiom, ( mvalid @ "
1739
+ f"{_thf(renamed, {}, display, 0, names, mode)} )).")
1740
+ goal = names.unit("goal")
1741
+ lines.append(f"thf({goal}, conjecture, "
1742
+ f"( mvalid @ {_thf(apart[-1], {}, display, 0, names, mode)} )).")
1743
+ return "\n".join(lines) + "\n"