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,1135 @@
1
+ """CASL text emission for classical FOL and many-sorted FOL (MSFOL).
2
+
3
+ `CASL <https://en.wikipedia.org/wiki/Common_Algebraic_Specification_Language>`_
4
+ (the Common Algebraic Specification Language) is the CoFI family's central
5
+ ASCII, keyword-based specification language; ``libraries`` of CASL ``spec``s
6
+ are what `Hets <https://github.com/spechub/Hets>`_ (the Heterogeneous Tool
7
+ Set) parses, structures, and hands off to first-order/HOL back-ends. This
8
+ module is the EXPORTER (kit AST -> CASL text): it turns a batch of
9
+ the kit's classical FOL / MSFOL formulas into a complete, Hets-parsable
10
+ ``spec ... end`` block, or a bare formula fragment for embedding in
11
+ hand-written CASL. The other direction, for the text this module writes, is
12
+ :func:`unicode_logic_kit.fol.casl_import.parse_casl_spec`.
13
+
14
+ Scope: classical, two-valued FOL and many-sorted FOL only
15
+ -----------------------------------------------------------
16
+ CASL's basic first-order layer has no counting quantifiers, no modal/temporal
17
+ operators, no second-order quantification, and no substructural connectives —
18
+ so this exporter accepts exactly the classical fragment those constructs sit
19
+ outside of: :class:`~unicode_logic_kit.fol.nodes.Atom`, :class:`Not`,
20
+ :class:`And`, :class:`Or`, :class:`Xor`, :class:`Implies`, :class:`Iff`,
21
+ :class:`Quantifier`, :class:`SortedQuantifier` on the formula side, and
22
+ :class:`Variable`, :class:`Constant`, :class:`SortedConstant`,
23
+ :class:`Function` on the term side. Every OTHER node class the kit knows about
24
+ (modal/temporal/epistemic operators, :class:`Count`/:class:`Measure`/
25
+ :class:`Cardinality`/:class:`Contrast`/:class:`SortedCount`/
26
+ :class:`SortedCardinality`, :class:`Number`, second-order quantifiers,
27
+ Lambda/Application, and the linear/Lambek/team-semantic node families) is
28
+ REFUSED with a loud :class:`NotImplementedError` naming the offending node
29
+ class, per the kit's honesty convention (an unsupported construct is a
30
+ precise, actionable error — never a silent mistranslation or a weaker result
31
+ than what the input actually says). :func:`_check_fragment` implements this
32
+ gate by class-NAME membership in :data:`_ALLOWED_CASL_NODES` rather than an
33
+ ``isinstance`` chain over dozens of unrelated classes scattered across the
34
+ kit's node modules — the gate is meant to reject everything not explicitly
35
+ allow-listed, and a name-set is the form that stays correct (rejects) when a
36
+ new node class is added elsewhere in the kit and nobody updates this module.
37
+ A richer bridge for one non-classical logic — quantified modal logic — DOES
38
+ exist, and goes through DOL (the Distributed Ontology, Modelling and
39
+ Specification Language, CoFI's heterogeneous layer over CASL/OWL/etc.)
40
+ rather than a native "logic Modal" CASL institution:
41
+ :func:`unicode_logic_kit.fol.qml.qml_validity_formula` already lowers a modal
42
+ formula to exactly this module's classical fragment (the shallow, first-order
43
+ "standard translation" — see that module's own docstring), and
44
+ :func:`unicode_logic_kit.hets.dol.to_dol_library_from_modal` composes that
45
+ translation with THIS module's :func:`to_casl_spec` (sanitising ``qml``'s
46
+ auto-generated identifiers first, since those are not always legal CASL) to
47
+ hand the result to Hets as a DOL library. Identity needs no translation on
48
+ that path any more: ``qml`` keeps an ``=`` atom RIGID and exactly 2-ary — no
49
+ world argument — which is precisely CASL's own built-in ``=``, and lowers
50
+ ``≠`` to ``¬(=)``. The alias that used to rename a world-relativized,
51
+ ternary ``=``/``≠`` to a fresh uninterpreted predicate is gone with the
52
+ behaviour that produced it; a hand-built non-binary ``=`` is passed through
53
+ and refused by this module's own arity check, and a ``≠`` atom is refused by
54
+ ``hets.dol``'s sanitiser. This module's OWN
55
+ fragment gate — and its own exactly-2-ary ``=`` check — is unaffected: it
56
+ still refuses every non-classical node, and every malformed equality atom,
57
+ it always refused (see :func:`_check_fragment` and :func:`_infer_formula`);
58
+ the bridge works by translating the modal formula down to this fragment
59
+ BEFORE it ever reaches here, not by widening what this exporter itself
60
+ accepts.
61
+
62
+ ``Number`` is deliberately EXCLUDED even though it is a term the classical
63
+ grammar can trivially produce (``NUMBER`` is a term-layer alternative), because
64
+ CASL's basic library has no built-in numeric sort — a bare literal such as
65
+ ``3`` would need an ad hoc ``Nat``/``Int`` structuring this module does not
66
+ attempt, so it is refused rather than silently emitted as an undeclared term.
67
+
68
+ Sort inference: a union-find over symbol-position "slots"
69
+ -------------------------------------------------------------
70
+ A CASL ``ops``/``preds`` block declares the ARGUMENT SORTS of every operation
71
+ and predicate symbol — but the kit's classical (unsorted) :class:`Atom` /
72
+ :class:`Function` / :class:`Constant` carry no sort annotations at all, and
73
+ even :class:`SortedQuantifier` only annotates the BOUND VARIABLE, not every
74
+ symbol that variable is later passed to. So before any declaration can be
75
+ written, this module infers a single concrete sort for every symbol POSITION
76
+ that appears anywhere across the axioms and conjectures being exported, by
77
+ propagating the sort annotations that ARE present (:class:`SortedQuantifier`,
78
+ :class:`SortedConstant`) through every place a term is passed as an argument.
79
+
80
+ The inference is a classic union-find (disjoint-set) over a universe of
81
+ members of two kinds:
82
+
83
+ * a SLOT — one of ``("pred", name, i)`` (argument ``i`` of predicate
84
+ ``name``), ``("func", name, i)`` (argument ``i`` of function ``name``),
85
+ ``("func_result", name)`` (the sort of what ``name(...)`` itself denotes as
86
+ a term), or ``("const", name)`` (a constant symbol's own sort);
87
+ * a concrete SORT NAME (a plain string) — every string used as a sort
88
+ anywhere (a :class:`SortedQuantifier` sort, a :class:`SortedConstant` sort,
89
+ or ``default_sort``) acts as its own permanently-rooted anchor.
90
+
91
+ Each formula is walked with a variable environment (``name -> concrete sort``)
92
+ built up as quantifiers are entered — :class:`SortedQuantifier` binds its
93
+ variable to its own (concrete) sort; a plain (unsorted) :class:`Quantifier`
94
+ binds its variable to ``default_sort``; shadowing is scope-correct because the
95
+ environment is threaded functionally (a fresh dict per binder, never mutated
96
+ in place). Every place two term positions must agree in sort contributes ONE
97
+ union: a non-equality :class:`Atom`'s argument ``i`` unifies
98
+ ``("pred", name, i)`` with that argument's own slot; an equality
99
+ :class:`Atom` unifies its two arguments' slots directly with each other; a
100
+ :class:`Function`'s argument ``i`` unifies ``("func", name, i)`` with that
101
+ argument's slot, and the :class:`Function` application ITSELF (when it occurs
102
+ as someone else's argument) contributes the slot ``("func_result", name)``; a
103
+ :class:`Variable`'s slot is simply its environment sort (already concrete); a
104
+ :class:`Constant`'s slot is ``("const", name)``; a :class:`SortedConstant`
105
+ unifies ``("const", name)`` with its own concrete sort immediately.
106
+
107
+ After every formula has been walked, each union-find equivalence class is
108
+ resolved to a single sort: a class containing a concrete sort name uses it (a
109
+ class can never legitimately contain TWO DIFFERENT concrete names — that
110
+ situation is a genuine sort conflict and is refused immediately, at the
111
+ ``union`` call that would create it, with a :class:`ValueError` naming both
112
+ conflicting sorts and the symbol/position being unified); a class with no
113
+ concrete member at all (a symbol never connected, directly or transitively,
114
+ to any sort annotation) resolves to ``default_sort``. :class:`_UnionFind`
115
+ keeps concrete-sort roots as an INVARIANT (a string member is always its
116
+ class's root, never reparented under a slot-tuple root), which is what makes
117
+ conflict detection a plain root-equality check rather than a separate
118
+ per-class bookkeeping pass.
119
+
120
+ This is the TYPED reading, and it is stronger than the kit's own. The kit has ONE
121
+ universe, a sort is a (non-empty) subset of it, a constant written ``c:S`` is in
122
+ ``S``, and an unannotated constant, an unsorted variable and the value of a function
123
+ are ANY element of the universe: a function has no declared result sort. The
124
+ inference above declares an unannotated constant or a function value at the sort of
125
+ the position it is used in (``∀x:Human Mortal(x), Mortal(socrates)`` declares
126
+ ``socrates : Human``), and CASL's sorts are disjoint, so an unsorted quantifier
127
+ (typed with ``default_sort``) bounds a sort of its own rather than the universe the
128
+ other sorts are subsets of. An export into a typed language keeps writing that
129
+ reading, as a designed feature (the round trip through
130
+ :mod:`~unicode_logic_kit.fol.casl_import` and the typed worked examples rest on it); a
131
+ caller that DECIDES with the text must not take its answer for the kit's. The check that says when the
132
+ two readings differ is :func:`~unicode_logic_kit.atp.tptp_tff.check_typed_reading`
133
+ (shared with the TF0 and NXF writers), and
134
+ :class:`~unicode_logic_kit.atp.hets_backend.HetsBackend` applies it before it asks
135
+ Hets, answering ``unknown`` / ``unsupported`` for such a problem.
136
+
137
+ **The default sort is a sort of its own.** ``default_sort`` (``Thing``) is the sort of every
138
+ unsorted quantifier and of every position no annotation reaches. A sort of the SAME name
139
+ that the formulas write (``∀x:Thing``, ``c:Thing``) or ``subsorts`` declares would be one
140
+ sort with it, so an unsorted position would silently be a position of the user's sort:
141
+ ``∀x:Thing P(x) ⊢ ∀y P(y)`` would be proved, which the kit's reading (a sort is a part of
142
+ the universe) does not say. When the default sort is used at all (an unsorted quantifier,
143
+ or a symbol connected to no annotation) and a sort of that name is also written, the
144
+ export is refused by name, and the refusal names the keyword that picks another default
145
+ sort: ``default_sort=`` of :func:`to_casl_spec` and :func:`formula_to_casl`
146
+ (``DolSpec.default_sort`` in :mod:`~unicode_logic_kit.hets.dol`).
147
+ :class:`~unicode_logic_kit.atp.hets_backend.HetsBackend` picks a default sort that no sort
148
+ of the problem has, so it never meets the refusal.
149
+
150
+ Two further checks ride along the same walk, both refusals the kit's honesty
151
+ convention requires rather than a mistranslation: a predicate or function
152
+ name used at two DIFFERENT arities across the exported formulas (CASL has no
153
+ notion of a single symbol overloaded across arities in this module's simple
154
+ model) raises :class:`ValueError`; and a name used both as a bare
155
+ :class:`Constant`/:class:`SortedConstant` and as a :class:`Function`
156
+ application raises :class:`ValueError` (this module declares at most one
157
+ ``ops`` entry per name, so the two uses cannot be reconciled). A free
158
+ :class:`Variable` — one never bound by any enclosing quantifier — also
159
+ raises :class:`ValueError`: CASL ``. <formula>`` axioms must be closed, so
160
+ there is no implicit universal closure or variable declaration to fall back
161
+ on here.
162
+
163
+ Bound variables and the symbols of the spec
164
+ ----------------------------------------------
165
+ CASL text writes a bound variable, a constant (a 0-ary operation) and a predicate as
166
+ one identifier, compared exactly (``w`` and ``W`` are two names), and inside a
167
+ quantifier the name of its variable is the variable: ``forall w : Thing . (P(w) =>
168
+ Q(w))`` next to ``ops w : Thing`` says nothing about a constant ``w``, whatever the
169
+ formula was. So a bound variable that is spelled like ANY symbol of the whole spec (a
170
+ constant, a function, a predicate, a sort, the default sort) is renamed to a fresh
171
+ variable name, fresh against every name the formulas hold, in its quantifier and in
172
+ every occurrence it binds (``forall w0 : Thing . (P(w0) => Q(w))``). Text without such
173
+ a clash is byte-identical to what it was before. The renaming is an alpha-conversion
174
+ of a closed sentence, so the text means the formula; read back through
175
+ :func:`~unicode_logic_kit.fol.casl_import.parse_casl_spec` it is the same formula up to
176
+ the names of the variables that had to be renamed. :func:`to_casl_spec` and
177
+ :func:`formula_to_casl` take ``visible_symbols`` for the symbols the text meets
178
+ without declaring them.
179
+
180
+ Reserved words and identifier hygiene
181
+ ----------------------------------------
182
+ CASL keywords (``sort``, ``pred``, ``forall``, ``not``, …; the exact list is
183
+ :data:`_CASL_KEYWORDS`, taken from the CASL Reference Manual's lexical
184
+ grammar) may not be used as ordinary identifiers. This module checks every
185
+ CONSTANT, FUNCTION, and VARIABLE name it emits against that list (variables
186
+ are checked too, even though the kit's grammar restricts a variable to a
187
+ single lowercase letter optionally followed by digits — ``[a-z][0-9]*`` — so
188
+ in practice a variable can never collide with any of the (all
189
+ two-or-more-character) CASL keywords; the check runs anyway, both for
190
+ uniformity with the constant/function path and as a safety net if the kit's
191
+ variable grammar or the keyword list ever changes) and refuses with
192
+ :class:`ValueError` on any collision. ``spec_name`` (the ``spec <NAME> = ...``
193
+ header) is validated separately: it must match the simple-identifier pattern
194
+ ``[A-Za-z][A-Za-z0-9_]*`` and must not itself be a keyword.
195
+
196
+ Formula emission rules (exact, classical two-valued reading)
197
+ ------------------------------------------------------------
198
+ * Connectives render as CASL's ASCII operators: ``/\\`` (and), ``\\/`` (or),
199
+ ``=>`` (implies), ``<=>`` (iff), ``not <f>`` (negation), ``t = t'``
200
+ (equality). :class:`Xor` has no direct CASL operator, so it is expanded to
201
+ its exact classical reading, ``not (<a> <=> <b>)`` — i.e. XOR-as-negated-IFF,
202
+ the standard truth-functional identity (P XOR Q is true exactly when P and Q
203
+ disagree, i.e. exactly when P IFF Q is false).
204
+ * A quantifier renders as ``forall x : Sort . <body>`` / ``exists x : Sort .
205
+ <body>``; a plain (unsorted) :class:`Quantifier` uses ``default_sort`` for
206
+ its bound variable's declared sort.
207
+ * Parenthesisation follows one rule, applied uniformly at every non-root
208
+ position: an :class:`Atom` renders bare; a :class:`Not` **of an atom** is
209
+ rendered bare as a connective operand (``not P /\\ Q`` — CASL's ``not``
210
+ binds tightest, so this is unambiguous), while a Not of a COMPOUND gets
211
+ the operand-level parens like any other compound child — yielding e.g.
212
+ ``(not (P /\\ Q)) \\/ R``, where the inner pair is Not self-delimiting
213
+ its own operand and the outer pair is the ordinary child wrap. The outer
214
+ pair is technically redundant (the inner parens already delimit the
215
+ scope) but harmless, and keeping the child rule uniform beats a special
216
+ case (adversarial-review adjudicated: docstring corrected to the actual,
217
+ semantically exact behaviour). As Not's OWN operand the rule is
218
+ narrower: an atom stays bare (``not P``), anything else is wrapped in
219
+ one pair (``not (P /\\ Q)``); every OTHER node
220
+ (And/Or/Xor/Implies/Iff, and — with one exception — Quantifier/
221
+ SortedQuantifier) is wrapped in parens whenever it is not the formula's own
222
+ root. The one exception: a quantifier used as the BODY of an enclosing
223
+ quantifier stays bare (``forall x:S . exists y:T . phi``, no parens) — CASL
224
+ quantifier scope already extends as far right as syntactically possible, so
225
+ chained binders are unambiguous without help, exactly the same reason
226
+ nested parens are never needed between a quantifier and a following
227
+ ``not``. A quantifier used as the operand of a BINARY connective, or as
228
+ Not's operand, IS wrapped (a bare ``P => exists x:S . Q`` would let the
229
+ quantifier's scope run past where the writer intended, so CASL text must
230
+ delimit it explicitly there) — this is also why a binary connective used as
231
+ a quantifier's body is wrapped (``forall x:S . (P => Q)``, never ``forall
232
+ x:S . P => Q``): the worked example in this module's docstring and test
233
+ suite is exactly this shape.
234
+ * A 0-ary :class:`Atom` renders as its bare predicate name, both in a formula
235
+ and as a ``preds`` declaration (``Rain : ()``).
236
+ * The truth constants ``$true`` / ``$false`` render as CASL's own formulas
237
+ ``true`` / ``false`` and declare no predicate.
238
+
239
+ ``to_casl_spec`` output shape (see the module's tests for the byte-exact
240
+ golden cases): a ``spec <name> =`` header; an optional ``sorts`` line (only
241
+ if at least one sort is used) with every distinct sort name, alphabetically,
242
+ comma-separated on one line; when ``subsorts`` is given (see below), one
243
+ further ``sort <child> < <parent>`` line per DIRECT edge, in
244
+ ``(child, parent)`` order — one line per edge even when a child has several
245
+ parents or several children share one parent, never CASL's list-sharing form
246
+ (``sort S1, S2 < T``), which keeps the emitted grammar a strict subset of
247
+ what :mod:`unicode_logic_kit.fol.casl_import` already parses one edge at a
248
+ time; an optional ``ops`` block (constants AND functions merged into ONE
249
+ alphabetically-sorted list — a constant's ``ops`` entry is ``name : Sort``,
250
+ a function's is ``name : Sort1 * … -> Result``);
251
+ an optional ``preds`` block (``name : Sort1 * … * Sortn``, or ``name : ()``
252
+ for a nullary predicate), alphabetically; each axiom as its own ``. <formula>``
253
+ line, each conjecture as its own ``. <formula> %implied`` line (the ``%implied``
254
+ annotation is what Hets turns into a proof obligation rather than an assumed
255
+ axiom); and a closing ``end``. A section header's entries after the first are
256
+ indented to align under the first entry's own column, each non-final entry
257
+ suffixed with ``;``. Output is a pure function of the input AST (no
258
+ timestamps, no incidental set-iteration-order nondeterminism — every
259
+ collection is explicitly sorted before being joined into text), so the same
260
+ input always produces byte-identical output.
261
+ """
262
+
263
+ from dataclasses import dataclass, field, replace
264
+ from typing import Dict, FrozenSet, Iterable, List, Mapping, Optional, Sequence, Set, Tuple, Union
265
+ import re
266
+
267
+ from .nodes import Node, SortedConstant, SortedQuantifier, Variable
268
+ from ._identifiers import fresh_variable_like, symbol_names
269
+ from ._truth_constants import truth_value
270
+ # Shared arity-conflict / constant-vs-function name-clash refusal, factored
271
+ # out into unicode_logic_kit.fol.signature so this module's independently-
272
+ # accumulated bookkeeping (see _Signature's docstring) and Signature.
273
+ # from_formulas's own arity/name-clash refusals come from one tested
274
+ # comparison-and-raise, even though each keeps its own accumulation
275
+ # strategy and message wording — see signature.py's module docstring's
276
+ # DESIGN NOTE.
277
+ from .signature import _check_single_valued, _check_not_dual_use
278
+
279
+ __all__ = ["to_casl_spec", "formula_to_casl"]
280
+
281
+
282
+ # =============================================================================
283
+ # Fragment gate
284
+ # =============================================================================
285
+
286
+ # Exactly the classical FOL / many-sorted-FOL node classes this exporter
287
+ # understands. See the module docstring's "Scope" section for why detection
288
+ # is by class-NAME membership here rather than an isinstance chain, and why
289
+ # Number is excluded despite being a plain classical term.
290
+ _ALLOWED_CASL_NODES = frozenset({
291
+ "Variable", "Constant", "SortedConstant", "Function",
292
+ "Atom", "Not", "And", "Or", "Xor", "Implies", "Iff",
293
+ "Quantifier", "SortedQuantifier",
294
+ })
295
+
296
+
297
+ def _check_fragment(formula: Node) -> None:
298
+ """Walk ``formula`` and refuse (loudly) the first node outside the
299
+ classical FOL / MSFOL fragment this exporter covers.
300
+
301
+ Raises :class:`NotImplementedError` naming the specific offending node
302
+ class. ``Node.walk()`` yields in pre-order, so for a disallowed
303
+ CONSTRUCT (e.g. a :class:`Count` wrapping an otherwise-classical
304
+ formula) the outer, disallowed node itself is what gets named — not
305
+ some allowed node buried inside it.
306
+ """
307
+ for n in formula.walk():
308
+ cls = type(n).__name__
309
+ if cls not in _ALLOWED_CASL_NODES:
310
+ raise NotImplementedError(
311
+ f"CASL export: {cls!r} is outside the classical FOL / "
312
+ "many-sorted FOL fragment this exporter covers (only Atom, "
313
+ "Not, And, Or, Xor, Implies, Iff, Quantifier, SortedQuantifier, "
314
+ "and the term classes Variable, Constant, SortedConstant, "
315
+ "Function are CASL-expressible here). Quantified modal logic "
316
+ "already has a route to CASL/DOL/Hets: translate the RAW modal "
317
+ "formula first with unicode_logic_kit.fol.qml.qml_validity_formula "
318
+ "(the standard translation to this classical fragment), then "
319
+ "hand it to unicode_logic_kit.hets.dol.to_dol_library_from_modal — "
320
+ "do not pass a modal node (Box/Diamond/…) to this exporter "
321
+ "directly. Every other non-classical logic (counting/measure/"
322
+ "cardinality, second-order quantification, linear/Lambek/"
323
+ "team-semantic connectives, …) has no such route yet and may "
324
+ "later be routed through a DOL (Distributed Ontology Language) "
325
+ "heterogeneous translation instead of a direct CASL emitter."
326
+ )
327
+
328
+
329
+ # =============================================================================
330
+ # CASL reserved words and identifier validation
331
+ # =============================================================================
332
+
333
+ # The CASL lexical keyword set (case-sensitive; all lowercase), per the CASL
334
+ # Reference Manual. None of these may be used as an ordinary sort/op/pred/
335
+ # variable identifier.
336
+ _CASL_KEYWORDS = frozenset({
337
+ "and", "arch", "as", "assoc", "axiom", "axioms", "closed", "comm", "def",
338
+ "else", "end", "exists", "false", "fit", "forall", "free", "from",
339
+ "generated", "get", "given", "hide", "idem", "if", "in", "lambda",
340
+ "library", "local", "logic", "not", "op", "ops", "pred", "preds",
341
+ "result", "reveal", "sort", "sorts", "spec", "then", "to", "true",
342
+ "type", "types", "unit", "units", "var", "vars", "version", "view",
343
+ "when", "with", "within",
344
+ })
345
+
346
+ _SIMPLE_ID_RE = re.compile(r"[A-Za-z][A-Za-z0-9_]*")
347
+
348
+
349
+ def _check_reserved(name: str, kind: str) -> None:
350
+ """Refuse ``name`` (a ``kind`` identifier — 'constant' / 'function' /
351
+ 'variable' / 'predicate' / 'sort') if it collides with a CASL keyword
352
+ or is not a simple CASL word at all.
353
+
354
+ Predicates and sorts are checked since the adversarial review: the
355
+ parser's casing rules never apply to directly-constructed AST nodes
356
+ (``Atom("axiom", ())``) or to raw sort strings (``SortedQuantifier``
357
+ annotations, ``default_sort``), and a keyword or malformed identifier
358
+ in ANY emitted position renders a spec HETS cannot parse (verified
359
+ live: HTTP 500 ``unexpected keyword``). See the module docstring's
360
+ "Reserved words" section for why the 'variable' case is checked even
361
+ though it can never fire under the kit's own variable grammar.
362
+ """
363
+ if name in _CASL_KEYWORDS:
364
+ raise ValueError(
365
+ f"CASL export: {kind} name '{name}' collides with the reserved "
366
+ f"CASL keyword '{name}' and cannot be emitted as an identifier."
367
+ )
368
+ if not _SIMPLE_ID_RE.fullmatch(name):
369
+ raise ValueError(
370
+ f"CASL export: {kind} name {name!r} is not a simple CASL "
371
+ "identifier matching [A-Za-z][A-Za-z0-9_]*."
372
+ )
373
+
374
+
375
+ def _validate_spec_name(name: str) -> None:
376
+ """Refuse a ``spec_name`` that is not a simple CASL identifier, or that
377
+ is itself a CASL keyword."""
378
+ if not _SIMPLE_ID_RE.fullmatch(name):
379
+ raise ValueError(
380
+ f"CASL export: spec_name {name!r} is not a simple CASL "
381
+ "identifier matching [A-Za-z][A-Za-z0-9_]*."
382
+ )
383
+ if name in _CASL_KEYWORDS:
384
+ raise ValueError(
385
+ f"CASL export: spec_name {name!r} collides with the reserved "
386
+ f"CASL keyword '{name}'."
387
+ )
388
+
389
+
390
+ # =============================================================================
391
+ # Sort inference: union-find over slots and concrete sort names
392
+ # =============================================================================
393
+
394
+ # A "slot" is a tuple key identifying one symbol POSITION whose sort is being
395
+ # inferred; a plain str member is a concrete, already-known sort name. See the
396
+ # module docstring's "Sort inference" section for the full account.
397
+ _Slot = Union[Tuple[str, str, int], Tuple[str, str], str]
398
+
399
+
400
+ class _UnionFind:
401
+ """Weighted union-find over sort-inference slots and concrete sort names.
402
+
403
+ Invariant: whenever a class contains a concrete sort name (a plain str
404
+ member), that string is ALWAYS the class's root — union() only ever
405
+ reparents a slot-tuple root onto a string root, never the reverse. That
406
+ invariant is what makes conflict detection O(1) once both roots are
407
+ known: if find(a) and find(b) are both strings and different, the merge
408
+ would put two incompatible concrete sorts in one class.
409
+ """
410
+
411
+ def __init__(self) -> None:
412
+ self._parent: Dict[_Slot, _Slot] = {}
413
+ self._rank: Dict[_Slot, int] = {}
414
+
415
+ def find(self, x: _Slot) -> _Slot:
416
+ """Return the root of x's class, path-compressing along the way."""
417
+ self._parent.setdefault(x, x)
418
+ path = []
419
+ while self._parent[x] != x:
420
+ path.append(x)
421
+ x = self._parent[x]
422
+ for n in path:
423
+ self._parent[n] = x
424
+ return x
425
+
426
+ def union(self, a: _Slot, b: _Slot, context: str) -> None:
427
+ """Merge the classes of ``a`` and ``b``.
428
+
429
+ ``context`` names what triggered this merge (a predicate/function
430
+ argument position, an equality, or a sort annotation) and is used
431
+ only to build the error message below.
432
+
433
+ Raises :class:`ValueError` if ``a`` and ``b``'s classes each already
434
+ carry a DIFFERENT concrete sort name — an unsatisfiable constraint —
435
+ naming both conflicting sorts and ``context``.
436
+ """
437
+ ra, rb = self.find(a), self.find(b)
438
+ if ra == rb:
439
+ return
440
+ a_concrete, b_concrete = isinstance(ra, str), isinstance(rb, str)
441
+ if a_concrete and b_concrete:
442
+ raise ValueError(
443
+ f"CASL export: sort conflict while resolving {context} — "
444
+ f"inferred both '{ra}' and '{rb}'."
445
+ )
446
+ if a_concrete:
447
+ self._parent[rb] = ra
448
+ elif b_concrete:
449
+ self._parent[ra] = rb
450
+ else:
451
+ rank_a, rank_b = self._rank.get(ra, 0), self._rank.get(rb, 0)
452
+ if rank_a < rank_b:
453
+ ra, rb = rb, ra
454
+ self._parent[rb] = ra
455
+ if rank_a == rank_b:
456
+ self._rank[ra] = rank_a + 1
457
+
458
+ def sort_of(self, slot: _Slot, default_sort: str) -> str:
459
+ """Return the concrete sort resolved for ``slot``, or ``default_sort``
460
+ if that slot's class has no concrete member at all."""
461
+ root = self.find(slot)
462
+ return root if isinstance(root, str) else default_sort
463
+
464
+
465
+ @dataclass
466
+ class _Signature:
467
+ """Mutable accumulator built by one :func:`_analyze` pass.
468
+
469
+ uf — the union-find carrying every sort-inference constraint.
470
+ pred_arity — predicate name -> the single arity it is checked against.
471
+ func_arity — function name -> the single arity it is checked against.
472
+ const_names — every constant symbol seen (Constant or SortedConstant).
473
+ func_names — every function symbol seen.
474
+ literal_sorts — every sort name written explicitly in the source (a
475
+ SortedQuantifier's or SortedConstant's own sort) or
476
+ implied by a plain Quantifier (default_sort) —
477
+ independent of whether union-find ever connects that sort
478
+ to any declared op/pred, so a vacuously-used sort (e.g. a
479
+ quantified variable that is never passed to anything) is
480
+ still declared in the spec's ``sorts`` line.
481
+ default_sort — threaded through so the walk functions don't need it as
482
+ a separate parameter.
483
+ """
484
+
485
+ uf: _UnionFind = field(default_factory=_UnionFind)
486
+ pred_arity: Dict[str, int] = field(default_factory=dict)
487
+ func_arity: Dict[str, int] = field(default_factory=dict)
488
+ const_names: Set[str] = field(default_factory=set)
489
+ func_names: Set[str] = field(default_factory=set)
490
+ literal_sorts: Set[str] = field(default_factory=set)
491
+ default_sort: str = "Thing"
492
+ #: the sorts the caller wrote (a SortedQuantifier's or a SortedConstant's), never the
493
+ #: default sort that a plain Quantifier implies
494
+ user_sorts: Set[str] = field(default_factory=set)
495
+ #: whether a plain Quantifier typed a variable with the default sort
496
+ default_quantified: bool = False
497
+
498
+
499
+ _CONST_VS_FUNCTION = (
500
+ "CASL export: '{name}' is used both as a constant and as a function — "
501
+ "this exporter declares at most one 'ops' entry per name, so the two "
502
+ "uses cannot be reconciled into a single CASL operation symbol."
503
+ )
504
+
505
+
506
+ def _infer_term(node: Node, env: Dict[str, str], sig: _Signature) -> _Slot:
507
+ """Process a term node: register its symbol, recurse into any
508
+ sub-terms, and return the union-find slot that represents its sort.
509
+
510
+ Raises ValueError on a free variable, an arity conflict, a constant/
511
+ function name clash, or (via ``sig.uf.union``) a sort conflict.
512
+ """
513
+ cls = type(node).__name__
514
+
515
+ if cls == "Variable":
516
+ if node.name not in env:
517
+ raise ValueError(
518
+ f"CASL export: free variable '{node.name}' — CASL axioms "
519
+ "must be closed formulas; every variable must be bound by "
520
+ "an enclosing quantifier."
521
+ )
522
+ return env[node.name]
523
+
524
+ if cls in ("Constant", "SortedConstant"):
525
+ name = node.name
526
+ _check_not_dual_use(name, sig.func_names, _CONST_VS_FUNCTION.format(name=name))
527
+ sig.const_names.add(name)
528
+ slot: _Slot = ("const", name)
529
+ if cls == "SortedConstant":
530
+ # _check_fragment admits a node only by class name, and SortedConstant is the
531
+ # one class of that name in the node hierarchy.
532
+ assert isinstance(node, SortedConstant)
533
+ sig.literal_sorts.add(node.sort)
534
+ sig.user_sorts.add(node.sort)
535
+ sig.uf.union(slot, node.sort, f"constant '{name}' sort annotation")
536
+ return slot
537
+
538
+ if cls == "Function":
539
+ name = node.name
540
+ _check_not_dual_use(name, sig.const_names, _CONST_VS_FUNCTION.format(name=name))
541
+ arity = len(node.args)
542
+ prev = sig.func_arity.get(name)
543
+ if prev is not None:
544
+ _check_single_valued(
545
+ {prev, arity},
546
+ f"CASL export: function '{name}' used with conflicting "
547
+ f"arities {prev} and {arity}."
548
+ )
549
+ sig.func_arity[name] = arity
550
+ sig.func_names.add(name)
551
+ for i, a in enumerate(node.args):
552
+ arg_slot = _infer_term(a, env, sig)
553
+ sig.uf.union(("func", name, i), arg_slot,
554
+ f"function '{name}' argument {i + 1}")
555
+ return ("func_result", name)
556
+
557
+ raise AssertionError(
558
+ f"CASL export: unreachable — term node {cls!r} passed the fragment "
559
+ "check but is not handled by _infer_term."
560
+ )
561
+
562
+
563
+ def _infer_formula(node: Node, env: Dict[str, str], sig: _Signature) -> None:
564
+ """Walk a formula node, threading the bound-variable environment,
565
+ registering every predicate/connective/quantifier constraint into
566
+ ``sig``. Raises ValueError per :func:`_infer_term` and on a predicate
567
+ arity conflict or a malformed equality atom.
568
+ """
569
+ cls = type(node).__name__
570
+
571
+ if cls == "Atom":
572
+ if truth_value(node) is not None:
573
+ return # CASL's own `true` / `false`: no predicate
574
+ if node.predicate == "=":
575
+ if len(node.args) != 2:
576
+ raise ValueError(
577
+ "CASL export: equality atom must have exactly 2 "
578
+ f"arguments, got {len(node.args)}."
579
+ )
580
+ s0 = _infer_term(node.args[0], env, sig)
581
+ s1 = _infer_term(node.args[1], env, sig)
582
+ sig.uf.union(s0, s1, "an equality (=) atom")
583
+ return
584
+ arity = len(node.args)
585
+ prev = sig.pred_arity.get(node.predicate)
586
+ if prev is not None:
587
+ _check_single_valued(
588
+ {prev, arity},
589
+ f"CASL export: predicate '{node.predicate}' used with "
590
+ f"conflicting arities {prev} and {arity}."
591
+ )
592
+ sig.pred_arity[node.predicate] = arity
593
+ for i, a in enumerate(node.args):
594
+ arg_slot = _infer_term(a, env, sig)
595
+ sig.uf.union(("pred", node.predicate, i), arg_slot,
596
+ f"predicate '{node.predicate}' argument {i + 1}")
597
+ return
598
+
599
+ if cls == "Not":
600
+ _infer_formula(node.formula, env, sig)
601
+ return
602
+
603
+ if cls in ("And", "Or", "Xor", "Implies", "Iff"):
604
+ _infer_formula(node.left, env, sig)
605
+ _infer_formula(node.right, env, sig)
606
+ return
607
+
608
+ if cls == "Quantifier":
609
+ _check_reserved(node.variable.name, "variable")
610
+ sig.literal_sorts.add(sig.default_sort)
611
+ sig.default_quantified = True
612
+ new_env = dict(env)
613
+ new_env[node.variable.name] = sig.default_sort
614
+ _infer_formula(node.formula, new_env, sig)
615
+ return
616
+
617
+ if cls == "SortedQuantifier":
618
+ # Same invariant as for SortedConstant in _infer_term.
619
+ assert isinstance(node, SortedQuantifier)
620
+ _check_reserved(node.variable.name, "variable")
621
+ sig.literal_sorts.add(node.sort)
622
+ sig.user_sorts.add(node.sort)
623
+ new_env = dict(env)
624
+ new_env[node.variable.name] = node.sort
625
+ _infer_formula(node.formula, new_env, sig)
626
+ return
627
+
628
+ raise AssertionError(
629
+ f"CASL export: unreachable — formula node {cls!r} passed the "
630
+ "fragment check but is not handled by _infer_formula."
631
+ )
632
+
633
+
634
+ def _analyze(formulas: Sequence[Node], default_sort: str,
635
+ declared_sorts: Iterable[str] = ()) -> _Signature:
636
+ """Run the full validation + sort-inference pipeline over a batch of
637
+ formulas that will share one CASL signature (all the axioms and
638
+ conjectures of a single :func:`to_casl_spec` call, or the single formula
639
+ passed to :func:`formula_to_casl`).
640
+
641
+ Every formula is fragment-checked FIRST, across the whole batch, so an
642
+ out-of-fragment node is always reported before any sort-inference error
643
+ that a DIFFERENT, in-fragment formula in the same batch might otherwise
644
+ trigger. Each formula gets its own FRESH variable environment (formulas
645
+ are independent closed statements; a variable named ``x`` in one axiom
646
+ is unrelated to ``x`` in another), but all formulas share one
647
+ :class:`_Signature` (one union-find, one arity/name-clash bookkeeping),
648
+ because they share one CASL ``ops``/``preds`` vocabulary.
649
+
650
+ ``declared_sorts`` are sorts the caller declares without a formula (the names of the
651
+ ``subsorts`` edges); like the sorts the formulas write they must not be the default
652
+ sort when that is used (:func:`_check_default_sort_is_free`).
653
+ """
654
+ for f in formulas:
655
+ _check_fragment(f)
656
+ # default_sort is validated UNCONDITIONALLY, not only when a quantifier
657
+ # binds it into sig.literal_sorts: the union-find's sort_of() fallback
658
+ # emits it into the `sorts` line for any argument slot never connected
659
+ # to a concrete annotation (e.g. a quantifier-free axiom batch), so a
660
+ # keyword or malformed default_sort would otherwise slip straight into
661
+ # the spec text (adversarial review, Tier 3 — the DolSpec.default_sort
662
+ # route reaches here with an arbitrary caller string).
663
+ _check_reserved(default_sort, "sort")
664
+ sig = _Signature(default_sort=default_sort)
665
+ for f in formulas:
666
+ _infer_formula(f, {}, sig)
667
+ for name in sig.const_names:
668
+ _check_reserved(name, "constant")
669
+ for name in sig.func_names:
670
+ _check_reserved(name, "function")
671
+ # Predicates and sorts are emitted identifiers exactly like constants
672
+ # and functions: a keyword-named predicate (Atom("axiom", ()) built
673
+ # directly on the AST — the parser's casing rule never applies there)
674
+ # or a keyword sort (SortedQuantifier/SortedConstant annotations, or a
675
+ # caller-supplied default_sort, all raw strings) would render a spec
676
+ # HETS cannot parse at all. Review-confirmed live; refuse loudly.
677
+ for name in sig.pred_arity:
678
+ if name != "=":
679
+ _check_reserved(name, "predicate")
680
+ for name in sig.literal_sorts:
681
+ _check_reserved(name, "sort")
682
+ _check_default_sort_is_free(sig, declared_sorts)
683
+ return sig
684
+
685
+
686
+ #: A sort name no caller can write (it is not an identifier): marks a slot no annotation reaches.
687
+ _NO_SORT = "\0"
688
+
689
+
690
+ def _default_sort_is_used(sig: _Signature) -> bool:
691
+ """Whether the default sort types anything: a variable of a plain quantifier, or a
692
+ symbol position that no sort annotation reaches (:meth:`_UnionFind.sort_of` falls back
693
+ to the default sort for it)."""
694
+ if sig.default_quantified:
695
+ return True
696
+ slots: List[_Slot] = [("const", name) for name in sig.const_names]
697
+ for name, arity in sig.func_arity.items():
698
+ slots.append(("func_result", name))
699
+ slots.extend(("func", name, i) for i in range(arity))
700
+ for name, arity in sig.pred_arity.items():
701
+ if name != "=":
702
+ slots.extend(("pred", name, i) for i in range(arity))
703
+ return any(sig.uf.sort_of(slot, _NO_SORT) == _NO_SORT for slot in slots)
704
+
705
+
706
+ def _check_default_sort_is_free(sig: _Signature, declared_sorts: Iterable[str]) -> None:
707
+ """Refuse a sort the caller wrote that is spelled like the default sort, when the
708
+ default sort is used (see the module docstring, "The default sort is a sort of its own").
709
+
710
+ Raises:
711
+ ValueError: such a sort exists; the message names it and the keyword that picks
712
+ another default sort.
713
+ """
714
+ written = sig.user_sorts | set(declared_sorts)
715
+ if sig.default_sort in written and _default_sort_is_used(sig):
716
+ raise ValueError(
717
+ f"CASL export: the sort {sig.default_sort!r} is written in the formulas (or "
718
+ f"declared in subsorts) and is also the default sort, which this exporter gives "
719
+ "every unsorted quantifier and every position no sort annotation reaches, so the "
720
+ "two would be ONE sort and an unsorted position would silently become a position "
721
+ f"of your sort {sig.default_sort!r} (∀x:{sig.default_sort} P(x) ⊢ ∀y P(y) would "
722
+ "be proved). Pick another default sort with the keyword default_sort= of "
723
+ "to_casl_spec / formula_to_casl (DolSpec.default_sort in hets.dol), for "
724
+ f"example default_sort={sig.default_sort + '1'!r}.")
725
+
726
+
727
+ # =============================================================================
728
+ # Bound variables against the symbols of the specification
729
+ # =============================================================================
730
+ #
731
+ # In CASL text a bound variable, a constant (a 0-ary operation) and a predicate are
732
+ # all written as one identifier, and identifiers are compared exactly (``w`` and ``W``
733
+ # are two names, as :func:`~unicode_logic_kit.fol.casl_import.parse_casl_spec` reads
734
+ # them). ``forall w : Thing . (P(w) => Q(w))`` therefore cannot say that ``Q`` is
735
+ # applied to a constant ``w``: inside the quantifier the name is the variable. A
736
+ # binder is renamed when its name is the name of any symbol of the specification, and
737
+ # only then, so text without such a clash is unchanged.
738
+
739
+ def _specification_symbols(sig: _Signature, extra: Iterable[str] = ()) -> FrozenSet[str]:
740
+ """Every name of the specification that a bound variable must not share: the constants,
741
+ functions and predicates it declares, every sort it names, the default sort, and the
742
+ names in ``extra`` (sorts declared by ``subsorts``, symbols visible from outside)."""
743
+ names: Set[str] = set(sig.const_names) | set(sig.func_names)
744
+ names.update(name for name in sig.pred_arity if name != "=")
745
+ names.update(sig.literal_sorts)
746
+ names.update(sig.user_sorts)
747
+ names.update(extra)
748
+ names.add(sig.default_sort)
749
+ return frozenset(names)
750
+
751
+
752
+ def _bind_apart(formulas: Sequence[Node], symbols: FrozenSet[str]) -> List[Node]:
753
+ """``formulas`` with every bound variable that is spelled like one of ``symbols`` renamed.
754
+
755
+ A binder whose name is in ``symbols`` is renamed to a fresh variable name (the same
756
+ one the occurrences it binds are rewritten to), fresh against every name any of the
757
+ formulas holds, against ``symbols``, and against every name minted before it. Every
758
+ other binder and every other node is kept as it is, and when no binder clashes the
759
+ formulas come back unchanged. A formula is a closed sentence here (a free variable
760
+ has been refused), so renaming a binder changes nothing the formula says.
761
+ """
762
+ clashing = {node.variable.name for f in formulas for node in f.walk()
763
+ if type(node).__name__ in ("Quantifier", "SortedQuantifier")
764
+ and node.variable.name in symbols}
765
+ if not clashing:
766
+ return list(formulas)
767
+ names = set(symbol_names(*formulas)) | set(symbols)
768
+
769
+ def rename(node: Node, scope: Dict[str, str]) -> Node:
770
+ kind = type(node).__name__
771
+ if kind == "Variable":
772
+ new = scope.get(node.name)
773
+ return node if new is None else Variable(new)
774
+ if kind in ("Quantifier", "SortedQuantifier"):
775
+ old = node.variable.name
776
+ if old in clashing:
777
+ new = fresh_variable_like(old, names)
778
+ names.add(new)
779
+ inner = {**scope, old: new}
780
+ else:
781
+ new = old
782
+ inner = {name: image for name, image in scope.items() if name != old}
783
+ return replace(node, variable=Variable(new), formula=rename(node.formula, inner))
784
+ return node.map_children(lambda child: rename(child, scope))
785
+
786
+ return [rename(f, {}) for f in formulas]
787
+
788
+
789
+ # =============================================================================
790
+ # Formula rendering
791
+ # =============================================================================
792
+ #
793
+ # Three separate "child" contexts, per the module docstring's parenthesisation
794
+ # rule — each decides, for the specific node it is about to render, whether
795
+ # that node needs an extra pair of parens in THAT position:
796
+ #
797
+ # _render_conn_operand — an operand of a binary connective (And/Or/Xor/
798
+ # Implies/Iff): bare only for an Atom or a Not-of-Atom; everything else
799
+ # (another binary connective, a Quantifier/SortedQuantifier, or a
800
+ # Not-of-compound) is wrapped.
801
+ # _render_not_operand — Not's own operand: bare for an Atom, wrapped for
802
+ # anything else (including another Not, a connective, or a quantifier).
803
+ # _render_quant_body — a quantifier's body: a binary connective is
804
+ # wrapped (matches _render_conn_operand's blanket rule — this is the
805
+ # 'forall x:S . (P => Q)' shape); a nested Quantifier/SortedQuantifier,
806
+ # Atom, or Not stays bare, since quantifier scope (and Not's prefix
807
+ # scope) already extends maximally to the right.
808
+ #
809
+ # _render(node, default_sort) itself is always called BARE — at the root of
810
+ # formula_to_casl's argument, at the root of each to_casl_spec axiom/
811
+ # conjecture line, and internally wherever one of the three context helpers
812
+ # above has already decided not to wrap. No function here re-wraps its own
813
+ # return value; wrapping is applied exactly once, by whichever caller placed
814
+ # the node in a non-root position.
815
+
816
+ _CONN_SYMBOLS = {"And": "/\\", "Or": "\\/", "Implies": "=>", "Iff": "<=>"}
817
+
818
+
819
+ def _quant_keyword(qtype: str) -> str:
820
+ """Map a Quantifier/SortedQuantifier ``type`` field to its CASL keyword."""
821
+ if qtype in ("∀", "forall"):
822
+ return "forall"
823
+ if qtype in ("∃", "exists"):
824
+ return "exists"
825
+ raise ValueError(f"CASL export: unknown quantifier type {qtype!r}.")
826
+
827
+
828
+ def _render_term(node: Node) -> str:
829
+ """Render a term node (Variable / Constant / SortedConstant / Function)."""
830
+ cls = type(node).__name__
831
+ if cls in ("Variable", "Constant", "SortedConstant"):
832
+ return node.name
833
+ if cls == "Function":
834
+ if not node.args:
835
+ # A 0-ary operation is declared like a constant (ops f : S) and
836
+ # must be REFERENCED bare too — 'f()' is not valid CASL term
837
+ # syntax (review-confirmed; mirrors _render_atom's 0-ary rule).
838
+ return node.name
839
+ args = ", ".join(_render_term(a) for a in node.args)
840
+ return f"{node.name}({args})"
841
+ raise NotImplementedError(
842
+ f"CASL export: term node {cls!r} is outside the classical FOL/MSFOL "
843
+ "fragment this exporter supports; non-classical logics may later go "
844
+ "through DOL."
845
+ )
846
+
847
+
848
+ def _render_atom(node: Node) -> str:
849
+ """Render an Atom: infix equality, a 0-ary bare predicate name, or an
850
+ applied predicate (the truth constants render as ``true`` / ``false``)."""
851
+ if truth_value(node) is not None:
852
+ return "true" if truth_value(node) else "false"
853
+ if node.predicate == "=":
854
+ if len(node.args) != 2:
855
+ raise ValueError(
856
+ "CASL export: equality atom must have exactly 2 arguments, "
857
+ f"got {len(node.args)}."
858
+ )
859
+ return f"{_render_term(node.args[0])} = {_render_term(node.args[1])}"
860
+ if not node.args:
861
+ return node.predicate
862
+ args = ", ".join(_render_term(a) for a in node.args)
863
+ return f"{node.predicate}({args})"
864
+
865
+
866
+ def _render(node: Node, default_sort: str) -> str:
867
+ """Render ``node`` bare (no self-wrapping) as CASL formula text."""
868
+ cls = type(node).__name__
869
+
870
+ if cls == "Atom":
871
+ return _render_atom(node)
872
+
873
+ if cls == "Not":
874
+ return f"not {_render_not_operand(node.formula, default_sort)}"
875
+
876
+ if cls in _CONN_SYMBOLS:
877
+ left = _render_conn_operand(node.left, default_sort)
878
+ right = _render_conn_operand(node.right, default_sort)
879
+ return f"{left} {_CONN_SYMBOLS[cls]} {right}"
880
+
881
+ if cls == "Xor":
882
+ # Exact classical reading: P XOR Q == not (P <=> Q). See the module
883
+ # docstring's "Formula emission rules".
884
+ left = _render_conn_operand(node.left, default_sort)
885
+ right = _render_conn_operand(node.right, default_sort)
886
+ return f"not ({left} <=> {right})"
887
+
888
+ if cls == "Quantifier":
889
+ body = _render_quant_body(node.formula, default_sort)
890
+ return (f"{_quant_keyword(node.type)} {node.variable.name} : "
891
+ f"{default_sort} . {body}")
892
+
893
+ if cls == "SortedQuantifier":
894
+ body = _render_quant_body(node.formula, default_sort)
895
+ return (f"{_quant_keyword(node.type)} {node.variable.name} : "
896
+ f"{node.sort} . {body}")
897
+
898
+ raise NotImplementedError(
899
+ f"CASL export: formula node {cls!r} is outside the classical "
900
+ "FOL/MSFOL fragment this exporter supports; non-classical logics "
901
+ "may later go through DOL."
902
+ )
903
+
904
+
905
+ def _render_conn_operand(node: Node, default_sort: str) -> str:
906
+ """Render ``node`` as the operand of a binary connective (And/Or/Xor/
907
+ Implies/Iff): bare for an Atom or a Not-of-Atom, else wrapped."""
908
+ cls = type(node).__name__
909
+ if cls == "Atom" or (cls == "Not" and type(node.formula).__name__ == "Atom"):
910
+ return _render(node, default_sort)
911
+ return f"({_render(node, default_sort)})"
912
+
913
+
914
+ def _render_not_operand(node: Node, default_sort: str) -> str:
915
+ """Render ``node`` as Not's own operand: bare for an Atom, else wrapped."""
916
+ if type(node).__name__ == "Atom":
917
+ return _render(node, default_sort)
918
+ return f"({_render(node, default_sort)})"
919
+
920
+
921
+ def _render_quant_body(node: Node, default_sort: str) -> str:
922
+ """Render ``node`` as a quantifier's body: a binary connective (And/Or/
923
+ Xor/Implies/Iff) is wrapped; everything else (Atom, Not, a nested
924
+ Quantifier/SortedQuantifier) stays bare."""
925
+ if type(node).__name__ in _CONN_SYMBOLS or type(node).__name__ == "Xor":
926
+ return f"({_render(node, default_sort)})"
927
+ return _render(node, default_sort)
928
+
929
+
930
+ # =============================================================================
931
+ # Declaration synthesis (to_casl_spec only)
932
+ # =============================================================================
933
+
934
+ def _render_op_type(arg_sorts: List[str], result_sort: str) -> str:
935
+ """Render an ``ops`` entry's type: ``Result`` for a 0-ary function
936
+ (indistinguishable in shape from a constant's own type), else
937
+ ``Sort1 * … * Sortn -> Result``."""
938
+ if not arg_sorts:
939
+ return result_sort
940
+ return " * ".join(arg_sorts) + " -> " + result_sort
941
+
942
+
943
+ def _render_pred_type(arg_sorts: List[str]) -> str:
944
+ """Render a ``preds`` entry's type: ``()`` for a 0-ary predicate, else
945
+ ``Sort1 * … * Sortn``."""
946
+ if not arg_sorts:
947
+ return "()"
948
+ return " * ".join(arg_sorts)
949
+
950
+
951
+ def _render_block(keyword: str, entries: List[Tuple[str, str]]) -> str:
952
+ """Render one ``ops``/``preds`` block: `` <keyword> name : type;`` for
953
+ the first entry, each further entry on its own line indented to align
954
+ under the first entry's own column, every non-final entry ending in
955
+ ``;``. ``entries`` must already be sorted by name."""
956
+ prefix = f" {keyword} "
957
+ cont_indent = " " * len(prefix)
958
+ last = len(entries) - 1
959
+ lines = []
960
+ for i, (name, type_str) in enumerate(entries):
961
+ text = f"{name} : {type_str}"
962
+ if i != last:
963
+ text += ";"
964
+ lines.append((prefix if i == 0 else cont_indent) + text)
965
+ return "\n".join(lines)
966
+
967
+
968
+ # =============================================================================
969
+ # Public API
970
+ # =============================================================================
971
+
972
+ def formula_to_casl(formula: Node, *, default_sort: str = "Thing",
973
+ visible_symbols: Iterable[str] = ()) -> str:
974
+ """Render a single closed formula as bare CASL formula text, without a
975
+ ``spec`` wrapper.
976
+
977
+ This is the same rendering :func:`to_casl_spec` uses for each of its
978
+ ``. <formula>`` lines, for embedding into hand-written CASL.
979
+
980
+ Runs the full validation pipeline first (see the module docstring's
981
+ "Sort inference" and "Reserved words" sections): fragment check, free-
982
+ variable check, arity/name-clash checks, and sort-conflict detection —
983
+ even though the resolved sorts themselves are not needed to render a
984
+ bare formula (only :class:`SortedQuantifier`'s own literal sort and
985
+ ``default_sort`` ever appear in the text), the checks still apply,
986
+ because CASL text with a free variable, a reserved-word identifier, or
987
+ an unsatisfiable sort constraint is not valid CASL either way.
988
+
989
+ A bound variable that is spelled like a symbol of the formula (a constant, a
990
+ function, a predicate or a sort it names, or ``default_sort``) is renamed, as in
991
+ :func:`to_casl_spec`. ``visible_symbols`` names the symbols of the hand-written CASL
992
+ the text is embedded into, which this function cannot see: a bound variable is
993
+ never given the spelling of one of them either.
994
+
995
+ Raises:
996
+ NotImplementedError: ``formula`` contains a node outside the
997
+ classical FOL/MSFOL fragment (see the module docstring's
998
+ "Scope" section).
999
+ ValueError: a free variable, a reserved-word identifier, an arity
1000
+ or constant/function-vs-function conflict, an unsatisfiable
1001
+ sort constraint, or a sort written in the formula that is spelled like
1002
+ ``default_sort`` while the default sort is used (the message names the
1003
+ keyword that picks another one).
1004
+ """
1005
+ sig = _analyze([formula], default_sort)
1006
+ [formula] = _bind_apart([formula], _specification_symbols(sig, visible_symbols))
1007
+ return _render(formula, default_sort)
1008
+
1009
+
1010
+ def to_casl_spec(
1011
+ axioms: Iterable[Node],
1012
+ *,
1013
+ conjectures: Iterable[Node] = (),
1014
+ spec_name: str = "KitExport",
1015
+ default_sort: str = "Thing",
1016
+ subsorts: Optional[Mapping[str, FrozenSet[str]]] = None,
1017
+ visible_symbols: Iterable[str] = (),
1018
+ ) -> str:
1019
+ """Render ``axioms`` and ``conjectures`` as one complete CASL basic spec:
1020
+ ``spec <spec_name> = … end``, Hets-parsable ASCII text.
1021
+
1022
+ ``axioms`` and ``conjectures`` share ONE signature: every predicate,
1023
+ function, and constant that appears anywhere across both is declared
1024
+ exactly once (sort inference — see the module docstring — runs over the
1025
+ whole combined batch). Each axiom becomes its own ``. <formula>`` line;
1026
+ each conjecture becomes its own ``. <formula> %implied`` line (the
1027
+ ``%implied`` annotation is what turns a formula into a Hets proof
1028
+ obligation rather than an assumed axiom). ``sorts``/``ops``/``preds``
1029
+ sections are emitted only when non-empty, sorted alphabetically by
1030
+ symbol name (``ops`` merges constants and functions into one list).
1031
+
1032
+ ``subsorts`` (default ``None``, no subsort declarations emitted) is the
1033
+ child-sort-to-DIRECT-parent-sorts mapping — the same shape as
1034
+ :attr:`~unicode_logic_kit.fol.signature.Signature.subsorts`, and typically
1035
+ passed as exactly that attribute — completing the round trip with
1036
+ :func:`unicode_logic_kit.fol.casl_import.parse_casl_spec`, which reads
1037
+ ``sort <child> < <parent>`` back into the identical mapping. Unlike
1038
+ every other declaration here, a subsort edge is NOT inferred from the
1039
+ formulas (nothing about ``S < T`` is derivable from how ``S``/``T`` are
1040
+ USED in an axiom), so both sort names in every edge are added to the
1041
+ ``sorts`` line even if neither appears in any formula, and both are
1042
+ checked against the CASL reserved-word list exactly like every other
1043
+ emitted sort name (see the module docstring's "Reserved words" section).
1044
+
1045
+ **Bound variables and the symbols of the spec.** CASL writes a bound variable and a
1046
+ constant as the same identifier, and the variable wins inside its quantifier, so a
1047
+ constant ``w`` under a quantifier that binds ``w`` would be read as the variable. A
1048
+ bound variable that is spelled like ANY symbol of the whole spec (a constant, function
1049
+ or predicate of any axiom or conjecture, a sort, ``default_sort``, a name in
1050
+ ``visible_symbols``) is therefore renamed to a fresh variable name (``w0``, ``w1``, …,
1051
+ fresh against every name the formulas hold), in its quantifier and in every
1052
+ occurrence it binds; names are compared exactly, as CASL does (``W`` and ``w`` do not
1053
+ clash). Text without such a clash is unchanged. The result reads back through
1054
+ :func:`~unicode_logic_kit.fol.casl_import.parse_casl_spec` as the same formula up to
1055
+ the names of the bound variables that had to be renamed (and is the same formula
1056
+ where none was). ``visible_symbols`` are the names of symbols the spec sees without
1057
+ declaring them (those of a spec it extends with ``then``); no formula is read for
1058
+ them, they only keep the binders away.
1059
+
1060
+ Raises:
1061
+ ValueError: neither ``axioms`` nor ``conjectures`` contains any
1062
+ formula; ``spec_name`` is not a simple identifier or collides
1063
+ with a CASL keyword; a sort name in ``subsorts`` is reserved; a sort
1064
+ written in the formulas or named in ``subsorts`` is spelled like
1065
+ ``default_sort`` while the default sort is used (the message names the
1066
+ keyword ``default_sort=`` that picks another one); or
1067
+ any of the sort-inference / identifier refusals documented on
1068
+ :func:`formula_to_casl` above.
1069
+ NotImplementedError: as :func:`formula_to_casl`.
1070
+ """
1071
+ _validate_spec_name(spec_name)
1072
+ axioms = list(axioms)
1073
+ conjectures = list(conjectures)
1074
+ all_formulas = axioms + conjectures
1075
+ if not all_formulas:
1076
+ raise ValueError(
1077
+ "CASL export: to_casl_spec needs at least one axiom or "
1078
+ "conjecture; an empty spec has no formulas to export."
1079
+ )
1080
+ subsorts = subsorts or {}
1081
+ subsort_edges: List[Tuple[str, str]] = sorted(
1082
+ (child, parent) for child, parents in subsorts.items() for parent in parents
1083
+ )
1084
+ for child, parent in subsort_edges:
1085
+ _check_reserved(child, "sort")
1086
+ _check_reserved(parent, "sort")
1087
+
1088
+ edge_sorts = [s for edge in subsort_edges for s in edge]
1089
+ sig = _analyze(all_formulas, default_sort, declared_sorts=edge_sorts)
1090
+ bound_apart = _bind_apart(all_formulas,
1091
+ _specification_symbols(sig, [*edge_sorts, *visible_symbols]))
1092
+ axioms, conjectures = bound_apart[:len(axioms)], bound_apart[len(axioms):]
1093
+ declared_sorts: Set[str] = set(sig.literal_sorts)
1094
+ for child, parent in subsort_edges:
1095
+ declared_sorts.add(child)
1096
+ declared_sorts.add(parent)
1097
+
1098
+ op_entries: List[Tuple[str, str]] = []
1099
+ for name in sorted(sig.func_names):
1100
+ arity = sig.func_arity[name]
1101
+ arg_sorts = [sig.uf.sort_of(("func", name, i), default_sort)
1102
+ for i in range(arity)]
1103
+ result_sort = sig.uf.sort_of(("func_result", name), default_sort)
1104
+ declared_sorts.update(arg_sorts)
1105
+ declared_sorts.add(result_sort)
1106
+ op_entries.append((name, _render_op_type(arg_sorts, result_sort)))
1107
+ for name in sorted(sig.const_names):
1108
+ const_sort = sig.uf.sort_of(("const", name), default_sort)
1109
+ declared_sorts.add(const_sort)
1110
+ op_entries.append((name, const_sort))
1111
+ op_entries.sort(key=lambda e: e[0])
1112
+
1113
+ pred_entries: List[Tuple[str, str]] = []
1114
+ for name in sorted(sig.pred_arity):
1115
+ arity = sig.pred_arity[name]
1116
+ arg_sorts = [sig.uf.sort_of(("pred", name, i), default_sort)
1117
+ for i in range(arity)]
1118
+ declared_sorts.update(arg_sorts)
1119
+ pred_entries.append((name, _render_pred_type(arg_sorts)))
1120
+
1121
+ lines = [f"spec {spec_name} ="]
1122
+ if declared_sorts:
1123
+ lines.append(" sorts " + ", ".join(sorted(declared_sorts)))
1124
+ for child, parent in subsort_edges:
1125
+ lines.append(f" sort {child} < {parent}")
1126
+ if op_entries:
1127
+ lines.append(_render_block("ops", op_entries))
1128
+ if pred_entries:
1129
+ lines.append(_render_block("preds", pred_entries))
1130
+ for f in axioms:
1131
+ lines.append(f" . {_render(f, default_sort)}")
1132
+ for f in conjectures:
1133
+ lines.append(f" . {_render(f, default_sort)} %implied")
1134
+ lines.append("end")
1135
+ return "\n".join(lines)