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,1760 @@
1
+ """Quantified modal logic via a first-order shallow embedding (Benzmüller-style, FO).
2
+
3
+ This is the *first-order fragment* of the shallow semantical embedding (SSE): a modal
4
+ formula over an explicit "current world" is translated into classical first-order
5
+ logic, so the existing back-ends (Z3, the resolution prover) can decide its validity
6
+ or the equivalence of two modal formulas. Unlike the propositional
7
+ :func:`unicode_logic_kit.fol.modal_translation.standard_translation`, this embedding
8
+ handles **object quantifiers** (`∀x` / `∃x`) under a chosen *domain regime*, so the
9
+ **Barcan** formula and its converse come out valid or invalid exactly as the regime
10
+ dictates.
11
+
12
+ Single-sorted embedding with guard predicates (worlds and individuals share one FO
13
+ sort): ``World(t)`` / ``Object(t)`` carve the two kinds apart; accessibility ``R`` is
14
+ typed World×World; existence ``E(x, w)`` ("object ``x`` exists at world ``w``") is
15
+ typed Object×World. The translation:
16
+
17
+ - ``P(t̄)`` → ``P(t̄, w)`` (the world is appended as the last argument) — for every
18
+ predicate EXCEPT identity: ``t₁ = t₂`` → ``t₁ = t₂`` and ``t₁ ≠ t₂`` → ``¬(t₁ = t₂)``,
19
+ with no world argument (see "Equality is rigid" below);
20
+ - ``□φ`` → ``∀v (World(v) ∧ R(w,v) → ST(φ,v))``; ``◇φ`` → ``∃v (World(v) ∧ R(w,v) ∧ ST(φ,v))``;
21
+ - **actualist** ``∀x φ`` → ``∀x (Object(x) ∧ E(x,w) → ST(φ,w))`` and dually ``∃x``;
22
+ - **constant / possibilist** ``∀x φ`` → ``∀x (Object(x) → ST(φ,w))`` (``E`` unused).
23
+
24
+ Domain regimes (the existence-axiom correspondence, verified against the Kripke
25
+ evaluator): **decreasing** ``∀x∀w∀v(E(x,v)∧R(w,v)→E(x,w))`` validates BF; **increasing**
26
+ (cumulative) ``∀x∀w∀v(E(x,w)∧R(w,v)→E(x,v))`` validates CBF; **constant** validates both;
27
+ **varying** neither.
28
+
29
+ **Equality is rigid.** An identity atom (``Atom("=", (t₁, t₂))``, what ``a = b`` parses
30
+ to; ``≠`` is its negation) is translated to the SAME binary identity over the object
31
+ terms, inside the relativised formula, with **no world argument**. So identity does not
32
+ vary by world: the necessity of identity ``a = b → □(a = b)`` and of distinctness
33
+ ``a ≠ b → □(a ≠ b)`` are valid in every frame and under every domain regime, as is
34
+ ``◇(a = b) → a = b``; constants and function symbols were already rigid here (``ST``
35
+ leaves terms alone), and now so is the relation between them. It is Z3's own binary
36
+ identity, so reflexivity, symmetry, transitivity and congruence (hence Leibniz's law,
37
+ ``a = b → (P(a) ↔ P(b))`` and ``a = b → (□P(a) ↔ □P(b))``) come from the solver, not from
38
+ an axiom: :func:`qml_axioms` has none for equality — checked, not assumed (see
39
+ ``tests/test_qml.py``). Two consequences worth stating, because they are easy to misread:
40
+
41
+ - ``□(a = b) → a = b`` is **not** valid in K — a dead-end world makes the box vacuously
42
+ true while ``a`` and ``b`` differ — and is valid in every frame that guarantees a
43
+ successor-or-self (T, S4, S5, KD, KD45 …). Rigidity makes the converse direction
44
+ ``a = b → □(a = b)`` free; the direction back needs the frame.
45
+ - ``a = b`` and ``¬(a = b)`` are not valid: two terms may denote the same object or two.
46
+
47
+ *Varying domains — the choice made here.* Identity ranges over the whole **object
48
+ domain** (everything typed ``Object``), not over the local domain ``D_w`` of the
49
+ world ``ST`` is at; an equality is **not** existence-guarded. So ``a = a``,
50
+ ``a = b → b = a`` and ``a = b → □(a = b)`` are valid under every mode, *including* at a
51
+ world where ``a`` does not exist (a constant outside ``D_w`` still denotes, and is
52
+ still itself). That is the reading the rest of this embedding already has for atoms on
53
+ a non-existent constant — ``P(c, w)`` is left open, neither forced true (positive free
54
+ logic) nor forced false (negative free logic) — and the rigid-designator reading of
55
+ constants (``_signature_typing_facts`` types a constant ``Object``, never ``E(c, w)``).
56
+ Existence is therefore *expressed*, not presupposed, by ``∃x (x = c)``: valid under
57
+ ``constant`` / ``possibilist`` (every object exists everywhere), **not** valid under
58
+ ``varying`` (nor ``increasing`` / ``decreasing``), where the constant may lie outside
59
+ ``D_w`` — and ``∃x (x = c) → □∃x (x = c)`` is valid exactly under the cumulative regime
60
+ (``increasing``) and ``constant``. (Here ``c`` is a CONSTANT, a multi-letter name such as
61
+ ``alice`` in the kit's syntax; a single letter is a variable, and a free variable is a
62
+ parameter, which exists at the world of evaluation: see "A free variable is a
63
+ parameter" below.) The quantifier clauses, the domain axioms and the
64
+ modes themselves are unchanged. A free-logic variant in which ``c = c`` fails for a
65
+ non-existent ``c`` would be a different (negative) logic and is deliberately not
66
+ offered; guard explicitly with ``∃x (x = c) → …`` where that reading is wanted.
67
+ The propositional evaluator :mod:`unicode_logic_kit.semantics.kripke` has no term
68
+ semantics and refuses ``=`` / ``≠`` by name instead, as do the propositional modal
69
+ tableau (:mod:`unicode_logic_kit.atp.modal_tableau`), the propositional standard
70
+ translation (:mod:`unicode_logic_kit.fol.modal_translation`) and the intuitionistic
71
+ GMT embedding (:mod:`unicode_logic_kit.hol.intuitionistic`) — one shared refusal, in
72
+ :func:`unicode_logic_kit.semantics._modal_reject.reject_equality`. The modal HOL
73
+ exports AGREE with this route: :func:`to_thf_modal`,
74
+ :mod:`unicode_logic_kit.hol.thf_modal` and :mod:`unicode_logic_kit.hol.isabelle_modal`
75
+ emit identity as the host logic's own ``=`` over the individual sort with no world
76
+ argument. So on equality a formula has exactly two fates in this kit: rigid
77
+ identity, or a refusal that names the atom.
78
+
79
+ **A free variable is a parameter.** A variable that is free in a formula names ONE unknown
80
+ individual of the object domain, the same everywhere in the formula (the assignment-wise
81
+ consequence relation of the textbooks), and it is rigid, like a constant: the same
82
+ individual at every world. Which individuals it may be depends on the domain regime. Under
83
+ ``constant`` / ``possibilist`` every individual exists at every world, so the parameter is
84
+ typed ``Object(y)`` like a constant and ``∀x P(x) → P(y)`` and ``P(y) → ∃x P(x)`` are
85
+ valid. Under ``varying`` / ``increasing`` / ``decreasing`` the parameter exists at the world
86
+ of evaluation, which the validity query states as ``E(y, w)`` (the existence guard the
87
+ free-logic route gives its parameters), and nowhere else is it required to exist. So
88
+ ``∀x P(x) → P(y)``, ``P(y) → ∃x P(x)`` and ``∃x (x = y)`` are valid in every regime, while
89
+ ``□∀x P(x) → □P(y)`` and ``□∃x (x = y)`` are valid under ``constant`` / ``possibilist`` and
90
+ ``increasing`` only: the individual may be missing from a later world of a ``varying`` or a
91
+ ``decreasing`` model, and then the later world's quantifier does not reach it. A CONSTANT
92
+ is different (see "Equality is rigid"): it may lie outside the domain of the world it is
93
+ read at, so ``∃x (x = alice)`` is valid only under ``constant`` / ``possibilist``. Use a
94
+ constant for that reading, or state the existence of the individual in the formula. For a
95
+ formula with no premise the parameter reading is the universal closure of the formula
96
+ (under a varying regime: of the formula guarded by the existence of its parameters).
97
+ ``P(y) → P(alice)`` and ``(P(y) ∧ Q(z)) → ∀x (P(x) ∧ Q(x))`` are not valid in any regime.
98
+ :func:`qml_translate` leaves a free variable alone; the typing and the guard are part of
99
+ the validity query (:func:`qml_validity_formula`), which :func:`qml_is_valid` and
100
+ :func:`qml_equivalent` decide.
101
+
102
+ **One relation per modal family.** ``R`` is alethic (``□`` / ``◇``, configured by
103
+ ``frame=``), ``T`` is the temporal *henceforth* relation (``Always`` / ``Eventually``
104
+ read it forward, ``Historically`` / ``Once`` read its converse), ``N`` is the *one-step
105
+ successor* (``Next``, and ``Previous`` over its converse) and ``D`` is deontic
106
+ (``Obligatory`` / ``Permitted``); the agent-indexed families use the ternary ``Rk`` /
107
+ ``Rb`` / ``Rs`` / ``Rw`` (configured by ``systems=``). :func:`qml_axioms` types every
108
+ relation the formula actually uses and, for ``T`` / ``N`` / ``D``, asserts these
109
+ **default-on** frame conditions — chosen so this route agrees with
110
+ :func:`unicode_logic_kit.semantics.kripke.satisfies_modal` and with the HOL routes, whose
111
+ ``t_refl`` / ``t_trans`` / ``n_in_t`` / ``d_serial`` are the same conditions
112
+ (:mod:`unicode_logic_kit.hol.isabelle_modal`, :mod:`unicode_logic_kit.hol.thf_modal`):
113
+
114
+ - ``T`` reflexive + transitive, ``N ⊆ T``, and — when both relations occur — the
115
+ ``first_step`` axiom below, the first-order shadow of the HOL routes' ``t_in_nstar``.
116
+ So ``Gφ → φ``, ``Gφ → GGφ``, ``Gφ → Fφ``, ``Gφ → Xφ``, ``φ → Fφ``, the fixpoint
117
+ unfolding ``(φ ∧ XGφ) → Gφ`` and the past mirrors ``Hφ → φ``, ``Hφ → HHφ``,
118
+ ``Hφ → Yφ``, ``φ → Once φ`` are valid, while ``Xφ → φ``, ``φ → Gφ``, ``Fφ → Gφ``,
119
+ ``Gφ → Hφ`` and ``Xφ → Fφ`` correctly stay invalid.
120
+ - ``D`` serial (Standard Deontic Logic, KD), the reading the ``Obligatory`` / ``Permitted``
121
+ node docstrings already declare. ``Oφ → Pφ`` and ``Oφ → ¬O¬φ`` become valid; ``Oφ → φ``
122
+ stays invalid. Honest contract: a ``True`` verdict for a deontic formula therefore means
123
+ "valid over every **serial-deontic** model". ``satisfies_modal`` enforces no seriality of
124
+ its own, so a hand-built dead-end deontic model still refutes ``Oφ → Pφ`` *there*.
125
+
126
+ **The transitive-closure limit — what stays out of reach.** ``satisfies_modal`` evaluates
127
+ ``Always`` / ``Eventually`` over the reflexive-transitive CLOSURE ``N*`` of the one-step
128
+ relation, i.e. the *intended* constraint is ``T = N*``. Reflexivity, transitivity and
129
+ ``N ⊆ T`` axiomatise the half ``T ⊇ N*`` (``N*`` is by definition the least refl-trans
130
+ relation containing ``N``), which is what makes the list above sound. The converse half
131
+ ``T ⊆ N*`` is **not** first-order definable — the same obstacle this module already
132
+ reports for ``Until`` / ``Since`` — because it demands that the witnessing path be
133
+ *finite*. A first-order CONSEQUENCE of it is definable, however, and this route asserts it
134
+ whenever ``T`` and ``N`` both occur (``first_step``, see
135
+ :func:`_temporal_first_step_axiom`)::
136
+
137
+ ∀w ∀v (World(w) ∧ World(v) ∧ T(w,v) → w = v ∨ ∃u (World(u) ∧ N(w,u) ∧ T(u,v)))
138
+
139
+ — "a henceforth-step is either standing still or one step followed by a henceforth-step".
140
+ Every ``T = N*`` model satisfies it, so it over-validates nothing, and with it the ``←``
141
+ direction of the fixpoint unfolding ``(φ ∧ XGφ) → Gφ`` becomes provable here (the ``→``
142
+ direction needs only ``T ⊇ N*``). Temporal induction ``(φ ∧ G(φ → Xφ)) → Gφ`` genuinely
143
+ does stay out of reach — measured: ``first_step`` does not close it — because reaching an
144
+ arbitrary ``T``-successor from the first step needs induction over the closure, which no
145
+ first-order theory states. For that one use the higher-order routes
146
+ :func:`unicode_logic_kit.hol.isabelle_modal.to_isabelle_modal` /
147
+ :func:`unicode_logic_kit.hol.isabelle_runner.isabelle_decide_modal`, whose ``t_in_nstar``
148
+ axiom pins ``t`` to ``rtranclp n``. The honest summary of this route's temporal strength
149
+ is therefore "refl + trans + ``N ⊆ T`` + ``first_step``" — a chosen axiom set, not "the
150
+ limit of first-order logic".
151
+
152
+ **Cross-family bridges** relate two families' relations and are **opt-in**: none is
153
+ emitted by default, because none of them is forced by ``satisfies_modal``. Pass
154
+ ``bridges=["knowledge_implies_belief", ...]``; the table is :data:`QML_BRIDGES`, an
155
+ unknown name raises ``ValueError`` listing the known ones, and so does a bridge whose
156
+ partner family the formula never mentions. Each condition is the exact frame correspondent
157
+ of its schema and is **the same condition the HOL routes emit under the same name** —
158
+ inclusions ``Rb ⊆ Rk`` / ``Rb ⊆ Rs`` for the two agent-indexed bridges, and the meet
159
+ condition ``∀w ∃v (D(w,v) ∧ R(w,v))`` (``d_meets_r``) for ``ought_implies_can``, which is
160
+ deliberately NOT the folklore ``D ⊆ R``: see :func:`_bridge_axiom` for the measured
161
+ witnesses showing that the inclusion validates the unrequested ``□φ → Oφ`` and
162
+ ``Pφ → ◇φ`` while the meet condition validates ``Oφ → ◇φ`` and neither artefact.
163
+
164
+ Validity is ``AX → ∀w (World(w) → ST(φ, w))``, checked with Z3. First-order modal logic
165
+ is undecidable, so this is **sound but bounded-incomplete** (Z3 may not close every
166
+ valid instance) — the model-theoretic partner is
167
+ :func:`unicode_logic_kit.semantics.kripke.satisfies_modal` with per-world ``domains``.
168
+
169
+ **Many-sorted formulas** (``SortedQuantifier``, ``∀x:S φ`` / ``∃x:S φ``) are handled by
170
+ relativizing the WHOLE input formula once, at :func:`qml_translate`'s entry, into the
171
+ guarded plain FOL ``fol.to_fol`` also builds (``∀x:S φ`` → ``∀x (S(x) → φ)``, ``∃x:S φ``
172
+ → ``∃x (S(x) ∧ φ)``) — before anything scans or translates it, so a ``SortedConstant``
173
+ anywhere in the formula (not only directly under a ``SortedQuantifier``) is relativized
174
+ too, and :func:`_signature_typing_facts` sees the resulting plain ``Constant`` like any
175
+ other. ``ST``'s existing ``P(t̄) → P(t̄, w)`` rule then appends the world argument to the
176
+ sort guard exactly as it does to every other atom — so a sort's guard predicate
177
+ ``S(x, w)`` comes out WORLD-RELATIVE (not rigid): an object can be ``S`` at one world and
178
+ not at another, the same "actualist" reading the ``E``xistence guard already gives the
179
+ object domain. :func:`qml_axioms` additionally emits, per sort the formula uses, the same
180
+ "every sort is non-empty" assumption the classical routes make
181
+ (:func:`~unicode_logic_kit.fol._msfl_nodes.nonempty_sort_axioms`) — extended PER WORLD here
182
+ (``∀w (World(w) → ∃x (Object(x) [∧ E(x,w)] ∧ S(x,w)))``) to match the world-relative guard,
183
+ mirroring the existing ``nonempty_dom`` axiom's shape — so e.g. ``∀x:S P(x) → ∃x:S P(x)``
184
+ agrees with the classical Z3 verdict at every world, not just incidentally at one.
185
+
186
+ A SORTED CONSTANT ``c:S`` denotes an element of ``S``, so :func:`qml_axioms` also emits
187
+ ``∀w (World(w) → S(c, w))`` for it (read from the formula as given, where the
188
+ annotation is still there; :func:`~unicode_logic_kit.fol._msfl_nodes.sort_membership_axioms`
189
+ names the constants). A constant is a rigid designator, so the fact holds at EVERY
190
+ world, and it is not guarded by ``E(c, w)``: ``c`` may lie outside the domain of a
191
+ world, which is the reading of constants this module already has. With it
192
+ ``∀x:S P(x) → P(c:S)`` is valid exactly where ``∀x P(x) → P(c)`` is (every constant
193
+ domain; no actualist mode, where ``c`` may be outside ``D_w`` and the instance is not
194
+ available), and it stays invalid with a plain ``c``, which no formula places in ``S``.
195
+
196
+ Public API: :func:`qml_translate`, :func:`qml_axioms`, :func:`qml_is_valid`,
197
+ :func:`qml_equivalent`, :func:`qml_validity_formula` (the closed classical-FOL
198
+ validity query itself, e.g. for :func:`unicode_logic_kit.hets.dol.to_dol_library_from_modal`
199
+ to hand to the CASL/DOL/Hets route), and the constants :data:`BARCAN`,
200
+ :data:`CONVERSE_BARCAN`, :data:`QML_BRIDGES`.
201
+ """
202
+
203
+ from functools import reduce
204
+ from typing import Dict, List, Optional
205
+
206
+ from .nodes import (
207
+ Node, Variable, Atom, Not, And, Or, Xor, Implies, Iff, Quantifier,
208
+ Box, Diamond, Knows, Believes, Says, Wants,
209
+ Always, Eventually, Next, Until,
210
+ Historically, Once, Previous, Since,
211
+ Obligatory, Permitted, SortedQuantifier,
212
+ )
213
+ from ._hybrid_nodes import Down
214
+ from ._fol_nodes import constant_name_to_ascii
215
+ from ._free_parameters import free_parameter_names
216
+ from ._identifiers import fresh_variables
217
+ from ._msfl_nodes import key_text, nonempty_sort_axioms, sort_membership_axioms
218
+ from ._truth_constants import truth_value
219
+ from .frames import (
220
+ FRAME_CONDITIONS, FRAMES as _SHARED_FRAMES, UnsupportedFrameCondition,
221
+ resolve_frame, parse_geach,
222
+ )
223
+ from ._numeral_symbols import numerals_as_constants, prefixed_numeral_name
224
+ from ._symbol_names import SymbolNames, dedupe
225
+
226
+ # Guard / typing predicate names (the contract with the axiom set).
227
+ _WORLD = "World"
228
+ _OBJECT = "Object"
229
+ _E = "E" # existence: E(x, w) ≙ x ∈ D_w
230
+
231
+ # Accessibility-relation names per modality (all typed World×World).
232
+ _R_ALETHIC = "R"
233
+ _R_TEMPORAL = "T"
234
+ _R_NEXT = "N"
235
+ _R_DEONTIC = "D"
236
+ # Epistemic / doxastic / assertive / bouletic accessibility relations are
237
+ # AGENT-INDEXED ternary predicates Rk(agent, w, v) / Rb(agent, w, v) /
238
+ # Rs(agent, w, v) / Rw(agent, w, v), so the agent can be a quantified object
239
+ # variable (``∀x (Student(x) → K_x φ)``) rather than baked into the relation name.
240
+ _R_KNOWS = "Rk"
241
+ _R_BELIEVES = "Rb"
242
+ _R_SAYS = "Rs"
243
+ _R_WANTS = "Rw"
244
+
245
+ # Every predicate name the translation itself emits. A USER predicate (or sort
246
+ # guard) with one of these names used to be read as the translation's own
247
+ # predicate once ST appended its world argument: a unary user ``R`` became
248
+ # ``R(x, w)`` — the alethic accessibility relation — so ``R(alice) →
249
+ # ◇R(alice)`` came out VALID in K, and a binary ``R`` crashed Z3 on the arity
250
+ # clash. :func:`_user_predicate` keeps the two namespaces apart.
251
+ _RESERVED_PREDICATES = frozenset({
252
+ _WORLD, _OBJECT, _E,
253
+ _R_ALETHIC, _R_TEMPORAL, _R_NEXT, _R_DEONTIC,
254
+ _R_KNOWS, _R_BELIEVES, _R_SAYS, _R_WANTS,
255
+ })
256
+ # U+00B7 MIDDLE DOT: punctuation, never part of an identifier the parsers
257
+ # produce, and no reserved name ends in it.
258
+ _USER_MARK = "·"
259
+
260
+
261
+ def _user_predicate(name: str) -> str:
262
+ """The name a USER predicate (or sort guard) gets in the translation.
263
+
264
+ Unchanged unless the name, with any trailing ``·`` stripped, is one of
265
+ :data:`_RESERVED_PREDICATES`; then one ``·`` is appended. The map is
266
+ injective (a changed name ends in ``·`` and strips to a reserved name,
267
+ which no unchanged name does) and never yields a reserved name, so a user
268
+ predicate can no longer be confused with ``World``/``Object``/``E`` or an
269
+ accessibility relation — and every formula that does not use those names
270
+ translates byte-for-byte as before.
271
+ """
272
+ if name.rstrip(_USER_MARK) in _RESERVED_PREDICATES:
273
+ return name + _USER_MARK
274
+ return name
275
+
276
+
277
+ _FORALL = "∀"
278
+ _EXISTS = "∃"
279
+ _ACTUALIST_MODES = frozenset({"varying", "increasing", "cumulative", "decreasing"})
280
+ _CONSTANT_MODES = frozenset({"constant", "possibilist"})
281
+ #: The named modal systems, shared with every other route (see
282
+ #: :mod:`unicode_logic_kit.fol.frames`). A ``G(m,n,r,s)`` Scott–Lemmon spec is
283
+ #: accepted as a frame name too; ``resolve_frame`` resolves both.
284
+ _FRAMES = _SHARED_FRAMES
285
+
286
+
287
+ def _resolve_frame(frame: str, *, route: str = "qml") -> tuple:
288
+ """The conditions of ``frame``, refusing what a FIRST-ORDER route cannot
289
+ express: the three non-first-order conditions (Löb, McKinsey, Grz) are
290
+ named as the scope boundary they are, with the higher-order routes that
291
+ DO carry them pointed at — never silently dropped, which would answer
292
+ about a larger frame class than the caller asked for."""
293
+ try:
294
+ conds = resolve_frame(frame)
295
+ except ValueError as exc:
296
+ raise ValueError(f"{route}: {exc}") from None
297
+ for cond in conds:
298
+ entry = FRAME_CONDITIONS.get(cond)
299
+ if entry is not None and not entry.first_order:
300
+ raise NotImplementedError(
301
+ f"{route}: the frame {frame!r} needs the condition {cond!r} "
302
+ f"({entry.description}), which the first-order embedding "
303
+ "cannot express. Use the higher-order embeddings "
304
+ "hol.thf_modal.to_thf_modal_full / "
305
+ "hol.isabelle_modal.to_isabelle_modal with the same frame — "
306
+ "they assert the schema itself.")
307
+ return conds
308
+
309
+
310
+ def _geach_axiom(spec, W, R):
311
+ """The Scott–Lemmon condition of ``G(m,n,r,s)`` as a closed FO axiom::
312
+
313
+ ∀w,u,v (R^m(w,u) ∧ R^r(w,v) → ∃t (R^n(u,t) ∧ R^s(v,t)))
314
+
315
+ ``R^0(a,b)`` is ``a = b`` and ``R^k`` for ``k > 1`` chains ``k-1``
316
+ existentially bound intermediate worlds, each World-guarded like every
317
+ other world variable here. The axiom is closed over its own variables,
318
+ so it cannot capture anything in a translated formula.
319
+
320
+ Those variables are minted by
321
+ :func:`~unicode_logic_kit.fol._identifiers.fresh_variables` (``v0``, ``v1``,
322
+ …). Until 0.30.0 they were ``_gw`` / ``_gu`` / ``_gv`` / ``_gt`` /
323
+ ``_gz0``, which the kit's own VARIABLE terminal rejects — one letter plus
324
+ digits, no underscore — so an axiom this function built could be printed
325
+ but not read back by :func:`unicode_logic_kit.api.parse_any`.
326
+ """
327
+ mint = _world_minter()
328
+
329
+ def path(a, b, k):
330
+ if k == 0:
331
+ return Atom("=", (a, b))
332
+ previous, mids, conj = a, [], None
333
+ for _ in range(k - 1):
334
+ z = mint()
335
+ mids.append(z)
336
+ step = And(W(z), R(previous, z))
337
+ conj = step if conj is None else And(conj, step)
338
+ previous = z
339
+ body = R(previous, b)
340
+ if conj is not None:
341
+ body = And(conj, body)
342
+ for z in reversed(mids):
343
+ body = Quantifier(_EXISTS, z, body)
344
+ return body
345
+
346
+ w, u, v, s = mint(), mint(), mint(), mint()
347
+ antecedent = And(And(W(w), And(W(u), W(v))),
348
+ And(path(w, u, spec.m), path(w, v, spec.r)))
349
+ consequent = Quantifier(_EXISTS, s, And(
350
+ W(s), And(path(u, s, spec.n), path(v, s, spec.s))))
351
+ body = Implies(antecedent, consequent)
352
+ for var in (v, u, w):
353
+ body = Quantifier(_FORALL, var, body)
354
+ return body
355
+
356
+
357
+ #: The letter every world variable this module mints is built from (plus
358
+ #: digits: ``w0``, ``w1``, …). It matches the propositional standard
359
+ #: translation's own world names, so the two routes' images read alike.
360
+ _WORLD_LETTER = "w"
361
+
362
+ #: The letter the Geach axiom's own bound worlds are built from. A separate
363
+ #: letter only for readability — the axiom is closed, so it cannot capture.
364
+ _GEACH_LETTER = "v"
365
+
366
+
367
+ def _world_minter(reserved=(), letter: str = _GEACH_LETTER):
368
+ """Return a callable minting never-repeating world Variables.
369
+
370
+ The names are :func:`~unicode_logic_kit.fol._identifiers.fresh_variables`'
371
+ — ``letter`` plus digits — because the translation's output has to be text
372
+ this kit's own parser reads back. Until 0.30.0 the shapes were ``_w0`` and
373
+ ``_gz0``, both of which :func:`unicode_logic_kit.api.parse_any` rejects.
374
+ """
375
+ used = set(reserved)
376
+
377
+ def mint() -> Variable:
378
+ name = fresh_variables(1, letter=letter, avoid=used)[0]
379
+ used.add(name)
380
+ return Variable(name)
381
+
382
+ return mint
383
+
384
+
385
+ class _Fresh:
386
+ """Fresh world-variable generator that avoids a reserved set of names.
387
+
388
+ ``reserved`` is the object-variable names of the formula being translated
389
+ (plus the current-world name): a world variable that reused one of them
390
+ would be captured by the object quantifier that binds it. The names are
391
+ ``w0``, ``w1``, … — see :func:`_world_minter`.
392
+ """
393
+
394
+ def __init__(self, reserved):
395
+ self._mint = _world_minter(reserved, _WORLD_LETTER)
396
+
397
+ def next(self) -> Variable:
398
+ return self._mint()
399
+
400
+
401
+ def _object_var_names(node: Node) -> set:
402
+ """All Variable names occurring in ``node`` (object variables, pre-translation)."""
403
+ names = set()
404
+ for n in node.walk():
405
+ if isinstance(n, Variable):
406
+ names.add(n.name)
407
+ return names
408
+
409
+
410
+ def _pick_world_name(formula: Node, preferred: str) -> str:
411
+ """Return a world-variable name not clashing with any object variable in ``formula``.
412
+
413
+ The translation appends the current-world variable as the last argument of every
414
+ atom (``A(x)`` → ``A(x, w)``); if the caller's world name coincides with an object
415
+ variable the formula already binds (e.g. ``∃w A(w)`` with the default world ``w``),
416
+ that object quantifier would *capture* the world parameter and corrupt the
417
+ translation. Keep the preferred name when it is free, otherwise pick a fresh one.
418
+ """
419
+ reserved = _object_var_names(formula)
420
+ if preferred not in reserved:
421
+ return preferred
422
+ # The fallback is a minted name, not ``_world0``: an identifier starting
423
+ # with an underscore is not a VARIABLE the kit's own parser accepts.
424
+ return fresh_variables(1, letter=_WORLD_LETTER, avoid=reserved)[0]
425
+
426
+
427
+ def _box(rel: str, w: Variable, body: Node, fresh: _Fresh, mode: str) -> Node:
428
+ v = fresh.next()
429
+ guard = And(Atom(_WORLD, (v,)), Atom(rel, (w, v)))
430
+ return Quantifier(_FORALL, v, Implies(guard, _st(body, v, fresh, mode)))
431
+
432
+
433
+ def _diamond(rel: str, w: Variable, body: Node, fresh: _Fresh, mode: str) -> Node:
434
+ v = fresh.next()
435
+ guard = And(Atom(_WORLD, (v,)), Atom(rel, (w, v)))
436
+ return Quantifier(_EXISTS, v, And(guard, _st(body, v, fresh, mode)))
437
+
438
+
439
+ def _box_conv(rel: str, w: Variable, body: Node, fresh: _Fresh, mode: str) -> Node:
440
+ """``∀v (World(v) ∧ rel(v, w) → ST(body, v))`` — a box over the CONVERSE relation."""
441
+ v = fresh.next()
442
+ guard = And(Atom(_WORLD, (v,)), Atom(rel, (v, w)))
443
+ return Quantifier(_FORALL, v, Implies(guard, _st(body, v, fresh, mode)))
444
+
445
+
446
+ def _diamond_conv(rel: str, w: Variable, body: Node, fresh: _Fresh, mode: str) -> Node:
447
+ """``∃v (World(v) ∧ rel(v, w) ∧ ST(body, v))`` — a diamond over the CONVERSE relation."""
448
+ v = fresh.next()
449
+ guard = And(Atom(_WORLD, (v,)), Atom(rel, (v, w)))
450
+ return Quantifier(_EXISTS, v, And(guard, _st(body, v, fresh, mode)))
451
+
452
+
453
+ def _box_agent(rel: str, agent: Node, w: Variable, body: Node, fresh: _Fresh, mode: str) -> Node:
454
+ """Agent-indexed □: ``∀v (World(v) ∧ rel(agent, w, v) → ST(body, v))``.
455
+
456
+ ``agent`` is carried into the relation as a real term argument, so a bound object
457
+ variable in agent position quantifies over agents.
458
+ """
459
+ v = fresh.next()
460
+ guard = And(Atom(_WORLD, (v,)), Atom(rel, (agent, w, v)))
461
+ return Quantifier(_FORALL, v, Implies(guard, _st(body, v, fresh, mode)))
462
+
463
+
464
+ _EQUALITY = "="
465
+ _DISEQUALITY = "≠"
466
+ #: The two spellings the kit gives identity atoms (``a = b`` parses to
467
+ #: ``Atom("=", (a, b))``, ``a ≠ b`` to ``Atom("≠", (a, b))``). Neither may ever
468
+ #: reach the generic ``P(t̄) → P(t̄, w)`` rule: see :func:`_st_equality`.
469
+ _EQUALITY_PREDICATES = frozenset({_EQUALITY, _DISEQUALITY})
470
+
471
+
472
+ def _st_equality(atom: Atom) -> Node:
473
+ """ST of an identity atom: the SAME binary identity, with NO world argument.
474
+
475
+ ``t₁ = t₂`` stays ``t₁ = t₂`` and ``t₁ ≠ t₂`` becomes ``¬(t₁ = t₂)``, whatever
476
+ world ``ST`` is currently at. That is what makes identity **rigid** (the module
477
+ docstring's "Equality is rigid" section states the contract and the choice made
478
+ for varying domains): the translated atom does not mention the world, so it
479
+ cannot vary with it. It is Z3's own binary identity, so reflexivity, symmetry,
480
+ transitivity and congruence over function symbols and predicates (Leibniz's law)
481
+ come from the solver, not from any axiom of ours — :func:`qml_axioms` adds none.
482
+
483
+ ``≠`` is lowered to ``¬(=)`` rather than kept as a binary ``≠`` atom because the
484
+ two are the same relation by definition and ``¬(=)`` is the form the downstream
485
+ consumers already read (CASL has no disequality connective at all, see
486
+ :mod:`unicode_logic_kit.hets.dol`).
487
+
488
+ The previous behaviour appended the world like for any other atom, turning
489
+ ``a = b`` into a TERNARY uninterpreted predicate ``=(a, b, w)`` — so ``a = a`` was
490
+ not valid and ``□(a = b) → a = b`` was "valid" only by reflexivity of the frame,
491
+ never by equality. A silently re-interpreted connective is the approximation this
492
+ package refuses; the arity check below is the same rule for a malformed atom.
493
+ """
494
+ if len(atom.args) != 2:
495
+ raise ValueError(
496
+ f"qml: equality atom {atom.predicate!r} needs exactly two terms, got "
497
+ f"{len(atom.args)} ({atom.to_unicode_str()}). '=' / '≠' are reserved for "
498
+ "identity and are never read as a world-relative predicate here; rename "
499
+ "the predicate if a different relation was meant.")
500
+ if atom.predicate == _DISEQUALITY:
501
+ return Not(Atom(_EQUALITY, tuple(atom.args)))
502
+ return atom
503
+
504
+
505
+ def _st(formula: Node, w: Variable, fresh: _Fresh, mode: str) -> Node:
506
+ """The shallow-embedding translation ST(formula, w)."""
507
+ if isinstance(formula, Down):
508
+ # N1: this route already has no rule for plain hybrid nominals/@
509
+ # (they fall through to the generic "unsupported node type" below,
510
+ # naming themselves that way) — ↓ gets its OWN, named check ahead of
511
+ # that generic one because it additionally makes validity
512
+ # undecidable, which is worth saying explicitly rather than leaving
513
+ # a reader to infer it from "unsupported node type Down".
514
+ raise NotImplementedError(
515
+ "qml: the ↓ binder is outside this first-order-modal shallow "
516
+ "embedding (which does not cover hybrid logic at all — no "
517
+ "nominals/@ either — and, separately, ↓ makes validity "
518
+ "undecidable). Use "
519
+ "unicode_logic_kit.fol.modal_translation.down_is_valid "
520
+ "(propositional H(@,↓), Z3, PROVED-only) or "
521
+ "unicode_logic_kit.atp.kripke_enum.KripkeEnumBackend / "
522
+ "modal_enum_search (bounded search, REFUTED-only), or evaluate "
523
+ "directly with unicode_logic_kit.semantics.kripke.satisfies_modal.")
524
+ if isinstance(formula, Atom):
525
+ if truth_value(formula) is not None:
526
+ return formula # `$true` / `$false`: the same at every world, no world argument
527
+ if formula.predicate in _EQUALITY_PREDICATES:
528
+ return _st_equality(formula)
529
+ return Atom(_user_predicate(formula.predicate), (*formula.args, w))
530
+ if isinstance(formula, Not):
531
+ return Not(_st(formula.formula, w, fresh, mode))
532
+ if isinstance(formula, And):
533
+ return And(_st(formula.left, w, fresh, mode), _st(formula.right, w, fresh, mode))
534
+ if isinstance(formula, Or):
535
+ return Or(_st(formula.left, w, fresh, mode), _st(formula.right, w, fresh, mode))
536
+ if isinstance(formula, Xor):
537
+ return Xor(_st(formula.left, w, fresh, mode), _st(formula.right, w, fresh, mode))
538
+ if isinstance(formula, Implies):
539
+ return Implies(_st(formula.left, w, fresh, mode), _st(formula.right, w, fresh, mode))
540
+ if isinstance(formula, Iff):
541
+ return Iff(_st(formula.left, w, fresh, mode), _st(formula.right, w, fresh, mode))
542
+
543
+ if isinstance(formula, Box):
544
+ return _box(_R_ALETHIC, w, formula.formula, fresh, mode)
545
+ if isinstance(formula, Diamond):
546
+ return _diamond(_R_ALETHIC, w, formula.formula, fresh, mode)
547
+ if isinstance(formula, Knows):
548
+ return _box_agent(_R_KNOWS, formula.agent, w, formula.formula, fresh, mode)
549
+ if isinstance(formula, Believes):
550
+ return _box_agent(_R_BELIEVES, formula.agent, w, formula.formula, fresh, mode)
551
+ if isinstance(formula, Says):
552
+ return _box_agent(_R_SAYS, formula.agent, w, formula.formula, fresh, mode)
553
+ if isinstance(formula, Wants):
554
+ return _box_agent(_R_WANTS, formula.agent, w, formula.formula, fresh, mode)
555
+ if isinstance(formula, Obligatory):
556
+ return _box(_R_DEONTIC, w, formula.formula, fresh, mode)
557
+ if isinstance(formula, Permitted):
558
+ return _diamond(_R_DEONTIC, w, formula.formula, fresh, mode)
559
+ if isinstance(formula, Always):
560
+ return _box(_R_TEMPORAL, w, formula.formula, fresh, mode)
561
+ if isinstance(formula, Eventually):
562
+ return _diamond(_R_TEMPORAL, w, formula.formula, fresh, mode)
563
+ if isinstance(formula, Next):
564
+ return _box(_R_NEXT, w, formula.formula, fresh, mode)
565
+ if isinstance(formula, Historically):
566
+ return _box_conv(_R_TEMPORAL, w, formula.formula, fresh, mode)
567
+ if isinstance(formula, Once):
568
+ return _diamond_conv(_R_TEMPORAL, w, formula.formula, fresh, mode)
569
+ if isinstance(formula, Previous):
570
+ return _box_conv(_R_NEXT, w, formula.formula, fresh, mode)
571
+
572
+ if isinstance(formula, Quantifier):
573
+ x = formula.variable
574
+ body = _st(formula.formula, w, fresh, mode)
575
+ obj = Atom(_OBJECT, (x,))
576
+ guard: Node
577
+ if mode in _ACTUALIST_MODES:
578
+ guard = And(obj, Atom(_E, (x, w))) # actualist: x exists at w
579
+ else:
580
+ guard = obj # constant / possibilist
581
+ if formula.type in (_FORALL, "forall"):
582
+ return Quantifier(_FORALL, x, Implies(guard, body))
583
+ if formula.type in (_EXISTS, "exists"):
584
+ return Quantifier(_EXISTS, x, And(guard, body))
585
+ raise ValueError(f"qml: unknown quantifier type {formula.type!r}")
586
+
587
+ if isinstance(formula, Until):
588
+ raise NotImplementedError(
589
+ "qml: Until is not first-order definable (needs transitive closure); "
590
+ "evaluate it with satisfies_modal, or emit the HOL embedding with "
591
+ "to_isabelle_modal (inductive least-fixpoint muntil)."
592
+ )
593
+ if isinstance(formula, Since):
594
+ raise NotImplementedError(
595
+ "qml: Since is not first-order definable (the backward mirror of "
596
+ "Until — it needs a transitive-closure fixpoint); evaluate it with "
597
+ "satisfies_modal, or emit the HOL embedding with to_isabelle_modal "
598
+ "(inductive least-fixpoint msince) / to_thf_modal_full."
599
+ )
600
+ if isinstance(formula, SortedQuantifier):
601
+ # Delegate to the guarded plain Quantifier _relativize builds, then
602
+ # translate THAT: _st's own Quantifier case adds the usual
603
+ # Object/E guard, and its Atom case appends the world argument to
604
+ # the sort guard atom exactly as it does to every other atom, so the
605
+ # two guards compose correctly with no special-casing here (see the
606
+ # module docstring's "Many-sorted formulas" section). Normally
607
+ # unreachable through the public entry points, which already
608
+ # relativize the WHOLE formula once up front (qml_translate) — kept
609
+ # as a defensive fallback for a direct/recursive _st call on an
610
+ # unrelativized sub-formula.
611
+ return _st(formula._relativize([]), w, fresh, mode)
612
+ raise NotImplementedError(f"qml: unsupported node type {type(formula).__name__}.")
613
+
614
+
615
+ def qml_translate(formula: Node, mode: str = "constant", world: str = "w") -> Node:
616
+ """Return the shallow-embedding translation ``ST(formula, world)`` (a classical FO Node).
617
+
618
+ ``mode`` selects the domain regime for object quantifiers — ``"constant"`` /
619
+ ``"possibilist"`` (unrelativised) or ``"varying"`` / ``"increasing"`` /
620
+ ``"decreasing"`` (actualist, guarded by the existence predicate ``E``).
621
+
622
+ If ``world`` clashes with an object variable the formula binds, a fresh world name
623
+ is substituted to prevent that quantifier from capturing the world parameter.
624
+
625
+ A variable that is free in ``formula`` stays free in the image, as the parameter it
626
+ is. What the translation does not state is its typing (an element of the object
627
+ domain) or, under a varying regime, its existence at ``world``: those are facts of the
628
+ validity query, :func:`qml_validity_formula`. See the module docstring's "A free
629
+ variable is a parameter".
630
+
631
+ An identity atom is the one atom that does NOT get the world appended: ``t₁ = t₂``
632
+ stays ``t₁ = t₂`` and ``t₁ ≠ t₂`` becomes ``¬(t₁ = t₂)``, so identity is rigid (see
633
+ the module docstring's "Equality is rigid", which also states the varying-domain
634
+ choice). A non-binary ``=`` / ``≠`` atom raises ``ValueError`` rather than being
635
+ read as a world-relative predicate.
636
+
637
+ A many-sorted ``formula`` (``SortedQuantifier`` / ``SortedConstant``) is relativized
638
+ ONCE, here, before anything else runs — see the module docstring's "Many-sorted
639
+ formulas" section for what that does and does not assume. The relativisation forgets
640
+ which constants were annotated, so the membership of a sorted constant in its sort is
641
+ NOT part of this translation: it is an axiom, which :func:`qml_axioms` emits from the
642
+ formula as given.
643
+ """
644
+ if mode not in _ACTUALIST_MODES and mode not in _CONSTANT_MODES:
645
+ raise ValueError(
646
+ f"qml: unknown mode {mode!r} (use one of "
647
+ f"{sorted(_ACTUALIST_MODES | _CONSTANT_MODES)}).")
648
+ formula = formula._relativize([])
649
+ world = _pick_world_name(formula, world)
650
+ fresh = _Fresh(_object_var_names(formula) | {world})
651
+ return _st(formula, Variable(world), fresh, mode)
652
+
653
+
654
+ def _v(*names):
655
+ return [Variable(n) for n in names]
656
+
657
+
658
+ _AGENT_FAMILIES = {"epistemic": _R_KNOWS, "doxastic": _R_BELIEVES,
659
+ "assertive": _R_SAYS, "bouletic": _R_WANTS}
660
+
661
+
662
+ def _agent_frame_axioms(rel_name: str, conds) -> List[Node]:
663
+ """Frame axioms for an AGENT-INDEXED relation ``Rel(a, w, v)`` (per agent ``a``).
664
+
665
+ Mirrors the alethic frame conditions but quantifies the agent too, so a chosen
666
+ epistemic/doxastic system (e.g. S5 for knowledge, KD45 for belief) constrains every
667
+ agent's accessibility — making e.g. factivity ``K_a φ → φ`` valid under a reflexive
668
+ (T/S4/S5) epistemic system.
669
+ """
670
+ a, w, v, u = _v("a", "w", "v", "u")
671
+ W = lambda z: Atom(_WORLD, (z,))
672
+ O = lambda z: Atom(_OBJECT, (z,))
673
+ Rel = lambda *args: Atom(rel_name, tuple(args))
674
+ fa = lambda var, body: Quantifier(_FORALL, var, body)
675
+ fa4 = lambda body: fa(a, fa(w, fa(v, fa(u, body))))
676
+ out: List[Node] = [
677
+ # typing: Rel(a, w, v) → Object(a) ∧ World(w) ∧ World(v).
678
+ fa(a, fa(w, fa(v, Implies(Rel(a, w, v), And(O(a), And(W(w), W(v))))))),
679
+ ]
680
+ if "refl" in conds:
681
+ out.append(fa(a, fa(w, Implies(And(O(a), W(w)), Rel(a, w, w)))))
682
+ if "trans" in conds:
683
+ out.append(fa4(Implies(
684
+ And(And(O(a), W(w)), And(And(W(v), W(u)), And(Rel(a, w, v), Rel(a, v, u)))),
685
+ Rel(a, w, u))))
686
+ if "sym" in conds:
687
+ out.append(fa(a, fa(w, fa(v, Implies(
688
+ And(And(O(a), W(w)), And(W(v), Rel(a, w, v))), Rel(a, v, w))))))
689
+ if "eucl" in conds:
690
+ out.append(fa4(Implies(
691
+ And(And(O(a), W(w)), And(And(W(v), W(u)), And(Rel(a, w, v), Rel(a, w, u)))),
692
+ Rel(a, v, u))))
693
+ if "serial" in conds:
694
+ out.append(fa(a, fa(w, Implies(And(O(a), W(w)),
695
+ Quantifier(_EXISTS, v, And(W(v), Rel(a, w, v)))))))
696
+ if "directed" in conds:
697
+ out.append(fa(a, fa(w, fa(v, fa(u, Implies(
698
+ And(And(O(a), W(w)), And(And(W(v), W(u)), And(Rel(a, w, v), Rel(a, w, u)))),
699
+ Quantifier(_EXISTS, Variable("z"), And(W(Variable("z")),
700
+ And(Rel(a, v, Variable("z")), Rel(a, u, Variable("z")))))))))))
701
+ if "connected" in conds:
702
+ out.append(fa(a, fa(w, fa(v, fa(u, Implies(
703
+ And(And(O(a), W(w)), And(And(W(v), W(u)), And(Rel(a, w, v), Rel(a, w, u)))),
704
+ Or(Rel(a, v, u), Rel(a, u, v))))))))
705
+ return out
706
+
707
+
708
+ # Every accessibility relation this embedding can introduce. ``qml_axioms`` called
709
+ # WITHOUT a formula ("give me the background theory") emits axioms for all of them.
710
+ QML_RELATIONS = (_R_ALETHIC, _R_TEMPORAL, _R_NEXT, _R_DEONTIC,
711
+ _R_KNOWS, _R_BELIEVES, _R_SAYS, _R_WANTS)
712
+
713
+ # Which relation each modal node reads. ``Until`` / ``Since`` are deliberately ABSENT:
714
+ # ``_st`` rejects them outright (not first-order definable), so they can never reach a
715
+ # validity query, and listing them here would suggest this route handles them.
716
+ _REL_OF_NODE = (
717
+ ((Box, Diamond), _R_ALETHIC),
718
+ ((Obligatory, Permitted), _R_DEONTIC),
719
+ ((Always, Eventually, Historically, Once), _R_TEMPORAL),
720
+ ((Next, Previous), _R_NEXT),
721
+ ((Knows,), _R_KNOWS),
722
+ ((Believes,), _R_BELIEVES),
723
+ ((Says,), _R_SAYS),
724
+ ((Wants,), _R_WANTS),
725
+ )
726
+
727
+
728
+ def _relations_used(formula: Node) -> set:
729
+ """The accessibility relations ``formula`` actually mentions.
730
+
731
+ Gating the frame axioms on this is **not** cosmetic. The deontic seriality axiom is
732
+ the only ∃-quantified frame condition in the set, and Z3's model finder chokes on it:
733
+ measured, asserting ``d_serial`` unconditionally turned ``∃x □A(x) → □∃x A(x)`` under
734
+ varying domains — a formula with no deontic operator at all — from a 0.017 s
735
+ countermodel into a 10 s timeout, i.e. flipped a *reported* verdict. The temporal
736
+ axioms are near-free by comparison, but the rule is uniform: a relation that does not
737
+ occur contributes nothing, so for a formula that uses none of ``T`` / ``N`` / ``D``
738
+ (and no configured ``systems=`` family) :func:`qml_axioms` returns the exact same
739
+ list — same members, same order — as it did before those axioms existed. That
740
+ identity is load-bearing: several suite queries sit on a Z3 knife edge under a short
741
+ timeout, where *any* perturbation of the axiom list can flip a verdict.
742
+ """
743
+ used = set()
744
+ for n in formula.walk():
745
+ for classes, rel in _REL_OF_NODE:
746
+ if isinstance(n, classes):
747
+ used.add(rel)
748
+ break
749
+ return used
750
+
751
+
752
+ def _relation_typing(rel: str) -> Node:
753
+ """``∀w ∀v (rel(w,v) → World(w) ∧ World(v))`` — a binary relation is World×World.
754
+
755
+ Mirrors the ``R`` typing axiom exactly. Without it a model could relate an *object*
756
+ to a world and the guard predicates would no longer carve the universe in two.
757
+ """
758
+ w, v = _v("w", "v")
759
+ return Quantifier(_FORALL, w, Quantifier(_FORALL, v, Implies(
760
+ Atom(rel, (w, v)), And(Atom(_WORLD, (w,)), Atom(_WORLD, (v,))))))
761
+
762
+
763
+ def _temporal_frame_axioms() -> List[Node]:
764
+ """``T`` is reflexive and transitive — the *henceforth* relation.
765
+
766
+ :func:`unicode_logic_kit.semantics.kripke.satisfies_modal` evaluates ``Always`` /
767
+ ``Eventually`` over the reflexive-transitive CLOSURE of the one-step ``"temporal"``
768
+ edges (and ``Historically`` / ``Once`` over the closure of the reversed edges), so
769
+ the current world is always in view and reachability composes. Asserting exactly that
770
+ makes ``Gφ → φ``, ``Gφ → GGφ``, ``Gφ → Fφ``, ``φ → Fφ`` and the past mirrors valid
771
+ here, matching the oracle and the Isabelle route's ``t_refl`` / ``t_trans``.
772
+
773
+ These two axioms plus ``n_in_t`` say ``T ⊇ N*`` and no more. The converse ``T ⊆ N*``
774
+ is not first-order definable, but a first-order CONSEQUENCE of it is, and
775
+ :func:`_temporal_first_step_axiom` asserts it; temporal induction stays out of reach
776
+ even so — see the module docstring and use ``isabelle_decide_modal`` for that one.
777
+ """
778
+ w, v, u = _v("w", "v", "u")
779
+ W = lambda a: Atom(_WORLD, (a,))
780
+ T = lambda a, b: Atom(_R_TEMPORAL, (a, b))
781
+ fa = lambda var, body: Quantifier(_FORALL, var, body)
782
+ return [
783
+ fa(w, Implies(W(w), T(w, w))),
784
+ fa(w, fa(v, fa(u, Implies(
785
+ And(And(W(w), W(v)), And(W(u), And(T(w, v), T(v, u)))), T(w, u))))),
786
+ ]
787
+
788
+
789
+ def _next_in_temporal_axiom() -> Node:
790
+ """``∀w ∀v (World(w) ∧ World(v) ∧ N(w,v) → T(w,v))`` — one step is a step.
791
+
792
+ ``_st`` splits the oracle's single ``"temporal"`` relation into two symbols: ``N``
793
+ carries the raw one-step edges that ``Next`` / ``Previous`` read, ``T`` carries the
794
+ closure that ``Always`` / ``Eventually`` read. Decoupled, the embedding would refute
795
+ ``Gφ → Xφ``, which the oracle validates. This inclusion restores it, and is the FO
796
+ counterpart of ``isabelle_modal``'s ``n_in_t``.
797
+ """
798
+ w, v = _v("w", "v")
799
+ return Quantifier(_FORALL, w, Quantifier(_FORALL, v, Implies(
800
+ And(And(Atom(_WORLD, (w,)), Atom(_WORLD, (v,))), Atom(_R_NEXT, (w, v))),
801
+ Atom(_R_TEMPORAL, (w, v)))))
802
+
803
+
804
+ def _temporal_first_step_axiom() -> Node:
805
+ """``∀w ∀v (World(w) ∧ World(v) ∧ T(w,v) → w = v ∨ ∃u (World(u) ∧ N(w,u) ∧ T(u,v)))``.
806
+
807
+ "A henceforth-step is either standing still or one step followed by a henceforth-step"
808
+ — the FIRST STEP of the path that witnesses ``T``, made explicit. This is the
809
+ first-order half of ``T ⊆ N*`` that *is* expressible: ``T ⊆ N*`` itself demands the
810
+ witnessing path be finite and so is not first-order definable, but every model with
811
+ ``T = N*`` satisfies the implication above, which is what makes asserting it sound.
812
+ ``t_refl`` / ``t_trans`` / ``n_in_t`` alone do not entail it — they hold of any
813
+ refl-trans superset of ``N``, including one with edges no ``N``-path underwrites.
814
+
815
+ What it buys, measured: the ``←`` direction of the fixpoint unfolding
816
+ ``(φ ∧ XGφ) → Gφ`` becomes provable (Z3 closes it in well under a second), where the
817
+ shipped axioms alone left it underivable. What it does NOT buy: temporal induction
818
+ ``(φ ∧ G(φ → Xφ)) → Gφ`` stays underivable, because unrolling the first step finitely
819
+ many times never reaches an arbitrary ``T``-successor — that needs induction over the
820
+ closure, which no first-order theory states. So the honest summary of this route's
821
+ temporal strength is "refl + trans + ``N ⊆ T`` + ``first_step``", not "everything
822
+ first-order logic can reach" and not "everything the oracle validates".
823
+
824
+ The ``w = v`` disjunct is genuine identity: at this level ``Atom("=", [w, v])``
825
+ lowers to Z3's own equality (``Node.to_z3`` maps binary ``=`` natively). An
826
+ object-language ``=`` inside a modal formula is translated to that SAME binary
827
+ identity (rigid, no world argument — see the module docstring), so the two share
828
+ a symbol and differ in what they relate: this axiom's operands are
829
+ ``World``-guarded worlds, a user equality's are objects, and ``World`` / ``Object``
830
+ are disjoint, so neither equates operands of the other kind.
831
+
832
+ Emitted only when ``T`` and ``N`` BOTH occur and ``temporal_closure`` is on, mirroring
833
+ ``isabelle_modal``'s ``t_in_nstar`` (which pins ``t = rtranclp n`` outright, HOL being
834
+ able to say what first-order logic cannot) and for the same reason: the axiom relates
835
+ the two relations, so it is vacuous — and misleading — unless both are in play.
836
+ """
837
+ w, v, u = _v("w", "v", "u")
838
+ W = lambda a: Atom(_WORLD, (a,))
839
+ T = lambda a, b: Atom(_R_TEMPORAL, (a, b))
840
+ fa = lambda var, body: Quantifier(_FORALL, var, body)
841
+ return fa(w, fa(v, Implies(
842
+ And(And(W(w), W(v)), T(w, v)),
843
+ Or(Atom("=", (w, v)),
844
+ Quantifier(_EXISTS, u, And(And(W(u), Atom(_R_NEXT, (w, u))), T(u, v)))))))
845
+
846
+
847
+ def _deontic_frame_axioms() -> List[Node]:
848
+ """``∀w (World(w) → ∃v (World(v) ∧ D(w,v)))`` — the deontic relation is serial (KD).
849
+
850
+ Standard Deontic Logic is exactly this condition, and it is the reading the
851
+ ``Obligatory`` / ``Permitted`` node docstrings and the :mod:`kripke` module docstring
852
+ already declare; both HOL routes commit to it unconditionally
853
+ (``isabelle_modal._deontic_axioms``, ``thf_modal._THF_DEONTIC_AXIOM``). It validates
854
+ ``Oφ → Pφ`` and ``Oφ → ¬O¬φ`` and leaves ``Oφ → φ`` invalid.
855
+
856
+ Consequence for the contract: ``True`` for a deontic formula means "valid over every
857
+ SERIAL-deontic model". ``satisfies_modal`` itself enforces nothing, so a hand-built
858
+ dead-end deontic model refutes ``Oφ → Pφ`` there — that is the honest reading of the
859
+ disagreement, not a bug on either side.
860
+ """
861
+ w, v = _v("w", "v")
862
+ return [Quantifier(_FORALL, w, Implies(
863
+ Atom(_WORLD, (w,)),
864
+ Quantifier(_EXISTS, v, And(Atom(_WORLD, (v,)), Atom(_R_DEONTIC, (w, v))))))]
865
+
866
+
867
+ # Cross-family bridges. Each entry names the two relations the bridge relates (with the
868
+ # operator label to quote in an error message), the schema it is named after, and the
869
+ # frame condition emitted for it.
870
+ #
871
+ # EVERY CONDITION IS THE EXACT FRAME CORRESPONDENT OF ITS SCHEMA — necessary as well as
872
+ # sufficient — and is the SAME condition ``hol.isabelle_modal`` / ``hol.thf_modal`` emit
873
+ # under the same option name, down to the fact name in ``"fact"``. One option name must
874
+ # denote one logic on every route that offers it; a route-vs-route split is exactly the
875
+ # disagreement ``bridges=`` was introduced to remove (``atp.modal_tableau`` refuses the
876
+ # option outright for the same reason).
877
+ #
878
+ # For the two agent-indexed bridges the correspondent is a relation INCLUSION: a box over
879
+ # the super-relation entails the box over the sub-relation, so the relation of the ENTAILED
880
+ # modality is the subset, and the reversed inclusion does not validate the principle.
881
+ #
882
+ # THE ``D ⊆ R`` TRAP — do NOT "simplify" ought_implies_can into an inclusion. Measured
883
+ # (:func:`_bridge_axiom` documents the witnesses): ``D ⊆ R`` on its own does not validate
884
+ # ``Oφ → ◇φ`` at all — a world with no deontic successor makes ``Oφ`` vacuously true while
885
+ # ``◇φ`` is false — and ``D ⊆ R`` TOGETHER WITH the default-on ``d_serial`` validates the
886
+ # strictly stronger ``□φ → Oφ`` ("whatever is necessary is obligatory") plus ``Pφ → ◇φ``,
887
+ # neither of which the caller asked for. The existential "meet" condition below is exactly
888
+ # right: it subsumes deontic seriality and validates the schema and nothing else.
889
+ #: The cross-family bridge schemas :func:`qml_is_valid` accepts in
890
+ #: ``bridges=[...]``, keyed by name. Each entry names the accessibility
891
+ #: relations it needs, the schema it validates, the frame condition that does
892
+ #: so, and the fact name the exporters emit. The same three names are accepted
893
+ #: by the HOL routes via :data:`unicode_logic_kit.hol.BRIDGES`.
894
+ QML_BRIDGES = {
895
+ "knowledge_implies_belief": {
896
+ "needs": ((_R_KNOWS, "Knows"), (_R_BELIEVES, "Believes")),
897
+ "schema": "K_a φ → B_a φ",
898
+ "condition": "Rb ⊆ Rk",
899
+ "fact": "rb_in_rk",
900
+ },
901
+ "sincerity": {
902
+ "needs": ((_R_SAYS, "Says"), (_R_BELIEVES, "Believes")),
903
+ "schema": "Say_a φ → B_a φ",
904
+ "condition": "Rb ⊆ Rs",
905
+ "fact": "rb_in_rs",
906
+ },
907
+ "ought_implies_can": {
908
+ "needs": ((_R_DEONTIC, "Obligatory/Permitted"), (_R_ALETHIC, "□/◇")),
909
+ "schema": "Oφ → ◇φ",
910
+ "condition": "∀w (World(w) → ∃v (World(v) ∧ D(w,v) ∧ R(w,v)))",
911
+ "fact": "d_meets_r",
912
+ },
913
+ }
914
+
915
+ # The two agent-indexed bridges are inclusions between ternary relations; the deontic one
916
+ # is the binary "meet" condition. Kept next to the table so the shape of each axiom is
917
+ # read off one place.
918
+ _BRIDGE_INCLUSIONS = {
919
+ "knowledge_implies_belief": (_R_BELIEVES, _R_KNOWS), # Rb ⊆ Rk
920
+ "sincerity": (_R_BELIEVES, _R_SAYS), # Rb ⊆ Rs
921
+ }
922
+
923
+
924
+ def _bridge_axiom(name: str) -> Node:
925
+ """The frame condition realising the cross-family bridge ``name``.
926
+
927
+ The two INCLUSION bridges are emitted **unguarded** — ``∀a ∀w ∀v (Rb(a,w,v) →
928
+ Rk(a,w,v))``, not the ``Object(a) ∧ World(w) ∧ …`` guarded form. That is deliberate
929
+ and load-bearing: the axiom constrains only the relations, every *use* site is already
930
+ ``World``-guarded by ``_box`` / ``_diamond`` / ``_box_agent``, and a guarded version
931
+ would silently fail to fire for a FREE agent variable, since nothing types a free
932
+ variable as an ``Object`` — so ``∀x (K_x φ → B_x φ)`` would come out invalid.
933
+ Soundness is untouched: any oracle model in which the inclusion holds between world
934
+ pairs expands to a first-order model of the unguarded axiom.
935
+
936
+ ``ought_implies_can`` is the ∃-quantified MEET condition ``∀w (World(w) → ∃v (World(v)
937
+ ∧ D(w,v) ∧ R(w,v)))`` — every world has an alternative that is *both* deontically
938
+ ideal and alethically possible — which is the exact correspondent of ``Oφ → ◇φ`` and
939
+ the same condition the HOL routes emit as ``d_meets_r``. Two details:
940
+
941
+ * Unlike the inclusions this one **must** be ``World``-guarded. Unguarded it would
942
+ demand a ``D``-successor for every element of the universe, including objects; with
943
+ the ``D`` typing axiom that forces every object to be a world, contradicting the
944
+ sort-disjointness axiom — an inconsistent hypothesis, under which *everything*
945
+ comes out valid. The guard costs nothing, since ``_box`` / ``_diamond`` only ever
946
+ instantiate the axiom at worlds.
947
+ * It does **not** validate the ``D ⊆ R`` artefacts ``□φ → Oφ`` and ``Pφ → ◇φ``, and
948
+ that is a measured difference rather than a stylistic one. The frame
949
+ ``W = {0,1,2}``, ``D = {(0,1),(0,2),(1,1),(2,2)}``, ``R = {(0,1),(1,1),(2,2)}``
950
+ satisfies the meet condition at every world, yet refutes ``□P → OP`` at 0 (with
951
+ ``P`` true only at 1) and ``PP → ◇P`` at 0 (with ``P`` true only at 2), while
952
+ ``OP → ◇P`` holds — checked against ``semantics.kripke.satisfies_modal``.
953
+ """
954
+ fa = lambda var, body: Quantifier(_FORALL, var, body)
955
+ if name in _BRIDGE_INCLUSIONS:
956
+ sub, sup = _BRIDGE_INCLUSIONS[name]
957
+ a, w, v = _v("a", "w", "v")
958
+ return fa(a, fa(w, fa(v, Implies(Atom(sub, (a, w, v)), Atom(sup, (a, w, v))))))
959
+ if name != "ought_implies_can":
960
+ raise ValueError(f"qml: no axiom shape for bridge {name!r}.")
961
+ w, v = _v("w", "v")
962
+ return fa(w, Implies(
963
+ Atom(_WORLD, (w,)),
964
+ Quantifier(_EXISTS, v, And(Atom(_WORLD, (v,)),
965
+ And(Atom(_R_DEONTIC, (w, v)), Atom(_R_ALETHIC, (w, v)))))))
966
+
967
+
968
+ def _validate_bridges(bridges) -> List[str]:
969
+ """Normalise and check ``bridges=``; an unknown name must never be silently ignored."""
970
+ if not bridges:
971
+ return []
972
+ if isinstance(bridges, str):
973
+ bridges = [bridges]
974
+ names = list(bridges)
975
+ for name in names:
976
+ if name not in QML_BRIDGES:
977
+ raise ValueError(
978
+ f"qml: unknown bridge {name!r} (use one of {sorted(QML_BRIDGES)}).")
979
+ return names
980
+
981
+
982
+ def _check_bridge_families(names: List[str], used: set) -> None:
983
+ """Reject a bridge whose partner family does not occur in the formula.
984
+
985
+ A bridge is a claim about how two relations sit relative to each other, so asking for
986
+ one while the formula mentions only one of the two families is asking for something
987
+ the query cannot express. The same three policies the HOL routes weigh apply here, and
988
+ the same one wins (see ``hol.isabelle_modal._bridge_axioms``):
989
+
990
+ - *skip it silently*: the caller asked for a logic and would get a strictly weaker one
991
+ with no signal — the "silently weakened logic" failure this package refuses;
992
+ - *emit it anyway*: not conservative. ``d_meets_r`` entails ``∃v R(w,v)``, i.e.
993
+ seriality of the ALETHIC relation, so emitting ``ought_implies_can`` for a ``□``-only
994
+ formula would turn ``□P → ◇P`` from invalid into valid under ``frame="K"`` — a
995
+ silent change to the alethic logic the caller *did* select;
996
+ - *raise*: one uniform rule for every bridge, present and future, and the same rule the
997
+ HOL routes apply, so a ``bridges=`` call means the same thing on every route.
998
+
999
+ ``ValueError`` — not ``NotImplementedError`` — because the route *can* express the
1000
+ bridge; the caller's formula simply does not mention one of the families.
1001
+ ``qml_axioms`` called **without** a formula asks for the whole background theory, in
1002
+ which every relation is in scope, so nothing is rejected there.
1003
+ """
1004
+ for name in names:
1005
+ spec = QML_BRIDGES[name]
1006
+ missing = [op for rel, op in spec["needs"] if rel not in used]
1007
+ if missing:
1008
+ raise ValueError(
1009
+ f"qml: the bridge {name!r} relates two modal families "
1010
+ f"({spec['condition']}), but the formula contains no "
1011
+ f"{' / '.join(missing)} operator, so that relation is unconstrained by "
1012
+ "the query. Emitting the condition anyway is not conservative — "
1013
+ "d_meets_r entails seriality of the alethic R, which would silently make "
1014
+ "□P → ◇P valid under frame='K' — and skipping it would answer for a "
1015
+ "weaker logic than requested. Drop the bridge, state the formula in both "
1016
+ "families, or call qml_axioms() without formula= for the whole "
1017
+ "background theory. (Same rule as to_isabelle_modal / to_thf_modal_full.)")
1018
+
1019
+
1020
+ def qml_axioms(mode: str = "constant", frame: str = "K", systems=None,
1021
+ formula: Optional[Node] = None, bridges=None,
1022
+ temporal_closure: bool = True) -> List[Node]:
1023
+ """Return the background axioms (sort typing, frame conditions, domain regime).
1024
+
1025
+ The conjunction of these is the hypothesis under which a translated formula's
1026
+ validity is checked. ``frame`` ∈ {K, T, S4, S5, KD, KD45} sets the ALETHIC system;
1027
+ ``mode`` selects the domain regime (see the module docstring for the existence-axiom
1028
+ correspondence). ``systems`` optionally sets the frame system for the AGENT-INDEXED
1029
+ epistemic / doxastic relations, e.g. ``systems={"epistemic": "S5", "doxastic": "KD45"}``
1030
+ — so knowledge can be made factive (T/S4/S5) and belief consistent (KD45), symmetric
1031
+ to the THF exporter.
1032
+
1033
+ The temporal (``T`` reflexive + transitive, ``N ⊆ T``) and deontic (``D`` serial)
1034
+ conditions are **on by default**, which is what brings this route into agreement with
1035
+ ``satisfies_modal`` and with the Isabelle / THF exporters; see the module docstring
1036
+ for the resulting validities and for the transitive-closure limit that keeps temporal
1037
+ induction out of reach.
1038
+
1039
+ ``formula`` gates the output: only the relations occurring in it are typed and
1040
+ constrained (see :func:`_relations_used` for why that matters — it is a real
1041
+ performance and *verdict* concern, not tidiness). Called **without** a formula this
1042
+ stays the "give me the whole background theory" call and emits axioms for every
1043
+ relation in :data:`QML_RELATIONS`. The same ``formula`` gates the many-sorted
1044
+ facts: per sort it uses, that the sort is non-empty at every world, and per sorted
1045
+ constant ``c:S``, that ``c`` is in ``S`` at every world (give the formula AS WRITTEN,
1046
+ not :func:`qml_translate`'s relativised form, which has forgotten the annotations).
1047
+
1048
+ ``bridges`` is an opt-in list of cross-family frame conditions from
1049
+ :data:`QML_BRIDGES`; an unknown name raises ``ValueError`` listing the known ones, and
1050
+ so does a bridge whose partner family the ``formula`` never mentions (see
1051
+ :func:`_check_bridge_families` — the same rule the HOL routes apply, so one option
1052
+ name means one thing on every route). A requested bridge is never gated away by
1053
+ :func:`_relations_used`, but it also does not drag its relations' default frame blocks
1054
+ into scope.
1055
+
1056
+ ``temporal_closure=False`` drops ``T``'s reflexivity and transitivity and the
1057
+ ``first_step`` axiom, keeping only ``N ⊆ T``, for parity with
1058
+ ``isabelle_modal_theory`` / ``to_thf_modal_full``. Be aware
1059
+ of what that means on a *decision* route: the answers are then those of a strictly
1060
+ weaker temporal logic than the one ``satisfies_modal`` implements (``Gφ → φ`` and
1061
+ ``Gφ → Fφ`` become underivable), so a ``False`` under it is not evidence about the
1062
+ oracle's temporal logic.
1063
+ """
1064
+ conds = _resolve_frame(frame)
1065
+ if mode not in _ACTUALIST_MODES and mode not in _CONSTANT_MODES:
1066
+ raise ValueError(
1067
+ f"qml: unknown mode {mode!r} (use one of "
1068
+ f"{sorted(_ACTUALIST_MODES | _CONSTANT_MODES)}).")
1069
+ bridge_names = _validate_bridges(bridges)
1070
+ used = set(QML_RELATIONS) if formula is None else _relations_used(formula)
1071
+ _check_bridge_families(bridge_names, used)
1072
+ x, w, v, u = _v("x", "w", "v", "u")
1073
+ t = Variable("t")
1074
+ W = lambda a: Atom(_WORLD, (a,))
1075
+ O = lambda a: Atom(_OBJECT, (a,))
1076
+ R = lambda a, b: Atom(_R_ALETHIC, (a, b))
1077
+ E = lambda a, b: Atom(_E, (a, b))
1078
+ fa = lambda var, body: Quantifier(_FORALL, var, body)
1079
+
1080
+ axioms: List[Node] = [
1081
+ # sort discipline: worlds and objects are disjoint; both kinds are non-empty.
1082
+ fa(t, Not(And(W(t), O(t)))),
1083
+ Quantifier(_EXISTS, w, W(w)),
1084
+ Quantifier(_EXISTS, x, O(x)),
1085
+ # typing of the relations.
1086
+ fa(w, fa(v, Implies(R(w, v), And(W(w), W(v))))),
1087
+ fa(x, fa(w, Implies(E(x, w), And(O(x), W(w))))),
1088
+ ]
1089
+ # typing for the other world×world relations, only where they occur (see
1090
+ # _relations_used). The agent-indexed ones are typed by _agent_frame_axioms.
1091
+ axioms += [_relation_typing(rel)
1092
+ for rel in (_R_TEMPORAL, _R_NEXT, _R_DEONTIC) if rel in used]
1093
+
1094
+ if "refl" in conds:
1095
+ axioms.append(fa(w, Implies(W(w), R(w, w))))
1096
+ if "trans" in conds:
1097
+ axioms.append(fa(w, fa(v, fa(u, Implies(
1098
+ And(And(W(w), W(v)), And(W(u), And(R(w, v), R(v, u)))), R(w, u))))))
1099
+ if "sym" in conds:
1100
+ axioms.append(fa(w, fa(v, Implies(And(And(W(w), W(v)), R(w, v)), R(v, w)))))
1101
+ if "eucl" in conds:
1102
+ axioms.append(fa(w, fa(v, fa(u, Implies(
1103
+ And(And(W(w), W(v)), And(W(u), And(R(w, v), R(w, u)))), R(v, u))))))
1104
+ if "serial" in conds:
1105
+ axioms.append(fa(w, Implies(W(w), Quantifier(_EXISTS, v, And(W(v), R(w, v))))))
1106
+ if "directed" in conds:
1107
+ # .2 convergence: ∀w,v,u (Rwv ∧ Rwu → ∃z (Rvz ∧ Ruz)).
1108
+ axioms.append(fa(w, fa(v, fa(u, Implies(
1109
+ And(And(W(w), W(v)), And(W(u), And(R(w, v), R(w, u)))),
1110
+ Quantifier(_EXISTS, t, And(W(t), And(R(v, t), R(u, t)))))))))
1111
+ if "connected" in conds:
1112
+ # .3 no-branching: ∀w,v,u (Rwv ∧ Rwu → Rvu ∨ Ruv). With reflexivity the
1113
+ # v=u case is covered (Rvv), so no world-equality is needed.
1114
+ axioms.append(fa(w, fa(v, fa(u, Implies(
1115
+ And(And(W(w), W(v)), And(W(u), And(R(w, v), R(w, u)))),
1116
+ Or(R(v, u), R(u, v)))))))
1117
+ if "functional" in conds:
1118
+ # CD: at most one successor. ∀w,v,u (Rwv ∧ Rwu → v = u).
1119
+ axioms.append(fa(w, fa(v, fa(u, Implies(
1120
+ And(And(W(w), W(v)), And(W(u), And(R(w, v), R(w, u)))),
1121
+ Atom("=", (v, u)))))))
1122
+ if "dense" in conds:
1123
+ # C4: ∀w,v (Rwv → ∃u (Rwu ∧ Ruv)).
1124
+ axioms.append(fa(w, fa(v, Implies(
1125
+ And(And(W(w), W(v)), R(w, v)),
1126
+ Quantifier(_EXISTS, u, And(W(u), And(R(w, u), R(u, v))))))))
1127
+ if "shift_refl" in conds:
1128
+ # Ṁ: ∀w,v (Rwv → Rvv) — every accessible world is reflexive.
1129
+ axioms.append(fa(w, fa(v, Implies(
1130
+ And(And(W(w), W(v)), R(w, v)), R(v, v)))))
1131
+ if "empty" in conds:
1132
+ # Ver: no accessible worlds at all, so □φ holds vacuously everywhere.
1133
+ axioms.append(fa(w, fa(v, Not(R(w, v)))))
1134
+ for cond in conds:
1135
+ spec = parse_geach(cond)
1136
+ if spec is not None:
1137
+ axioms.append(_geach_axiom(spec, W, R))
1138
+
1139
+ # inter-modality frame conditions (default-on; see the module docstring). Each block
1140
+ # fires only when its relation occurs, so a formula in the alethic fragment gets the
1141
+ # exact axiom list it got before these existed.
1142
+ if _R_TEMPORAL in used and temporal_closure:
1143
+ axioms += _temporal_frame_axioms()
1144
+ if _R_TEMPORAL in used and _R_NEXT in used:
1145
+ # the link is vacuous — and misleading — unless BOTH relations occur.
1146
+ axioms.append(_next_in_temporal_axiom())
1147
+ if temporal_closure:
1148
+ # ... and, when T is meant to be the closure, say that each T-step starts
1149
+ # with an N-step. Mirrors isabelle_modal's t_in_nstar, which pins t = n**.
1150
+ axioms.append(_temporal_first_step_axiom())
1151
+ if _R_DEONTIC in used:
1152
+ axioms += _deontic_frame_axioms()
1153
+
1154
+ # non-empty local domains (standard classical QML: every world has an existing
1155
+ # individual). The Barcan counter-models all use non-empty domains, so this does
1156
+ # not affect BF/CBF; it makes ∀x φ → ∃x φ valid, and keeps (A) and the THF export
1157
+ # (B), which carries the same nonempty_dom axiom, in agreement.
1158
+ if mode in _ACTUALIST_MODES:
1159
+ axioms.append(fa(w, Implies(W(w), Quantifier(_EXISTS, x, And(O(x), E(x, w))))))
1160
+
1161
+ # non-empty SORTS (many-sorted formulas only): the same MSFOL convention
1162
+ # fol._msfl_nodes.nonempty_sort_axioms enforces for the classical routes
1163
+ # (every sort is non-empty), extended PER WORLD here because a sort guard
1164
+ # is an ordinary, WORLD-RELATIVE atom once qml_translate's up-front
1165
+ # relativisation feeds it through ST (see the module docstring's
1166
+ # "Many-sorted formulas" section) — so ``∀x:S P(x) → ∃x:S P(x)`` agrees
1167
+ # with the classical Z3-via-api.prove verdict at every world, not just
1168
+ # incidentally at one. Gated on ``formula`` the same way
1169
+ # _signature_typing_facts is: there is nothing to scan for sorts without one.
1170
+ if formula is not None:
1171
+ for name in _sort_names_used(formula):
1172
+ s_guard = Atom(_user_predicate(name), (x, w))
1173
+ witness = And(O(x), s_guard)
1174
+ if mode in _ACTUALIST_MODES:
1175
+ witness = And(And(O(x), E(x, w)), s_guard)
1176
+ axioms.append(fa(w, Implies(W(w), Quantifier(_EXISTS, x, witness))))
1177
+ # membership of a SORTED CONSTANT: ``c:S`` denotes an element of ``S``. A
1178
+ # constant is a rigid designator, so the fact is rigid too -- ``S(c, w)`` at
1179
+ # EVERY world -- and it is NOT guarded by ``E(c, w)``: the guarded form makes
1180
+ # no query's verdict differ, because in the constant-domain modes ``E`` is
1181
+ # not forced and in the actualist modes a constant may lie outside ``D_w``
1182
+ # (see the module docstring), so the guard would never fire. Read from the
1183
+ # ORIGINAL formula: the relativised one has already forgotten which
1184
+ # constants were annotated. The atoms ``sort_membership_axioms`` returns
1185
+ # carry no world, so each is lifted here, through the same predicate naming
1186
+ # the guard of a sorted quantifier gets.
1187
+ for member in sort_membership_axioms(formula):
1188
+ assert isinstance(member, Atom) # sort_membership_axioms yields atoms ``S(c)`` only
1189
+ axioms.append(fa(w, Implies(
1190
+ W(w), Atom(_user_predicate(member.predicate),
1191
+ (member.args[0], w)))))
1192
+
1193
+ # domain-regime existence axioms.
1194
+ typed = lambda body: And(And(O(x), W(w)), And(W(v), body))
1195
+ if mode in ("increasing", "cumulative", "constant"):
1196
+ axioms.append(fa(x, fa(w, fa(v, Implies(typed(And(E(x, w), R(w, v))), E(x, v))))))
1197
+ if mode in ("decreasing", "constant"):
1198
+ axioms.append(fa(x, fa(w, fa(v, Implies(typed(And(E(x, v), R(w, v))), E(x, w))))))
1199
+
1200
+ # agent-indexed epistemic / doxastic frame systems (optional).
1201
+ if systems:
1202
+ for fam, sys in systems.items():
1203
+ if fam not in _AGENT_FAMILIES:
1204
+ raise ValueError(
1205
+ f"qml: unknown modal family {fam!r} for systems= "
1206
+ f"(use one of {sorted(_AGENT_FAMILIES)}).")
1207
+ if sys not in _FRAMES:
1208
+ raise ValueError(
1209
+ f"qml: unknown system {sys!r} for {fam} (use one of {sorted(_FRAMES)}).")
1210
+ # both validators stay UNCONDITIONAL — a typo in systems= must be reported
1211
+ # even when the configured family does not occur in the formula.
1212
+ if _AGENT_FAMILIES[fam] in used:
1213
+ axioms += _agent_frame_axioms(_AGENT_FAMILIES[fam], _FRAMES[sys])
1214
+
1215
+ # explicitly requested bridges are never gated away, but they also do not pull their
1216
+ # relations' default frame blocks into scope (see the qml_axioms docstring).
1217
+ axioms += [_bridge_axiom(name) for name in bridge_names]
1218
+ return axioms
1219
+
1220
+
1221
+ def _sort_names_used(formula: Node) -> List[str]:
1222
+ """Distinct sort names ``formula`` mentions, in first-occurrence order.
1223
+
1224
+ Reuses :func:`~unicode_logic_kit.fol._msfl_nodes.nonempty_sort_axioms`'s own
1225
+ scan (each of its returned ``∃x (S(x))`` sentences names its sort as the
1226
+ predicate of its body atom) rather than re-implementing "which of the four
1227
+ many-sorted node types occur" here — so this route and the classical one
1228
+ can never disagree about which sorts a formula uses.
1229
+ """
1230
+ sorts: List[str] = []
1231
+ for axiom in nonempty_sort_axioms(formula):
1232
+ assert isinstance(axiom, Quantifier) and isinstance(axiom.formula, Atom) # ∃x S(x)
1233
+ sorts.append(axiom.formula.predicate)
1234
+ return sorts
1235
+
1236
+
1237
+ def _signature_typing_facts(formula: Node) -> List[Node]:
1238
+ """Object-typing facts for the constants and functions of ``formula``.
1239
+
1240
+ The guarded embedding sorts the universe into Worlds and Objects. A bare
1241
+ constant is otherwise UNTYPED: no model is forced to put it in Object, so
1242
+ an Object-guarded quantifier could never instantiate it — ``∀x P(x) →
1243
+ P(c)`` came out spuriously invalid, and an Object-guarded agent frame
1244
+ axiom (``systems=``) never fired for a NAMED agent. Constants (including
1245
+ numbers and agent names) denote objects rigidly, matching
1246
+ ``satisfies_modal``; a function maps objects to objects.
1247
+ """
1248
+ from .nodes import Constant as _C, Number as _N, Function as _F
1249
+ consts, funcs = {}, {}
1250
+ for n in formula.walk():
1251
+ if isinstance(n, (_C, _N)):
1252
+ # keyed (and so ordered) by the bare name, as ever: the key is not shown anywhere
1253
+ consts[key_text(n)] = n
1254
+ elif isinstance(n, _F):
1255
+ funcs[(n.name, len(n.args))] = n
1256
+ facts: List[Node] = [Atom(_OBJECT, (t,))
1257
+ for _, t in sorted(consts.items())]
1258
+ for (name, arity), fn in sorted(funcs.items()):
1259
+ xs = [Variable(f"_a{i}") for i in range(arity)]
1260
+ guards: List[Node] = [Atom(_OBJECT, (x,)) for x in xs]
1261
+ guard = reduce(And, guards)
1262
+ body: Node = Implies(guard, Atom(_OBJECT, (type(fn)(name, tuple(xs)),)))
1263
+ for x in reversed(xs):
1264
+ body = Quantifier(_FORALL, x, body)
1265
+ facts.append(body)
1266
+ return facts
1267
+
1268
+
1269
+ def _validity_formula(formula: Node, mode: str, frame: str, systems=None,
1270
+ bridges=None, temporal_closure: bool = True) -> Node:
1271
+ """Build ``⋀axioms ∧ ⋀typing-facts → ∀w (World(w) → ST(formula, w))``.
1272
+
1273
+ The axioms are gated on ``formula``'s signature, so a query only carries the frame
1274
+ conditions of the relations it actually mentions. New parameters are APPENDED, so the
1275
+ positional call in :mod:`unicode_logic_kit.atp.resolution` keeps working unchanged and
1276
+ inherits the gating.
1277
+
1278
+ A free variable of ``formula`` is a parameter (the module docstring's "A free variable
1279
+ is a parameter"), and it stays free in the query: it is one unknown individual, so a
1280
+ solver that reads it as an uninterpreted constant and one that closes the whole
1281
+ query universally decide the same validity. Under ``constant`` / ``possibilist`` the
1282
+ individual is typed ``Object(y)``, among the typing facts, as every constant is. Under
1283
+ an actualist regime it is guarded where the formula is evaluated: the consequent is
1284
+ ``∀w (World(w) ∧ E(y, w) → ST(formula, w))``.
1285
+ """
1286
+ # The sort-name / relation-usage scan in qml_axioms needs the ORIGINAL,
1287
+ # un-relativized ``formula`` (a SortedQuantifier/SortedConstant is what it
1288
+ # looks for) — so that scan runs on ``formula`` itself, while everything
1289
+ # downstream of it (typing facts, translation) runs on ONE relativized
1290
+ # copy, so a SortedConstant gets typed as an Object exactly like a plain
1291
+ # Constant would (see qml_translate's own up-front relativisation, which
1292
+ # this pre-empts so _signature_typing_facts sees the same plain
1293
+ # Constant qml_translate's translation will).
1294
+ axioms = qml_axioms(mode, frame, systems, formula=formula, bridges=bridges,
1295
+ temporal_closure=temporal_closure)
1296
+ relativized = formula._relativize([])
1297
+ axioms += _signature_typing_facts(relativized)
1298
+ parameters = [Variable(name) for name in free_parameter_names([relativized])]
1299
+ if mode in _CONSTANT_MODES:
1300
+ axioms += [Atom(_OBJECT, (parameter,)) for parameter in parameters]
1301
+ w = _pick_world_name(relativized, "w")
1302
+ where: Node = Atom(_WORLD, (Variable(w),))
1303
+ if mode in _ACTUALIST_MODES:
1304
+ for parameter in parameters:
1305
+ where = And(where, Atom(_E, (parameter, Variable(w))))
1306
+ body = Implies(where, qml_translate(relativized, mode, world=w))
1307
+ closed = Quantifier(_FORALL, Variable(w), body)
1308
+ hyp = reduce(And, axioms)
1309
+ return Implies(hyp, closed)
1310
+
1311
+
1312
+ def qml_is_valid(formula: Node, mode: str = "constant", frame: str = "K",
1313
+ systems=None, timeout: int = 10000, bridges=None,
1314
+ temporal_closure: bool = True) -> bool:
1315
+ """Return True iff ``formula`` is QML-valid under ``mode`` / ``frame`` (via Z3).
1316
+
1317
+ ``systems`` optionally sets the agent-indexed epistemic / doxastic frame systems,
1318
+ e.g. ``systems={"epistemic": "S5"}`` makes knowledge factive so ``∀x (K_x φ → φ)``
1319
+ comes out valid. ``bridges`` opts into the cross-family relation inclusions of
1320
+ :data:`QML_BRIDGES` (none by default).
1321
+
1322
+ Sound but bounded-incomplete: ``True`` means Z3 proved validity; ``False`` means it
1323
+ did not (a genuine countermodel, or — since first-order modal logic is undecidable
1324
+ — an instance Z3 could not close). For a definite countermodel use
1325
+ :func:`unicode_logic_kit.semantics.kripke.satisfies_modal` over an explicit model.
1326
+
1327
+ Two contract details worth knowing before reading a verdict:
1328
+
1329
+ - a temporal or deontic formula is judged under the default-on frame conditions, so
1330
+ ``True`` for a deontic formula means "valid over every SERIAL-deontic model", and
1331
+ temporal induction ``(φ ∧ G(φ → Xφ)) → Gφ`` comes back ``False`` here despite
1332
+ holding in every intended model — decide that one with
1333
+ :func:`unicode_logic_kit.hol.isabelle_runner.isabelle_decide_modal`;
1334
+ - an identity atom ``=`` / ``≠`` is rigid (no world argument) and existence-
1335
+ independent under every ``mode``; see the module docstring's "Equality is rigid"
1336
+ for the varying-domain choice and for why ``□(a = b) → a = b`` needs a frame
1337
+ with a successor-or-self (T, S4, S5, KD …) while ``a = b → □(a = b)`` holds in K;
1338
+ - a variable that is free in ``formula`` is a PARAMETER: one unknown individual, the
1339
+ same everywhere in the formula. Under ``mode='constant'`` / ``'possibilist'`` it is an
1340
+ element of the object domain, so ``∀x P(x) → P(y)`` and ``P(y) → ∃x P(x)`` are valid;
1341
+ under a varying regime it exists at the world of evaluation (and need not exist at any
1342
+ other), so those two are valid there too while ``□∀x P(x) → □P(y)`` is valid under
1343
+ ``'constant'`` / ``'possibilist'`` / ``'increasing'`` only. A constant is not read
1344
+ that way (it may lie outside the world's domain); see the module docstring's "A free
1345
+ variable is a parameter";
1346
+ - a deontic ``False`` is typically Z3 returning *unknown* rather than a countermodel:
1347
+ the ∃-quantified seriality axiom defeats its model finder, measured identical at
1348
+ 1 s / 2 s / 10 s budgets (the same behaviour ``frame="KD"`` has always had). A short
1349
+ ``timeout`` therefore costs no reliability on deontic non-theorems.
1350
+ """
1351
+ from ..atp.z3_models import is_valid
1352
+ return is_valid(_validity_formula(formula, mode, frame, systems, bridges=bridges,
1353
+ temporal_closure=temporal_closure), timeout=timeout)
1354
+
1355
+
1356
+ def qml_validity_formula(formula: Node, mode: str = "constant", frame: str = "K",
1357
+ systems=None, bridges=None,
1358
+ temporal_closure: bool = True) -> Node:
1359
+ """Return the classical-FOL validity query :func:`qml_is_valid` decides,
1360
+ as a :class:`Node` — closed unless ``formula`` has a free variable, which stays
1361
+ free in it as the parameter it is (closing the query universally gives the same
1362
+ validity) — the documented, public counterpart of the private
1363
+ :func:`_validity_formula` this function simply delegates to (kept private and
1364
+ unchanged, including its positional-argument shape, since
1365
+ :mod:`unicode_logic_kit.atp.resolution` already calls it positionally).
1366
+
1367
+ ``⋀axioms ∧ ⋀typing-facts → ∀w (World(w) → ST(formula, w))`` — see
1368
+ :func:`_validity_formula`'s own docstring for exactly what ``axioms`` and
1369
+ ``typing-facts`` contain. The Node this returns is built ONLY from
1370
+ :class:`Atom`/:class:`Not`/:class:`And`/:class:`Or`/:class:`Xor`/
1371
+ :class:`Implies`/:class:`Iff`/:class:`Quantifier` and the term classes
1372
+ :class:`Variable`/:class:`Constant`/:class:`Function` — i.e. exactly the
1373
+ classical FOL fragment :mod:`unicode_logic_kit.fol.casl_export` accepts — so it
1374
+ is ready to hand to :func:`~unicode_logic_kit.fol.casl_export.to_casl_spec`
1375
+ directly. The fresh world / Geach variables this route mints are ``w0`` / ``v0``
1376
+ style names (:func:`unicode_logic_kit.fol._identifiers.fresh_variables`), which are
1377
+ legal CASL identifiers as well as legal names for this kit's own parser — before
1378
+ 0.30.0 they were ``_w0`` / ``_gz0``, and :mod:`unicode_logic_kit.hets.dol` had to
1379
+ rename every one. An object-language ``=`` needs no handling either: it is
1380
+ translated to the SAME binary, rigid identity (no world argument — see the module
1381
+ docstring's "Equality is rigid"), which is exactly CASL's own fixed built-in
1382
+ ``=``, and ``≠`` to ``¬(=)``, which ``casl_export`` renders as it renders any
1383
+ negation. What ``hets.dol`` still has to rename is the ``·`` mark this module
1384
+ appends to a user predicate named like one of its own relations (``R`` → ``R·``):
1385
+ U+00B7 is punctuation no CASL identifier may contain. Use
1386
+ :func:`unicode_logic_kit.hets.dol.to_dol_library_from_modal` — it calls this
1387
+ function, sanitises the names injectively, and renders the result as a complete
1388
+ DOL library over :func:`~unicode_logic_kit.fol.casl_export.to_casl_spec` (see that
1389
+ module's own docstring for the sanitisation contract) — rather than feeding this
1390
+ function's raw output to ``to_casl_spec`` yourself.
1391
+ """
1392
+ return _validity_formula(formula, mode, frame, systems, bridges=bridges,
1393
+ temporal_closure=temporal_closure)
1394
+
1395
+
1396
+ def qml_equivalent(left: Node, right: Node, mode: str = "constant", frame: str = "K",
1397
+ systems=None, timeout: int = 10000, bridges=None,
1398
+ temporal_closure: bool = True) -> bool:
1399
+ """Return True iff two modal formulas are QML-equivalent under ``mode`` / ``frame``.
1400
+
1401
+ A variable free in either formula is one parameter shared by both (see
1402
+ :func:`qml_is_valid`): the formulas are equivalent when ``left ↔ right`` is valid for
1403
+ every individual it may denote.
1404
+ """
1405
+ return qml_is_valid(Iff(left, right), mode=mode, frame=frame, systems=systems,
1406
+ timeout=timeout, bridges=bridges,
1407
+ temporal_closure=temporal_closure)
1408
+
1409
+
1410
+ # The Barcan formula and its converse, over a unary predicate A — the standard
1411
+ # litmus tests for the domain regime.
1412
+ def _barcan_pair():
1413
+ x = Variable("x")
1414
+ A = lambda t: Atom("A", [t])
1415
+ bf = Implies(Diamond(Quantifier(_EXISTS, x, A(x))),
1416
+ Quantifier(_EXISTS, x, Diamond(A(x))))
1417
+ cbf = Implies(Quantifier(_EXISTS, x, Diamond(A(x))),
1418
+ Diamond(Quantifier(_EXISTS, x, A(x))))
1419
+ return bf, cbf
1420
+
1421
+
1422
+ BARCAN, CONVERSE_BARCAN = _barcan_pair()
1423
+
1424
+
1425
+ # ===========================================================================
1426
+ # (B) Higher-order shallow embedding — TPTP THF export (Benzmüller-style)
1427
+ # ===========================================================================
1428
+ #
1429
+ # A genuine higher-order shallow embedding: modal propositions are functions
1430
+ # ``mu > $o`` (world → bool), the modalities are λ-lifted quantifiers over the
1431
+ # accessibility relation ``r``, and object quantifiers are ``existsAt``-guarded
1432
+ # (actualist). The emitted THF problem is decidable by a higher-order ATP
1433
+ # (Leo-III, Satallax) — the toolkit emits it the way it emits TPTP/Prover9, it
1434
+ # does not run it. Covers the alethic □/◇ fragment.
1435
+
1436
+ _THF_DEFS = """\
1437
+ thf(mnot, definition, ( mnot = ( ^ [Phi: mu>$o, W: mu] : ~ ( Phi @ W ) ) )).
1438
+ thf(mand, definition, ( mand = ( ^ [Phi: mu>$o, Psi: mu>$o, W: mu] : ( ( Phi @ W ) & ( Psi @ W ) ) ) )).
1439
+ thf(mor, definition, ( mor = ( ^ [Phi: mu>$o, Psi: mu>$o, W: mu] : ( ( Phi @ W ) | ( Psi @ W ) ) ) )).
1440
+ thf(mimplies, definition, ( mimplies = ( ^ [Phi: mu>$o, Psi: mu>$o, W: mu] : ( ( Phi @ W ) => ( Psi @ W ) ) ) )).
1441
+ thf(mequiv, definition, ( mequiv = ( ^ [Phi: mu>$o, Psi: mu>$o, W: mu] : ( ( Phi @ W ) <=> ( Psi @ W ) ) ) )).
1442
+ thf(mbox, definition, ( mbox = ( ^ [Phi: mu>$o, W: mu] : ! [V: mu] : ( ( r @ W @ V ) => ( Phi @ V ) ) ) )).
1443
+ thf(mdia, definition, ( mdia = ( ^ [Phi: mu>$o, W: mu] : ? [V: mu] : ( ( r @ W @ V ) & ( Phi @ V ) ) ) )).
1444
+ thf(mforall, definition, ( mforall = ( ^ [Phi: $i>(mu>$o), W: mu] : ! [X: $i] : ( ( existsAt @ X @ W ) => ( Phi @ X @ W ) ) ) )).
1445
+ thf(mexists, definition, ( mexists = ( ^ [Phi: $i>(mu>$o), W: mu] : ? [X: $i] : ( ( existsAt @ X @ W ) & ( Phi @ X @ W ) ) ) )).
1446
+ thf(mvalid, definition, ( mvalid = ( ^ [Phi: mu>$o] : ! [W: mu] : ( Phi @ W ) ) )).\
1447
+ """
1448
+
1449
+ _THF_FRAME = {
1450
+ "refl": "thf(refl, axiom, ( ! [W: mu] : ( r @ W @ W ) )).",
1451
+ "trans": "thf(trans, axiom, ( ! [W: mu, V: mu, U: mu] : ( ( ( r @ W @ V ) & ( r @ V @ U ) ) => ( r @ W @ U ) ) )).",
1452
+ "sym": "thf(symm, axiom, ( ! [W: mu, V: mu] : ( ( r @ W @ V ) => ( r @ V @ W ) ) )).",
1453
+ "serial": "thf(serial, axiom, ( ! [W: mu] : ? [V: mu] : ( r @ W @ V ) )).",
1454
+ "eucl": "thf(euclid, axiom, ( ! [W: mu, V: mu, U: mu] : ( ( ( r @ W @ V ) & ( r @ W @ U ) ) => ( r @ V @ U ) ) )).",
1455
+ # .2 convergence: any two successors of a world have a common successor.
1456
+ "directed": "thf(directed, axiom, ( ! [W: mu, V: mu, U: mu] : ( ( ( r @ W @ V ) & ( r @ W @ U ) ) => ? [Z: mu] : ( ( r @ V @ Z ) & ( r @ U @ Z ) ) ) )).",
1457
+ # .3 no-branching: the successors of a world are linearly r-ordered.
1458
+ "connected": "thf(connected, axiom, ( ! [W: mu, V: mu, U: mu] : ( ( ( r @ W @ V ) & ( r @ W @ U ) ) => ( ( r @ V @ U ) | ( r @ U @ V ) ) ) )).",
1459
+ # CD: at most one successor.
1460
+ "functional": "thf(functional, axiom, ( ! [W: mu, V: mu, U: mu] : ( ( ( r @ W @ V ) & ( r @ W @ U ) ) => ( V = U ) ) )).",
1461
+ # C4: dense.
1462
+ "dense": "thf(dense, axiom, ( ! [W: mu, V: mu] : ( ( r @ W @ V ) => ? [U: mu] : ( ( r @ W @ U ) & ( r @ U @ V ) ) ) )).",
1463
+ # Ṁ: every accessible world is reflexive.
1464
+ "shift_refl": "thf(shift_refl, axiom, ( ! [W: mu, V: mu] : ( ( r @ W @ V ) => ( r @ V @ V ) ) )).",
1465
+ # Ver: no accessible worlds at all.
1466
+ "empty": "thf(empty_r, axiom, ( ! [W: mu, V: mu] : ~ ( r @ W @ V ) )).",
1467
+ # The three schemas with NO first-order frame condition are asserted as
1468
+ # schemas, quantified over propositions — which is exactly what the
1469
+ # higher-order route can do and every first-order route cannot.
1470
+ "loeb": "thf(loeb, axiom, ( ! [Phi: mu > $o, W: mu] : ( ( mbox @ ( mimplies @ ( mbox @ Phi ) @ Phi ) @ W ) => ( mbox @ Phi @ W ) ) )).",
1471
+ "mckinsey": "thf(mckinsey, axiom, ( ! [Phi: mu > $o, W: mu] : ( ( mbox @ ( mdia @ Phi ) @ W ) => ( mdia @ ( mbox @ Phi ) @ W ) ) )).",
1472
+ "grz": "thf(grz, axiom, ( ! [Phi: mu > $o, W: mu] : ( ( mbox @ ( mimplies @ ( mbox @ ( mimplies @ Phi @ ( mbox @ Phi ) ) ) @ Phi ) @ W ) => ( Phi @ W ) ) )).",
1473
+ }
1474
+
1475
+ _THF_DOMAIN = {
1476
+ "constant": "thf(const_dom, axiom, ( ! [X: $i, W: mu] : ( existsAt @ X @ W ) )).",
1477
+ "increasing": "thf(cumulative_dom, axiom, ( ! [X: $i, W: mu, V: mu] : ( ( ( existsAt @ X @ W ) & ( r @ W @ V ) ) => ( existsAt @ X @ V ) ) )).",
1478
+ "decreasing": "thf(decreasing_dom, axiom, ( ! [X: $i, W: mu, V: mu] : ( ( ( existsAt @ X @ V ) & ( r @ W @ V ) ) => ( existsAt @ X @ W ) ) )).",
1479
+ }
1480
+ _THF_DOMAIN["cumulative"] = _THF_DOMAIN["increasing"]
1481
+ # possibilist ≡ constant domain (every individual exists at every world): the FO
1482
+ # embedding treats them identically, so the THF export must emit const_dom too —
1483
+ # otherwise its actualist mforall/mexists macros would model a varying domain.
1484
+ _THF_DOMAIN["possibilist"] = _THF_DOMAIN["constant"]
1485
+
1486
+
1487
+ # `feq` / `fneq` are the functors equality USED to get in the THF / Isabelle exports of
1488
+ # the modal layer, where it was an ordinary uninterpreted, world-relativized predicate
1489
+ # and so answered a different question than qml_is_valid. It is now rigid identity on
1490
+ # every one of those routes (`_RigidNames` in hol.thf_modal overrides the two entries,
1491
+ # and `_lower_identity` rewrites `≠` to `¬(=)` before any name is looked up), so the two
1492
+ # equality entries below are unreachable from to_thf_modal / to_isabelle_modal /
1493
+ # hol.thf_modal / hol.isabelle_modal. They are kept because this table also names `⊥`
1494
+ # and `⊤` and is shared with routes that do not lower identity; a route that reads one
1495
+ # of the two gets a valid, distinct functor rather than a crash — never a SILENT
1496
+ # uninterpreted reading, because reaching them at all now takes a caller that opted out
1497
+ # of `_RigidNames`.
1498
+ _THF_PRED_ALIAS = {"=": "feq", "≠": "fneq", "⊥": "bottom", "⊤": "top"}
1499
+
1500
+ # The THF export's own fixed functors. A user symbol that sanitises onto one of
1501
+ # these (a predicate literally named ``r`` or ``mbox``) must NOT claim it — it
1502
+ # would re-declare a built-in at a conflicting type, so the emitted problem would
1503
+ # be rejected by a strict THF parser (or, worse, silently change meaning).
1504
+ # :class:`_ThfNames` pre-claims them, pushing such symbols to ``r_2`` etc.
1505
+ _THF_RESERVED = frozenset({
1506
+ "mu", "r", "existsAt",
1507
+ "mnot", "mand", "mor", "mimplies", "mequiv", "mbox", "mdia",
1508
+ "mforall", "mexists", "mvalid",
1509
+ })
1510
+
1511
+
1512
+ def _thf_name(name: str) -> str:
1513
+ """Lower-case ASCII predicate/constant/function stem for a THF functor (NOT injective).
1514
+
1515
+ Non-ASCII characters are transliterated FIRST via
1516
+ :func:`~unicode_logic_kit.fol._fol_nodes.constant_name_to_ascii` (ASCII passthrough;
1517
+ Greek -> conventional name; anything else -> a reversible ``uXXXX`` escape) — THF
1518
+ functors are ASCII-only (TPTP ``lower_word``), so a raw Unicode letter reaching this
1519
+ far would be silent corruption, not sanitisation. A leading digit (which can also
1520
+ now arise from a transliterated escape, though those always start with the ASCII
1521
+ letter ``u``) is prefixed with ``p`` so the result is a legal lower identifier — TPTP
1522
+ functors can never start with a digit, and unlike the predicate/constant/function
1523
+ case this class had no digit guard before, since a THF functor name in this exporter
1524
+ is always built from a Constant/Function/Atom name, and only Constant names (via the
1525
+ kit's NAME terminal) can be digit-leading in the first place. A leading underscore
1526
+ gets the same ``p``: the constant ``'_sk0'``, the constant ``'-3'`` and the function
1527
+ ``+`` would otherwise be written ``_sk0``, ``_3`` and ``_``, none of which is a lower
1528
+ word (nor, upper-cased for a variable, an upper word).
1529
+
1530
+ Use :class:`_ThfNames` to get a per-formula *unique* functor — ``_thf_name`` alone
1531
+ can map distinct symbols (``Ab`` / ``ab``, or ``theta`` / the Greek ``θ``) to the
1532
+ same functor.
1533
+ """
1534
+ if name in _THF_PRED_ALIAS:
1535
+ return _THF_PRED_ALIAS[name]
1536
+ safe = "".join(c if (c.isalnum() or c == "_") else "_"
1537
+ for c in constant_name_to_ascii(name))
1538
+ if not safe:
1539
+ return "p"
1540
+ if safe[0].isdigit() or safe[0] == "_":
1541
+ safe = "p" + safe
1542
+ return safe[:1].lower() + safe[1:]
1543
+
1544
+
1545
+ class _ThfNames(SymbolNames):
1546
+ """Per-formula THF functor resolver (the shared :class:`SymbolNames` over
1547
+ ``_thf_name`` + the equality/inequality aliases), so distinct source symbols that
1548
+ sanitise alike — ``Ab`` / ``ab`` — or a predicate used at two arities get DISTINCT
1549
+ functors. Without it ``□Ab → □ab`` could collapse to the tautology ``□ab → □ab``.
1550
+ The export's own built-in functors (``reserved``, default :data:`_THF_RESERVED`)
1551
+ are pre-claimed so no user symbol can shadow them.
1552
+ """
1553
+
1554
+ def __init__(self, formula: Node, reserved=_THF_RESERVED):
1555
+ super().__init__(formula, _thf_name, _THF_PRED_ALIAS, reserved=reserved)
1556
+ self._var_map: Dict[str, str] = {}
1557
+ self._var_used: set = set()
1558
+
1559
+ def variable(self, name: str) -> str:
1560
+ """Unique upper-case THF variable token for object-variable ``name``.
1561
+
1562
+ THF variable tokens are the whole (transliterated) name upper-cased — the
1563
+ pre-existing convention this method preserves, including its one known
1564
+ simplification: two source names differing only by case (``x``/``X``) still
1565
+ fold onto the same token, since THF variables have no separate case-preserving
1566
+ slot the way predicate/function/constant identifiers do. What this method adds
1567
+ is (a) ASCII purity — a raw non-ASCII variable name would otherwise reach the
1568
+ THF text verbatim (just upper-cased), which is exactly the same "isalnum() lets
1569
+ Unicode letters through" gap :func:`_thf_name` had — and (b) de-collision via
1570
+ :func:`~unicode_logic_kit.fol._symbol_names.dedupe`, so two DISTINCT source names
1571
+ that only coincide AFTER transliteration (not the pre-existing case-fold above,
1572
+ which is unaffected by this fix) do not collapse into one bound variable.
1573
+ """
1574
+ if name in self._var_map:
1575
+ return self._var_map[name]
1576
+ cand = dedupe(_thf_name(name).upper(), self._var_used)
1577
+ self._var_map[name] = cand
1578
+ return cand
1579
+
1580
+
1581
+ def _thf_term(node: Node, names: "_ThfNames") -> str:
1582
+ """Render an individual term in THF (Variable → unique upper-case token, else a
1583
+ unique functor)."""
1584
+ if isinstance(node, Variable):
1585
+ return names.variable(node.name)
1586
+ from .nodes import Constant, Number, Function
1587
+ if isinstance(node, Constant):
1588
+ return names.constant(node.name)
1589
+ if isinstance(node, Number):
1590
+ return names.constant(prefixed_numeral_name(node.value))
1591
+ if isinstance(node, Function):
1592
+ head = names.function(node)
1593
+ return "( " + " @ ".join([head] + [_thf_term(a, names) for a in node.args]) + " )"
1594
+ raise NotImplementedError(f"to_thf_modal: unsupported term {type(node).__name__}.")
1595
+
1596
+
1597
+ def _thf_lift(node: Node, names: "_ThfNames") -> str:
1598
+ """Render a modal formula as a THF term of type ``mu > $o``."""
1599
+ if isinstance(node, Down):
1600
+ # See the identical, more fully-explained guard in _st above — this
1601
+ # basic THF export never covered hybrid logic either; hol.thf_modal
1602
+ # (the full shallow embedding) gets its OWN ↓ guard, see that module.
1603
+ raise NotImplementedError(
1604
+ "to_thf_modal: the ↓ binder is outside the alethic □/◇ fragment "
1605
+ "supported by this THF export (which does not cover hybrid logic "
1606
+ "at all — no nominals/@ either); use "
1607
+ "unicode_logic_kit.fol.modal_translation.down_is_valid or "
1608
+ "unicode_logic_kit.atp.kripke_enum.KripkeEnumBackend instead.")
1609
+ if isinstance(node, Atom):
1610
+ # An identity atom never reaches here with its own predicate name: the
1611
+ # callers lower `≠` to `¬(=)` and hand `names` a `_RigidNames`, whose
1612
+ # `atom` maps `=` onto the `meq` macro — THF's own `=` over `$i` with the
1613
+ # world dropped, the reading qml_is_valid has ("Equality is rigid").
1614
+ constant = truth_value(node)
1615
+ if constant is not None:
1616
+ # `$true` / `$false`: the proposition true (false) at every world.
1617
+ return "( ^ [W: mu] : $true )" if constant else "( ^ [W: mu] : $false )"
1618
+ head = names.atom(node)
1619
+ if not node.args:
1620
+ return head
1621
+ return "( " + " @ ".join([head] + [_thf_term(a, names) for a in node.args]) + " )"
1622
+ if isinstance(node, Not):
1623
+ return f"( mnot @ {_thf_lift(node.formula, names)} )"
1624
+ if isinstance(node, And):
1625
+ return f"( mand @ {_thf_lift(node.left, names)} @ {_thf_lift(node.right, names)} )"
1626
+ if isinstance(node, Or):
1627
+ return f"( mor @ {_thf_lift(node.left, names)} @ {_thf_lift(node.right, names)} )"
1628
+ if isinstance(node, Implies):
1629
+ return f"( mimplies @ {_thf_lift(node.left, names)} @ {_thf_lift(node.right, names)} )"
1630
+ if isinstance(node, Iff):
1631
+ return f"( mequiv @ {_thf_lift(node.left, names)} @ {_thf_lift(node.right, names)} )"
1632
+ if isinstance(node, Box):
1633
+ return f"( mbox @ {_thf_lift(node.formula, names)} )"
1634
+ if isinstance(node, Diamond):
1635
+ return f"( mdia @ {_thf_lift(node.formula, names)} )"
1636
+ if isinstance(node, Quantifier):
1637
+ x = names.variable(node.variable.name)
1638
+ binder = "mforall" if node.type in (_FORALL, "forall") else "mexists"
1639
+ return f"( {binder} @ ( ^ [{x}: $i] : {_thf_lift(node.formula, names)} ) )"
1640
+ raise NotImplementedError(
1641
+ f"to_thf_modal: {type(node).__name__} is outside the alethic □/◇ fragment "
1642
+ "supported by this THF export — use "
1643
+ "unicode_logic_kit.hol.thf_modal.to_thf_modal_full, which covers the full "
1644
+ "modal family (epistemic/doxastic/assertive/bouletic/deontic/temporal "
1645
+ "incl. Until/Since, and hybrid nominals/@).")
1646
+
1647
+
1648
+ def _thf_signature(formula: Node, names: Optional["_ThfNames"] = None) -> List[str]:
1649
+ """Type declarations for every predicate / constant / function in ``formula``.
1650
+
1651
+ Uses the de-colliding :class:`_ThfNames` resolver (built from ``formula`` if not
1652
+ supplied) so each distinct symbol gets a unique functor and a unique declaration.
1653
+ """
1654
+ if names is None:
1655
+ names = _ThfNames(formula)
1656
+ decls = []
1657
+ for (name, arity), functor in sorted(names.pred.items(), key=lambda kv: kv[1]):
1658
+ typ = " > ".join(["$i"] * arity + ["mu > $o"]) if arity else "mu > $o"
1659
+ decls.append(f"thf({functor}_decl, type, ( {functor} : ( {typ} ) )).")
1660
+ for name, functor in sorted(names.const.items(), key=lambda kv: kv[1]):
1661
+ decls.append(f"thf({functor}_decl, type, ( {functor} : $i )).")
1662
+ for (name, arity), functor in sorted(names.func.items(), key=lambda kv: kv[1]):
1663
+ typ = " > ".join(["$i"] * (arity + 1))
1664
+ decls.append(f"thf({functor}_decl, type, ( {functor} : ( {typ} ) )).")
1665
+ return decls
1666
+
1667
+
1668
+ def to_thf_modal(formula: Node, mode: str = "constant", frame: str = "K") -> str:
1669
+ """Emit a Benzmüller-style TPTP **THF** shallow embedding of ``formula``.
1670
+
1671
+ Produces a complete, self-contained THF problem — type declarations, the lifted
1672
+ modal operators, the frame axioms for ``frame`` (K/T/S4/S5/KD/KD45), the
1673
+ ``existsAt`` domain axioms for ``mode`` (constant / increasing / decreasing /
1674
+ varying), and the conjecture ``mvalid @ ⟨formula⟩`` — ready for a higher-order
1675
+ ATP (Leo-III, Satallax). Covers the alethic □/◇ fragment.
1676
+
1677
+ Equality ``=`` / ``≠`` is **rigid identity**, the reading of :func:`qml_is_valid` /
1678
+ :func:`qml_translate` (module docstring, "Equality is rigid"): THF's own ``=`` over
1679
+ the individual sort ``$i`` with no world argument, through one extra macro
1680
+ ``meq = ^ [A: $i, B: $i, W: mu] : ( A = B )`` that is emitted only when the formula
1681
+ contains identity (so an equality-free problem is unchanged). ``t₁ ≠ t₂`` is lowered
1682
+ to ``¬(t₁ = t₂)`` as ``qml_translate`` does, a non-binary ``=`` / ``≠`` atom raises
1683
+ ``ValueError`` as there, and no ``feq`` / ``fneq`` functor is declared. So a prover
1684
+ on this problem answers the question :func:`qml_is_valid` answers: ``∀x (x = x)``,
1685
+ ``a = b → □(a = b)`` and ``◇(a = b) → a = b`` are theorems under ``frame='K'``,
1686
+ ``□(a = b) → a = b`` only under a reflexive / serial frame. Identity is not
1687
+ existence-guarded (the module docstring's varying-domain choice). The macro and the
1688
+ lowering live in :mod:`unicode_logic_kit.hol.thf_modal`, which emits the same line.
1689
+
1690
+ THF has no free variable, so a variable that is free in ``formula`` (a parameter, see
1691
+ the module docstring's "A free variable is a parameter") is bound in the conjecture:
1692
+ ``! [Y: $i] : mvalid @ …``, and under a varying regime ``existsAt @ Y`` guards the
1693
+ formula (``! [Y: $i] : mvalid @ (mimplies @ (existsAt @ Y) @ …)``), which is the
1694
+ reading :func:`qml_is_valid` has. For a single formula with no premise this binding
1695
+ is the parameter reading itself.
1696
+ """
1697
+ # Lazy: hol.thf_modal imports this module, so the shared rigid-identity helpers
1698
+ # are reached from inside the function (as to_isabelle_modal below reaches hol).
1699
+ from ..hol.thf_modal import (
1700
+ _RigidNames, _THF_RIGID_EQ_DEF, _has_identity, _lower_identity,
1701
+ )
1702
+ if frame not in _FRAMES:
1703
+ raise ValueError(f"to_thf_modal: unknown frame {frame!r}.")
1704
+ # A numeral is a constant identified by its value (1 and 1.0 are one), named ``n1``:
1705
+ # a user constant spelled like it is refused, not merged with it.
1706
+ [formula], _ = numerals_as_constants([formula], where="to_thf_modal",
1707
+ spell=prefixed_numeral_name)
1708
+ formula = _lower_identity(formula, "to_thf_modal")
1709
+ lines = [
1710
+ f"% Shallow embedding of a quantified modal formula (mode={mode}, frame={frame}).",
1711
+ "% Conjecture is 'Theorem' iff the formula is QML-valid under this regime.",
1712
+ "thf(mu_type, type, ( mu : $tType )).",
1713
+ "thf(r_decl, type, ( r : ( mu > mu > $o ) )).",
1714
+ "thf(existsAt_decl, type, ( existsAt : ( $i > mu > $o ) )).",
1715
+ ]
1716
+ names = _RigidNames(formula)
1717
+ lines += _thf_signature(formula, names)
1718
+ lines.append(_THF_DEFS)
1719
+ if _has_identity(formula):
1720
+ lines.append(_THF_RIGID_EQ_DEF)
1721
+ lines.append("thf(nonempty_dom, axiom, ( ! [W: mu] : ? [X: $i] : ( existsAt @ X @ W ) )).")
1722
+ for cond in _FRAMES[frame]:
1723
+ lines.append(_THF_FRAME[cond])
1724
+ if mode in _THF_DOMAIN:
1725
+ lines.append(_THF_DOMAIN[mode])
1726
+ elif mode not in ("varying",) and mode not in _CONSTANT_MODES:
1727
+ raise ValueError(f"to_thf_modal: unknown mode {mode!r}.")
1728
+ goal = _thf_lift(formula, names)
1729
+ parameters = free_parameter_names([formula])
1730
+ if mode in _ACTUALIST_MODES:
1731
+ for name in reversed(parameters):
1732
+ goal = f"( mimplies @ ( existsAt @ {names.variable(name)} ) @ {goal} )"
1733
+ conjecture = f"mvalid @ {goal}"
1734
+ for name in reversed(parameters):
1735
+ conjecture = f"! [{names.variable(name)}: $i] : ( {conjecture} )"
1736
+ lines.append(f"thf(goal, conjecture, ( {conjecture} )).")
1737
+ return "\n".join(lines) + "\n"
1738
+
1739
+
1740
+ def to_isabelle_modal(formula: Node, mode: str = "constant", frame: str = "K") -> str:
1741
+ """Emit a complete, loadable Isabelle/HOL theory shallow-embedding ``formula``.
1742
+
1743
+ This delegates to :func:`unicode_logic_kit.hol.isabelle_modal.to_isabelle_modal` — the
1744
+ real, full-modal-family exporter that emits a loadable ``theory … begin … end`` with
1745
+ every lifted operator defined and a genuine ``lemma`` (it replaced the earlier
1746
+ alethic-only skeleton). Use that module directly for the additional options
1747
+ (epistemic/doxastic/deontic/temporal coverage, the proof ``tactic``,
1748
+ ``temporal_closure``).
1749
+
1750
+ A variable that is free in ``formula`` stays a free variable of the lemma, which then
1751
+ holds for EVERY individual of the type ``i``. Under ``constant`` / ``possibilist`` that
1752
+ is the parameter reading of :func:`qml_is_valid` (one formula, no premise). Under a
1753
+ varying regime it is stronger than that reading, which asks the individual to exist
1754
+ at the world of evaluation only: a proof of the lemma proves the parameter instance,
1755
+ and a counterexample to the lemma in which the individual does not exist at the world
1756
+ of evaluation does not refute it. Use :func:`qml_is_valid` for the verdict under such
1757
+ a regime.
1758
+ """
1759
+ from ..hol.isabelle_modal import to_isabelle_modal as _real
1760
+ return _real(formula, mode=mode, frame=frame)