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,712 @@
1
+ r"""DOL (Distributed Ontology, Modelling and Specification Language) library
2
+ emission over CASL specs — a thin structuring layer built entirely on top of
3
+ :mod:`unicode_logic_kit.fol.casl_export`, never reimplementing it.
4
+
5
+ `DOL <https://ontohub.org/dol>`_ is CoFI/OntoHub's heterogeneous layer above
6
+ CASL (and OWL, and every other logic Hets understands): a ``library``
7
+ groups several named ``spec`` blocks and lets one spec build on another via
8
+ structuring (``spec B = A then ...``). :mod:`unicode_logic_kit.fol.casl_export`
9
+ already renders one classical/many-sorted FOL formula batch as one
10
+ self-contained ``spec ... end`` block; this module's only job is to emit
11
+ SEVERAL such blocks inside one ``library``/``logic CASL`` wrapper, with an
12
+ optional ``then``-extension between them. It does not add a single new
13
+ formula-rendering rule of its own — every axiom/conjecture line in the
14
+ output comes verbatim from :func:`~unicode_logic_kit.fol.casl_export.to_casl_spec`.
15
+
16
+ Scope: emission only — no ``parse_dol_library``
17
+ ------------------------------------------------
18
+ This module is one-way, like :mod:`~unicode_logic_kit.fol.casl_export` before
19
+ it added an importer counterpart
20
+ (:mod:`unicode_logic_kit.fol.casl_import`) — going the other way (parsing a
21
+ full DOL library text, with its ``library``/``logic``/multi-``spec``/
22
+ ``then`` structuring, back into per-spec ASTs) is a materially bigger
23
+ grammar than :func:`~unicode_logic_kit.fol.casl_import.parse_casl_spec`
24
+ already refuses to attempt for a SINGLE spec (see that module's own "Scope"
25
+ section — ``then``/``view``/structuring constructs are explicitly out of
26
+ its fragment). A ``parse_dol_library`` is future scope, not started here;
27
+ :func:`unicode_logic_kit.fol.casl_import.parse_casl_spec` remains usable
28
+ directly on any ONE ``spec ... end`` block sliced out of this module's own
29
+ output (see ``tests/test_dol.py``'s offline consistency check, which does
30
+ exactly that as an export/import round-trip sanity test).
31
+
32
+ Reuse strategy: cut ``to_casl_spec``'s own output, do not re-derive it
33
+ ------------------------------------------------------------------------
34
+ :func:`to_dol_library` calls the PUBLIC
35
+ :func:`~unicode_logic_kit.fol.casl_export.to_casl_spec` once per
36
+ :class:`DolSpec` — never touching that module's private internals — and
37
+ then deterministically slices the BODY (the ``sorts``/``ops``/``preds``/
38
+ axiom lines) out of its result: ``to_casl_spec``'s own documented output
39
+ shape is always exactly one header line (``spec <NAME> =``), the body, and
40
+ one trailing ``end`` line (see that function's docstring), so
41
+ ``body_text.split("\n")[1:-1]`` is exactly the body, verbatim, no matter how
42
+ many sorts/ops/preds/axiom lines it has. This module then re-wraps that same
43
+ body under EITHER the same plain header (no ``extends``) or a
44
+ ``spec <name> = <extends> then`` header (with ``extends``) — see
45
+ :func:`_spec_block` for the one place this slice-and-rewrap happens, guarded
46
+ by an assertion that the expected header/footer shape actually held (so a
47
+ future change to ``to_casl_spec``'s own output shape fails LOUDLY here
48
+ rather than silently emitting a malformed library).
49
+
50
+ ``extends`` and signature re-declaration — a deliberate, harmless
51
+ consequence of per-spec-independent inference
52
+ --------------------------------------------------------------------------
53
+ ``to_casl_spec`` infers its ``sorts``/``ops``/``preds`` declarations from
54
+ ONLY the formulas passed to that one call — it has no notion of "these
55
+ symbols are already declared by the spec I ``then``-extend". So if spec
56
+ ``B`` (``DolSpec(extends="A", ...)``) reuses a predicate or constant that
57
+ spec ``A`` already declared, ``B``'s own emitted block RE-DECLARES it too.
58
+ This is valid, unremarkable CASL: re-declaring an already-visible symbol
59
+ with an IDENTICAL profile (same arity/argument sorts/result sort) under
60
+ ``then`` is a no-op in CASL's static semantics, not a conflict — Hets
61
+ accepts it. It only becomes a genuine problem if the SAME symbol name ends
62
+ up with two DIFFERENT profiles across ``A`` and ``B`` (e.g. different
63
+ ``default_sort`` values producing different inferred sorts for the same
64
+ predicate) — that is a real modelling error on the caller's part, and
65
+ surfaces exactly the way any other CASL redeclaration conflict would (a
66
+ Hets-side parse/static-analysis error), not something this module tries to
67
+ pre-empt. Callers who want a shared vocabulary should keep every
68
+ :class:`DolSpec` in the same library on the same ``default_sort``.
69
+
70
+ A spec that extends another SEES the other's symbols without declaring them, and a bound
71
+ variable that is spelled like a visible symbol would be read as that variable. So a
72
+ bound variable of the extending spec is never given the spelling of a symbol of the
73
+ specs it extends (found through ``specs``, transitively): the extending spec's
74
+ ``to_casl_spec`` call is given those names as ``visible_symbols`` and renames the
75
+ clashing binders, as it does for the spec's own symbols.
76
+
77
+ What is NOT re-checked here
78
+ ------------------------------
79
+ * Referential integrity of ``extends``: this module does not verify that a
80
+ ``DolSpec.extends`` name is itself a key of ``specs`` (or defined
81
+ anywhere at all) — a library legitimately may extend a spec defined in
82
+ ANOTHER library/import this module knows nothing about. That is the
83
+ caller's responsibility; a dangling reference surfaces as a Hets-side
84
+ error when the library is actually uploaded, not here.
85
+ * Formula validity: every :func:`~unicode_logic_kit.fol.casl_export.to_casl_spec`
86
+ refusal (an out-of-fragment node, a free variable, a sort conflict, a
87
+ reserved-word identifier, an empty axiom+conjecture batch, …) propagates
88
+ through :func:`to_dol_library` UNCHANGED — this module adds no new
89
+ formula-level validation of its own, by design (single source of truth).
90
+
91
+ Name validation
92
+ ----------------
93
+ The library name and every ``DolSpec.extends`` target name are checked
94
+ against the same simple-CASL-identifier pattern and reserved-keyword list
95
+ :mod:`~unicode_logic_kit.fol.casl_export` checks ``spec_name`` against (a
96
+ small, independent literal copy of that list — the same "duplicate by
97
+ content, not by importing a private name" reasoning
98
+ :mod:`unicode_logic_kit.fol.signature` and
99
+ :mod:`unicode_logic_kit.fol.casl_import` give for their own copies). Each
100
+ :class:`DolSpec`'s own key in ``specs`` is NOT separately checked here — it
101
+ is passed straight through as ``spec_name`` to
102
+ :func:`~unicode_logic_kit.fol.casl_export.to_casl_spec`, which already
103
+ performs that exact check (:func:`~unicode_logic_kit.fol.casl_export._validate_spec_name`)
104
+ and raises ``ValueError`` itself; duplicating it here would just be a second
105
+ copy of the same regex answering the same question.
106
+ """
107
+
108
+ from collections import OrderedDict
109
+ from dataclasses import dataclass, field
110
+ from typing import Optional, Sequence
111
+ import re
112
+
113
+ from ..fol.casl_export import to_casl_spec
114
+ from ..fol.nodes import Atom, Constant, Function, Node, SortedConstant, Variable
115
+ from ..fol._symbol_names import dedupe
116
+ from ..fol.qml import qml_validity_formula
117
+
118
+ __all__ = [
119
+ "to_dol_library", "DolSpec",
120
+ "to_dol_library_from_modal", "sanitize_modal_identifiers",
121
+ ]
122
+
123
+
124
+ # Mirrors unicode_logic_kit.fol.casl_export._CASL_KEYWORDS /
125
+ # unicode_logic_kit.fol.casl_import._CASL_KEYWORDS — a small, independent
126
+ # literal copy (see the module docstring's "Name validation" section for why
127
+ # this is not an import of another module's private name).
128
+ _CASL_KEYWORDS = frozenset({
129
+ "and", "arch", "as", "assoc", "axiom", "axioms", "closed", "comm", "def",
130
+ "else", "end", "exists", "false", "fit", "forall", "free", "from",
131
+ "generated", "get", "given", "hide", "idem", "if", "in", "lambda",
132
+ "library", "local", "logic", "not", "op", "ops", "pred", "preds",
133
+ "result", "reveal", "sort", "sorts", "spec", "then", "to", "true",
134
+ "type", "types", "unit", "units", "var", "vars", "version", "view",
135
+ "when", "with", "within",
136
+ })
137
+
138
+ _SIMPLE_ID_RE = re.compile(r"[A-Za-z][A-Za-z0-9_]*")
139
+
140
+
141
+ def _check_casl_word(name: str, kind: str) -> None:
142
+ """Refuse ``name`` (a ``kind`` identifier) if it is not a simple CASL
143
+ word, or collides with a CASL keyword — see the module docstring's
144
+ "Name validation" section."""
145
+ if not _SIMPLE_ID_RE.fullmatch(name):
146
+ raise ValueError(
147
+ f"hets.dol: {kind} {name!r} is not a simple CASL identifier "
148
+ "matching [A-Za-z][A-Za-z0-9_]*."
149
+ )
150
+ if name in _CASL_KEYWORDS:
151
+ raise ValueError(
152
+ f"hets.dol: {kind} name '{name}' collides with the reserved "
153
+ f"CASL keyword '{name}'."
154
+ )
155
+
156
+
157
+ @dataclass
158
+ class DolSpec:
159
+ """One ``spec`` block's worth of input to :func:`to_dol_library`.
160
+
161
+ Args:
162
+ axioms: the spec's own axioms (rendered as ``. <formula>`` lines,
163
+ exactly as :func:`~unicode_logic_kit.fol.casl_export.to_casl_spec`
164
+ would for a standalone spec).
165
+ conjectures: the spec's own ``%implied`` goals. Defaults to empty —
166
+ a spec that only extends another and adds axioms (no new goal)
167
+ is common.
168
+ extends: the name of another spec this one structurally extends via
169
+ CASL's ``then`` — renders the header as
170
+ ``spec <this_name> = <extends> then`` instead of plain
171
+ ``spec <this_name> =``. ``None`` (the default) renders a plain,
172
+ unstructured spec. Not checked for referential integrity against
173
+ the other keys of the ``specs`` mapping passed to
174
+ :func:`to_dol_library` — see that function's module-level
175
+ docstring section "What is NOT re-checked here".
176
+ default_sort: passed straight through to this spec's own
177
+ :func:`~unicode_logic_kit.fol.casl_export.to_casl_spec` call —
178
+ see that function's docstring for what it controls (the sort a
179
+ plain, unsorted quantifier is declared at).
180
+ """
181
+
182
+ axioms: Sequence[Node]
183
+ conjectures: Sequence[Node] = field(default_factory=tuple)
184
+ extends: Optional[str] = None
185
+ default_sort: str = "Thing"
186
+
187
+
188
+ def _names_of(spec: DolSpec) -> set:
189
+ """Every symbol name of ``spec``'s own formulas: constants, functions, predicates, the
190
+ sorts they write, and the spec's default sort (an over-approximation is harmless: the
191
+ names only keep a bound variable away)."""
192
+ names = {spec.default_sort}
193
+ for formula in (*spec.axioms, *spec.conjectures):
194
+ for node in formula.walk():
195
+ if isinstance(node, (Constant, SortedConstant, Function)):
196
+ names.add(node.name)
197
+ elif isinstance(node, Atom):
198
+ names.add(node.predicate)
199
+ sort = getattr(node, "sort", None)
200
+ if isinstance(sort, str):
201
+ names.add(sort)
202
+ return names
203
+
204
+
205
+ def _inherited_names(spec: DolSpec, specs: "OrderedDict[str, DolSpec]") -> frozenset:
206
+ """The symbol names ``spec`` sees from the specs it extends (``then``), transitively.
207
+
208
+ An extended spec that is not a key of ``specs`` is defined elsewhere and cannot be
209
+ read here (see the module docstring's "What is NOT re-checked here")."""
210
+ names: set = set()
211
+ seen: set = set()
212
+ parent = spec.extends
213
+ while parent is not None and parent in specs and parent not in seen:
214
+ seen.add(parent)
215
+ names |= _names_of(specs[parent])
216
+ parent = specs[parent].extends
217
+ return frozenset(names)
218
+
219
+
220
+ def _spec_block(spec_name: str, spec: DolSpec, inherited: frozenset = frozenset()) -> str:
221
+ """Render one ``spec <spec_name> = [<extends> then] ... end`` block by
222
+ slicing the body out of a fresh :func:`~unicode_logic_kit.fol.casl_export.to_casl_spec`
223
+ call — see the module docstring's "Reuse strategy" section. ``inherited`` are the
224
+ symbol names the spec sees from the specs it extends: a bound variable is never
225
+ given the spelling of one of them (``to_casl_spec``'s ``visible_symbols``)."""
226
+ body_text = to_casl_spec(
227
+ list(spec.axioms), conjectures=list(spec.conjectures),
228
+ spec_name=spec_name, default_sort=spec.default_sort,
229
+ visible_symbols=inherited,
230
+ )
231
+ body_lines = body_text.split("\n")
232
+ expected_header = f"spec {spec_name} ="
233
+ if len(body_lines) < 2 or body_lines[0] != expected_header or body_lines[-1] != "end":
234
+ # Defensive: guards the slicing assumption above against a future
235
+ # (unanticipated) change to to_casl_spec's output shape — fail
236
+ # loudly here rather than silently emit a malformed library.
237
+ raise AssertionError(
238
+ "hets.dol: to_casl_spec's output shape did not match the "
239
+ f"expected 'spec {spec_name} = ... end' envelope this module's "
240
+ f"body-slicing logic assumes; got first line {body_lines[0]!r} "
241
+ f"and last line {body_lines[-1]!r}."
242
+ )
243
+ body = body_lines[1:-1]
244
+
245
+ if spec.extends is not None:
246
+ _check_casl_word(spec.extends, "extends target name")
247
+ header = f"spec {spec_name} = {spec.extends} then"
248
+ else:
249
+ header = expected_header
250
+
251
+ return "\n".join([header, *body, "end"])
252
+
253
+
254
+ def to_dol_library(name: str, specs: "OrderedDict[str, DolSpec]") -> str:
255
+ """Render ``specs`` as one DOL library: ``library <name>`` / ``logic
256
+ CASL`` / one ``spec … end`` block per entry, in ``specs`` iteration
257
+ order.
258
+
259
+ Args:
260
+ name: the library's own name — checked against the same simple-
261
+ CASL-identifier + reserved-keyword rule as every other name
262
+ this module or :mod:`~unicode_logic_kit.fol.casl_export` emits.
263
+ specs: an ordered mapping from spec name to :class:`DolSpec`. Order
264
+ matters for the emitted text (specs are written out in
265
+ iteration order) and, practically, for validity: a
266
+ ``DolSpec.extends`` reference should generally name a spec that
267
+ appears EARLIER in this mapping (or is defined elsewhere), since
268
+ CASL structuring is not mutually recursive — this module does
269
+ not enforce that ordering itself (see the module docstring's
270
+ "What is NOT re-checked here").
271
+
272
+ Returns:
273
+ The complete library text (no trailing newline, matching
274
+ :func:`~unicode_logic_kit.fol.casl_export.to_casl_spec`'s own
275
+ convention), ready to upload as a ``.dol`` file via
276
+ :meth:`unicode_logic_kit.hets.HetsClient.upload`.
277
+
278
+ Raises:
279
+ ValueError: ``specs`` is empty; ``name`` or any
280
+ ``DolSpec.extends`` value is not a simple CASL identifier or
281
+ collides with a CASL keyword; or (propagated, unchanged) any
282
+ :func:`~unicode_logic_kit.fol.casl_export.to_casl_spec` refusal
283
+ for one of the individual specs (an out-of-fragment node, a
284
+ free variable, a sort conflict, a bad ``spec_name``, an empty
285
+ axiom+conjecture batch, …).
286
+ NotImplementedError: propagated, unchanged, from
287
+ :func:`~unicode_logic_kit.fol.casl_export.to_casl_spec` (a
288
+ non-classical node in one of the specs' formulas).
289
+ """
290
+ _check_casl_word(name, "library name")
291
+ if not specs:
292
+ raise ValueError(
293
+ "hets.dol: to_dol_library needs at least one spec in 'specs'; "
294
+ "an empty library has nothing to structure."
295
+ )
296
+
297
+ lines = [f"library {name}", "logic CASL", ""]
298
+ items = list(specs.items())
299
+ for idx, (spec_name, spec) in enumerate(items):
300
+ lines.append(_spec_block(spec_name, spec, _inherited_names(spec, specs)))
301
+ if idx != len(items) - 1:
302
+ lines.append("")
303
+ return "\n".join(lines)
304
+
305
+
306
+ # =============================================================================
307
+ # Quantified modal logic -> CASL/DOL, over fol.qml's standard translation
308
+ # =============================================================================
309
+ #
310
+ # fol.qml.qml_validity_formula already lowers a modal formula to exactly the
311
+ # classical FOL fragment fol.casl_export accepts (see that function's own
312
+ # docstring) — this section's job is the ONE concrete gap between
313
+ # "qml_validity_formula's output" and "a formula to_casl_spec will actually
314
+ # accept", point (1); point (2) records the second place one might expect a
315
+ # gap, object identity, and why there is none:
316
+ #
317
+ # (1) Illegal identifiers. qml's own auto-generated fresh variables (the
318
+ # world variables _Fresh mints, e.g. "_w0", and the Geach axiom's own
319
+ # "_gz0"/"_gw"/"_gu"/"_gv"/"_gt", and the argument variables
320
+ # _signature_typing_facts mints for a function's typing fact, "_a0") start
321
+ # with an underscore, which is not a legal CASL SIMPLE-ID
322
+ # ([A-Za-z][A-Za-z0-9_]*) — casl_export's own honesty convention is to
323
+ # REFUSE a bad identifier rather than silently rename it (see its "Reserved
324
+ # words and identifier hygiene" docstring section), which is exactly right
325
+ # for a name the CALLER chose, but qml's fresh names are an internal
326
+ # implementation detail of the translation, not something a caller of this
327
+ # bridge ever typed — so THIS module fixes them before they ever reach
328
+ # casl_export, rather than asking every caller of qml_validity_formula to
329
+ # pre-empt a naming convention that is qml's own.
330
+ #
331
+ # (2) Object identity needs no help: it arrives binary and CASL-native.
332
+ # fol.qml's ``_st`` translates an identity atom ``a = b`` to the SAME binary
333
+ # ``a = b`` over the object terms, with NO world argument, and ``a ≠ b`` to
334
+ # ``¬(a = b)`` (see that module's "Equality is rigid" section). Identity does
335
+ # not vary by world, and the translation says so by not mentioning the world.
336
+ # That binary ``=`` is exactly CASL's own built-in equality — always two
337
+ # terms, rigid, never declared in ``preds`` — so fol.casl_export renders it
338
+ # infix (``a = b``), fol.casl_import parses it back to the identical Atom, and
339
+ # nothing in this module renames, aliases or special-cases it. The one other
340
+ # ``=`` atom a qml query can contain, the world identity ``w = v`` in qml's
341
+ # own temporal ``first_step`` axiom, is the same native symbol: it relates two
342
+ # worlds where a user equality relates objects, and what keeps the two kinds
343
+ # apart is the ``World``/``Object`` guards, not the symbol. ``≠`` never reaches
344
+ # this module at all, because qml has already written it as a negation of
345
+ # ``=``, which CASL renders like any other ``not``.
346
+ #
347
+ # So :func:`_rewrite_casl_names` leaves every ``=`` atom under its literal
348
+ # name and refuses every ``≠`` atom. Both matter only for a Node that did NOT
349
+ # come out of qml_validity_formula, i.e. one built by hand and handed to
350
+ # :func:`sanitize_modal_identifiers` directly (no text front-end of the kit
351
+ # parses to a non-binary ``=``/``≠``, and qml_validity_formula itself refuses
352
+ # one with a ValueError):
353
+ #
354
+ # * a ``=`` atom that is not binary is not CASL's identity and is not
355
+ # renamed onto some uninterpreted predicate either: it is passed through
356
+ # unchanged, so fol.casl_export's own exactly-two-terms check refuses it by
357
+ # name (one refusal, one place);
358
+ # * a ``≠`` atom of ANY arity is refused here with :class:`NotImplementedError`:
359
+ # CASL has no disequality, and the sanitiser renames identifiers, it does not
360
+ # rewrite connectives, so the caller writes ``Not(Atom("=", ...))``.
361
+ #
362
+ # History, because the symbols below used to exist: fol.qml once appended the
363
+ # world to ``=`` like to any atom, which made it a ternary uninterpreted
364
+ # predicate, and this module aliased it to ``weq``/``wneq``. Both are gone
365
+ # together; an atom literally named ``weq`` is now an ordinary user predicate
366
+ # like any other.
367
+ #
368
+ # No change to fol.qml's or fol.casl_export's own logic: qml_validity_formula
369
+ # is called unmodified, casl_export's own exactly-2-ary "=" check is never
370
+ # touched, and the sanitised Node is handed to to_casl_spec via
371
+ # to_dol_library exactly as any other DolSpec's axioms/conjectures would be.
372
+
373
+
374
+ def _casl_sanitize_stem(name: str) -> str:
375
+ """A best-effort CASL-legal *candidate* for ``name`` — not yet guaranteed
376
+ unique (see :class:`_CaslIdentifierShim`, which wraps this in
377
+ :func:`~unicode_logic_kit.fol._symbol_names.dedupe` against every other name
378
+ already claimed in the same namespace, so injectivity is enforced exactly
379
+ once, in one place).
380
+
381
+ Strips every LEADING underscore first — the shape this shim was written for
382
+ (see the section comment above): the fresh names
383
+ :mod:`unicode_logic_kit.fol.qml` used to generate were ``_w0`` / ``_gz0`` /
384
+ ``_a0``, each an ordinary, already-legal identifier once that convention is
385
+ peeled off. Since 0.30.0 that module mints ``w0`` / ``v0`` style names
386
+ instead (they have to be legal for the KIT's own parser too), so no CALLER
387
+ in this repository still hands this function such a name — the rule stays
388
+ for any other one that might. What is left over is then re-checked against
389
+ CASL's own SIMPLE-ID grammar, and this is the part that is still reached:
390
+ a candidate that does not start with an ASCII letter (all-underscore, or
391
+ digit-leading) is prefixed with ``v``, and any remaining illegal character
392
+ is folded to ``_`` — which is what happens to ``fol.qml``'s ``·`` mark on a
393
+ user predicate named like one of its own relations (``R`` -> ``R·`` ->
394
+ ``R_``), the one shape of illegal CASL identifier this route still emits.
395
+ """
396
+ candidate = name.lstrip("_") or "id"
397
+ if not (candidate[0].isascii() and candidate[0].isalpha()):
398
+ candidate = "v" + candidate
399
+ return "".join(
400
+ ch if (ch.isascii() and (ch.isalnum() or ch == "_")) else "_"
401
+ for ch in candidate)
402
+
403
+
404
+ class _CaslIdentifierShim:
405
+ """Per-formula, INJECTIVE renamer: maps every Variable / (Sorted)Constant-
406
+ or-Function / Atom-predicate name onto a legal, unique CASL identifier,
407
+ leaving every name that already qualifies untouched.
408
+
409
+ Three separate TABLES, one per kind of symbol: ``variable`` (bound
410
+ object/world variables), ``term`` (constants AND functions share ONE
411
+ table here, mirroring
412
+ :func:`~unicode_logic_kit.fol.casl_export.to_casl_spec`'s own merged
413
+ ``ops`` declaration block and its ``_CONST_VS_FUNCTION`` dual-use
414
+ refusal), and ``predicate`` (Atom predicate names). A name is mapped only
415
+ within its own table, so a variable and a constant that already are legal
416
+ keep their spelling even when it is the same one; the text then says the
417
+ two apart by renaming the binder (see
418
+ :func:`~unicode_logic_kit.fol.casl_export.to_casl_spec`). A name the shim
419
+ MINTS, on the other hand, is drawn from ONE pool of taken spellings shared
420
+ by the three tables and the sorts of the formula, because CASL writes all
421
+ of them as one identifier: a renamed variable is never given the spelling
422
+ of a constant, a function, a predicate or a sort. ``=`` is not in any
423
+ of them: it is CASL's own native, rigid equality and not a declared
424
+ symbol, so :func:`_rewrite_casl_names` keeps it under its literal name
425
+ and it is never a simple identifier for :meth:`_seed` to claim; ``≠`` is
426
+ refused there before it could be renamed. See the section comment above,
427
+ point (2).
428
+
429
+ TWO PASSES per namespace, in that order, is what makes this injective
430
+ regardless of tree-walk order: :meth:`__init__` first walks the WHOLE
431
+ formula and seeds every name that is ALREADY a legal CASL identifier —
432
+ self-mapped, claimed in that namespace's ``used`` set — before any
433
+ illegal name is resolved. Only then does :meth:`_resolve` compute a
434
+ sanitised candidate for an illegal name and hand it to
435
+ :func:`~unicode_logic_kit.fol._symbol_names.dedupe`, which appends a
436
+ numeric suffix on any collision — including a collision with an
437
+ ALREADY-legal name from the seeding pass. This order is load-bearing:
438
+ the fresh variable ``_w0`` sanitises to the natural candidate ``w0`` via
439
+ :func:`_casl_sanitize_stem`, and if the SAME formula also happens to
440
+ bind a genuine, unrelated object variable literally named ``w0`` (an
441
+ already-legal name — this is the exact collision batch note (3) asks to
442
+ be tested), seeding it FIRST means ``_w0``'s candidate is found already
443
+ taken and gets bumped to ``w0_2`` — two DISTINCT source names, two
444
+ DISTINCT output names — rather than the two silently merging into one
445
+ bound variable (a single-pass, seed-as-you-go walk could not guarantee
446
+ this, since it would depend on which of the two names the tree happens
447
+ to visit first).
448
+
449
+ Deliberately narrow: an already-legal name that happens to collide with
450
+ a CASL KEYWORD (e.g. a user constant literally named ``type``) is left
451
+ untouched here and surfaces exactly as it always did — a loud
452
+ :class:`ValueError` from :func:`~unicode_logic_kit.fol.casl_export.to_casl_spec`
453
+ itself — because that is a naming choice the CALLER made, not qml's own
454
+ fresh-name convention, and this shim exists to fix the latter, not to
455
+ silently rename the former (see the section comment above).
456
+ """
457
+
458
+ def __init__(self, formula: Node):
459
+ self._var, self._term, self._pred = {}, {}, {}
460
+ # The three tables map names of three kinds, but CASL writes all of them as one
461
+ # identifier, so the three kinds draw the names they mint from ONE set of taken
462
+ # spellings: a minted name is fresh against every symbol of the formula, not
463
+ # only against the other symbols of its own kind.
464
+ taken: set = set()
465
+ self._var_used = self._term_used = self._pred_used = taken
466
+ for n in formula.walk():
467
+ if isinstance(n, Variable):
468
+ self._seed(self._var, self._var_used, n.name)
469
+ elif isinstance(n, (Constant, SortedConstant, Function)):
470
+ self._seed(self._term, self._term_used, n.name)
471
+ elif isinstance(n, Atom):
472
+ # '=' and '≠' are not simple identifiers, so _seed ignores them.
473
+ self._seed(self._pred, self._pred_used, n.predicate)
474
+ sort = getattr(n, "sort", None)
475
+ if isinstance(sort, str):
476
+ # a sort a node names is a spelling taken too (it has no table here)
477
+ taken.add(sort)
478
+
479
+ @staticmethod
480
+ def _seed(table: dict, used: set, name: str) -> None:
481
+ if name not in table and _SIMPLE_ID_RE.fullmatch(name):
482
+ table[name] = name
483
+ used.add(name)
484
+
485
+ @staticmethod
486
+ def _resolve(table: dict, used: set, name: str) -> str:
487
+ if name in table:
488
+ return table[name]
489
+ result = dedupe(_casl_sanitize_stem(name), used)
490
+ table[name] = result
491
+ return result
492
+
493
+ def variable(self, name: str) -> str:
494
+ return self._resolve(self._var, self._var_used, name)
495
+
496
+ def term(self, name: str) -> str:
497
+ return self._resolve(self._term, self._term_used, name)
498
+
499
+ def predicate(self, name: str) -> str:
500
+ return self._resolve(self._pred, self._pred_used, name)
501
+
502
+
503
+ def _rewrite_casl_names(node: Node, shim: _CaslIdentifierShim) -> Node:
504
+ """Rebuild ``node`` with every Variable / (Sorted)Constant / Function /
505
+ Atom-predicate name replaced by ``shim``'s resolution of it.
506
+
507
+ The five term/atom classes are handled explicitly (a name is a plain
508
+ ``str`` field, so the generic structural recursion below would copy it
509
+ verbatim rather than sanitise it); every other node — every connective
510
+ and (Sorted)Quantifier — recurses via
511
+ :meth:`~unicode_logic_kit.fol.nodes.Node.map_children`, the kit's shared
512
+ structural-rewrite primitive, which already applies this same function to
513
+ a Quantifier's bound ``variable`` field (a :class:`Variable`, handled by
514
+ the explicit case above) alongside its ``formula`` — so a Quantifier
515
+ needs no special case of its own here. A node type outside
516
+ ``fol.casl_export``'s classical fragment (e.g. a raw :class:`Box` reaching
517
+ this function directly, rather than through
518
+ :func:`~unicode_logic_kit.fol.qml.qml_validity_formula` first) is passed
519
+ through unchanged by that same generic recursion — this function performs
520
+ no fragment check of its own; :func:`~unicode_logic_kit.fol.casl_export.to_casl_spec`
521
+ still refuses it, by name, exactly as it always did.
522
+
523
+ An Atom whose predicate is ``=`` or ``≠`` is never renamed (see the section
524
+ comment above :func:`_casl_sanitize_stem`, point (2)): an ``=`` atom is
525
+ CASL's own rigid identity and is kept under its literal name, whatever its
526
+ arity (a non-binary one is refused downstream by
527
+ :func:`~unicode_logic_kit.fol.casl_export.to_casl_spec`, by name, not
528
+ relabelled here); a ``≠`` atom, which CASL has no rendering for at any
529
+ arity and which ``fol.qml`` has already written as ``¬(=)`` before this
530
+ function sees the formula, is refused with :class:`NotImplementedError`
531
+ rather than relabelled onto an unrelated identifier — see that branch's own
532
+ comment.
533
+ """
534
+ if isinstance(node, Variable):
535
+ return Variable(shim.variable(node.name))
536
+ if isinstance(node, SortedConstant):
537
+ return SortedConstant(shim.term(node.name), node.sort)
538
+ if isinstance(node, Constant):
539
+ return Constant(shim.term(node.name))
540
+ if isinstance(node, Function):
541
+ return Function(shim.term(node.name),
542
+ tuple(_rewrite_casl_names(a, shim) for a in node.args))
543
+ if isinstance(node, Atom):
544
+ args = tuple(_rewrite_casl_names(a, shim) for a in node.args)
545
+ if node.predicate == "=":
546
+ # CASL's own built-in identity: not a declared symbol, nothing to
547
+ # rename. A non-binary '=' (only reachable by hand-building the
548
+ # Node; qml_validity_formula refuses one) is left as it is so that
549
+ # fol.casl_export's exactly-two-terms check refuses it by name.
550
+ return Atom("=", args)
551
+ if node.predicate == "≠":
552
+ # fol.casl_export has no rendering rule for "≠" at ANY arity (CASL
553
+ # has no disequality connective), and fol.qml.qml_validity_formula
554
+ # never leaves one in its output (it writes "≠" as "not (=)"), so a
555
+ # "≠" atom reaches here only in a Node built by hand or parsed from
556
+ # classical text and handed to sanitize_modal_identifiers directly,
557
+ # which is documented as usable standalone. Falling through to the
558
+ # ordinary predicate-renaming path below would silently relabel it
559
+ # onto an arbitrary, unrelated legal identifier ('v_'), losing every
560
+ # trace of "not equal" -- the silent approximation the project's own
561
+ # refuse-loudly rule forbids. Refuse it instead, mirroring how
562
+ # fol.casl_export.to_casl_spec already refuses the same atom when
563
+ # handed it directly ("not a simple CASL identifier"), one layer
564
+ # earlier and with a message that says what to write instead. This
565
+ # sanitiser renames identifiers; it does not rewrite connectives.
566
+ raise NotImplementedError(
567
+ "hets.dol: sanitize_modal_identifiers cannot rename a '≠' atom "
568
+ f"({node.to_unicode_str()}) -- CASL has no native disequality "
569
+ "connective, so it would either become a meaningless renamed "
570
+ "predicate (masking the lost 'not equal' meaning) or crash "
571
+ "fol.casl_export's own identifier check. "
572
+ "unicode_logic_kit.fol.qml.qml_validity_formula never leaves one "
573
+ "in its output (it writes '≠' as 'not (=)' itself), so this "
574
+ "atom did not arrive through it -- write "
575
+ "'Not(Atom(\"=\", [a, b]))' instead of '≠' in the source formula."
576
+ )
577
+ return Atom(shim.predicate(node.predicate), args)
578
+ return node.map_children(lambda c: _rewrite_casl_names(c, shim))
579
+
580
+
581
+ def sanitize_modal_identifiers(formula: Node) -> Node:
582
+ """Return ``formula`` with every not-yet-legal CASL identifier — in
583
+ practice, :mod:`unicode_logic_kit.fol.qml`'s own auto-generated fresh
584
+ variables — renamed to a legal one, INJECTIVELY (two distinct source
585
+ names never collapse onto one output name) and leaving every
586
+ already-legal name untouched. Identity needs no renaming: a ``=`` atom is
587
+ CASL's own native, rigid equality and is kept as it is (see the section
588
+ comment above :func:`_casl_sanitize_stem`, point (2), for why that is also
589
+ what ``fol.qml`` hands over). See :class:`_CaslIdentifierShim` for the exact
590
+ algorithm and :func:`to_dol_library_from_modal`, which calls this
591
+ automatically, for the intended entry point.
592
+
593
+ Exposed publicly (rather than kept as a private helper of
594
+ :func:`to_dol_library_from_modal`) so a caller can inspect the sanitised
595
+ Node itself — e.g. to hand it to
596
+ :func:`~unicode_logic_kit.fol.casl_export.formula_to_casl` /
597
+ :func:`~unicode_logic_kit.fol.casl_export.to_casl_spec` directly instead of
598
+ going through a whole DOL library, or to unit-test the renaming in
599
+ isolation from CASL rendering.
600
+
601
+ A caller who reaches this function directly, bypassing
602
+ :func:`to_dol_library_from_modal` /
603
+ :func:`~unicode_logic_kit.fol.qml.qml_validity_formula`, owns two details the
604
+ translation would otherwise have settled: a ``≠`` atom must already be
605
+ written ``Not(Atom("=", ...))``, because ``fol.qml`` does that rewrite and
606
+ this function does not (it refuses a ``≠`` atom with
607
+ :class:`NotImplementedError` instead of renaming it onto an unrelated
608
+ identifier), and a ``=`` atom must be binary, because a non-binary one is
609
+ passed through untouched for :func:`~unicode_logic_kit.fol.casl_export.to_casl_spec`
610
+ to refuse.
611
+
612
+ Raises:
613
+ NotImplementedError: ``formula`` contains a ``≠`` atom, of any arity
614
+ (CASL has no native rendering for it; ``fol.qml`` writes ``≠`` as
615
+ the negation of ``=`` before a formula reaches this bridge).
616
+ """
617
+ return _rewrite_casl_names(formula, _CaslIdentifierShim(formula))
618
+
619
+
620
+ def to_dol_library_from_modal(
621
+ formula: Node, *,
622
+ mode: str = "constant", frame: str = "K", systems=None, bridges=None,
623
+ temporal_closure: bool = True,
624
+ spec_name: str = "ModalQuery", library_name: str = "ModalLib",
625
+ default_sort: str = "Thing",
626
+ ) -> str:
627
+ """Render one quantified-modal-logic validity query as a complete DOL
628
+ library, ready to upload to Hets — the official, tested composition of
629
+ :func:`unicode_logic_kit.fol.qml.qml_validity_formula` (the modal-to-
630
+ classical-FOL standard translation), :func:`sanitize_modal_identifiers`
631
+ (the identifier fix the section comment above this class explains), and
632
+ THIS module's own :func:`to_dol_library` / :class:`DolSpec` /
633
+ :func:`~unicode_logic_kit.fol.casl_export.to_casl_spec` — no change to any
634
+ of those three's own logic.
635
+
636
+ Args:
637
+ formula: the RAW modal formula (``Box``/``Diamond``/``Knows``/…,
638
+ NOT yet translated) — the same argument
639
+ :func:`~unicode_logic_kit.fol.qml.qml_is_valid` takes.
640
+ mode, frame, systems, bridges, temporal_closure: forwarded verbatim to
641
+ :func:`~unicode_logic_kit.fol.qml.qml_validity_formula` — see that
642
+ function's and :func:`~unicode_logic_kit.fol.qml.qml_is_valid`'s own
643
+ docstrings for what each selects (domain regime, alethic frame
644
+ system, agent-indexed epistemic/doxastic systems, cross-family
645
+ bridges, and the default-on temporal closure axioms).
646
+ spec_name: the single emitted spec's name (one qml validity query is
647
+ one self-contained CASL ``spec`` — there is nothing here to
648
+ structure across several specs the way :func:`to_dol_library`'s
649
+ general ``extends`` chaining supports).
650
+ library_name: the DOL library's own name.
651
+ default_sort: the ONE CASL sort the whole query is declared over —
652
+ :func:`~unicode_logic_kit.fol.qml.qml_translate`'s embedding is
653
+ SINGLE-sorted (worlds and objects share one FO sort, carved apart
654
+ by the ``World``/``Object`` GUARD PREDICATES qml's own axioms
655
+ assert — see ``fol.qml``'s module docstring), so there is no
656
+ richer CASL sort structure to pass through here; this is exactly
657
+ :func:`~unicode_logic_kit.fol.casl_export.to_casl_spec`'s own
658
+ ``default_sort`` parameter, forwarded unchanged.
659
+
660
+ The query is emitted as the spec's sole ``%implied`` CONJECTURE (no
661
+ separate axioms — :func:`~unicode_logic_kit.fol.qml.qml_validity_formula`'s
662
+ result is already the one closed implication ``⋀axioms ∧
663
+ ⋀typing-facts → …`` in full), so ``client.prove(iri, spec_name)`` is what
664
+ a caller runs against the uploaded library — the same shape
665
+ ``tests/test_dol.py``'s own live tests already use.
666
+
667
+ The translation binds world variables (``w``, ``v``, ``w0``, ...) and object variables
668
+ (``x``, ...) of its own, and a user constant, function or predicate of the same
669
+ spelling must not be read as one of them. :func:`~unicode_logic_kit.fol.casl_export.to_casl_spec`
670
+ renames every bound variable that is spelled like a symbol of the spec, so the text
671
+ reads back (through :func:`~unicode_logic_kit.fol.casl_import.parse_casl_spec`) as the
672
+ translation up to the names of those variables, and a constant called ``w`` or ``w0``
673
+ never changes the verdict.
674
+
675
+ An object-language ``=`` atom in ``formula`` arrives from
676
+ :func:`~unicode_logic_kit.fol.qml.qml_validity_formula` as the same binary
677
+ identity over the object terms with no world argument (``≠`` as its
678
+ negation), which is exactly CASL's own rigid, built-in ``=``: it is
679
+ rendered infix, declared nowhere, and read back by
680
+ :func:`~unicode_logic_kit.fol.casl_import.parse_casl_spec` as the identical
681
+ atom. So identity is rigid on this route exactly as it is for
682
+ :func:`~unicode_logic_kit.fol.qml.qml_is_valid` (``a = b → □(a = b)`` is a
683
+ theorem here, in every frame), and the two routes agree on it. See the
684
+ module-level section comment above :func:`_casl_sanitize_stem`, point (2).
685
+
686
+ Raises:
687
+ ValueError: any of :func:`~unicode_logic_kit.fol.qml.qml_axioms`'s /
688
+ :func:`~unicode_logic_kit.fol.qml.qml_translate`'s own refusals
689
+ (an unknown ``mode``/``frame``/``systems``/``bridges`` value, a
690
+ frame requesting a bridge whose partner family the formula never
691
+ mentions, or a ``=``/``≠`` atom that does not have exactly two
692
+ terms), or (propagated, unchanged) any
693
+ :func:`~unicode_logic_kit.fol.casl_export.to_casl_spec` /
694
+ :func:`to_dol_library` refusal for ``spec_name`` / ``library_name``
695
+ / ``default_sort``.
696
+ NotImplementedError: ``frame`` needs a non-first-order condition
697
+ (Löb/McKinsey/Grz — :func:`~unicode_logic_kit.fol.qml.qml_axioms`
698
+ refuses those by name, pointing at the higher-order routes that
699
+ DO carry them), or ``formula`` uses a construct
700
+ :func:`~unicode_logic_kit.fol.qml.qml_translate` itself does not
701
+ cover (``Until``/``Since``, the ``↓`` binder — see that module's
702
+ own docstring for the exact scope: propositional + first-order
703
+ modal, alethic/temporal/deontic/per-agent
704
+ epistemic-doxastic/PAL).
705
+ """
706
+ node = qml_validity_formula(formula, mode=mode, frame=frame, systems=systems,
707
+ bridges=bridges, temporal_closure=temporal_closure)
708
+ sanitized = sanitize_modal_identifiers(node)
709
+ specs: "OrderedDict[str, DolSpec]" = OrderedDict()
710
+ specs[spec_name] = DolSpec(axioms=[], conjectures=[sanitized],
711
+ default_sort=default_sort)
712
+ return to_dol_library(library_name, specs)