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,812 @@
1
+ """Classical FOL / MSFOL → HOL exporters: TPTP **THF** problems and Isabelle/HOL theories.
2
+
3
+ A classical first-order formula embeds into higher-order logic *trivially*: the
4
+ first-order fragment (TPTP ``fof``) is a syntactic subset of typed higher-order
5
+ logic (TPTP ``thf``), and likewise sits inside Isabelle/HOL over uninterpreted
6
+ constants and predicates. This module turns a toolkit :class:`~unicode_logic_kit.fol.nodes.Node`
7
+ into:
8
+
9
+ * a complete, self-contained TPTP **THF** problem — a single individual type, a
10
+ typed declaration for every predicate / function / constant, and the formula as
11
+ the ``conjecture`` (:func:`to_thf_fol`); and
12
+ * a loadable **Isabelle/HOL** theory — the same uninterpreted signature declared
13
+ with ``consts`` and the formula as a real ``lemma`` to be discharged by an
14
+ external prover / Sledgehammer (:func:`to_isabelle_fol`).
15
+
16
+ **MSFOL (many-sorted FOL).** Two routes map a many-sorted formula into single-sorted
17
+ HOL. The *typed* route would give each sort its own HOL type; the *guard-relativization*
18
+ route — the one implemented here for robustness — turns each sort into a unary guard
19
+ predicate and relativizes the quantifiers (``∀x:S φ ↦ ∀x. S(x) → φ``, ``∃x:S φ ↦
20
+ ∃x. S(x) ∧ φ``). That reduction is exactly the toolkit's
21
+ :func:`~unicode_logic_kit.fol.nodes.to_fol`, so the MSFOL exporters reuse it and then
22
+ emit the resulting plain-FOL formula. That reduction forgets two facts the many-sorted
23
+ reading carries — no sort is empty, and a sorted constant ``c:S`` is an element of ``S``
24
+ (:func:`~unicode_logic_kit.fol.nodes.sort_axioms`) — so the MSFOL exporters state them
25
+ (``include_sort_facts=True``, the default). They are stated OUTSIDE the conjecture: a
26
+ THF ``axiom`` line per fact, Isabelle hypotheses ``⟦…⟧ ⟹ φ`` of the lemma, a hypothesis
27
+ of the Lean theorem. A fact conjoined to the goal itself (``Human(socrates) ∧ φ``) could
28
+ never be proved, even for a tautology ``φ``, because the guard is an uninterpreted
29
+ predicate. Only a formula that is itself ASSERTED (``conjecture=False``) keeps the
30
+ membership facts as a conjunct — there they are part of what is asserted — and gets the
31
+ non-emptiness facts as separate ``axiom`` lines. ``include_sort_facts=False`` is the bare
32
+ relativisation, with no sort facts at all.
33
+
34
+ **Equality.** By default, to stay consistent with the rest of the toolkit's HOL layer
35
+ (``qml.to_thf_modal``), ``=`` / ``≠`` are emitted as *uninterpreted* binary predicates
36
+ (``feq`` / ``fneq``), **not** primitive HOL identity. Pass ``native_equality=True`` to
37
+ :func:`to_thf_fol` / :func:`to_isabelle_fol` (and the MSFOL wrappers) to emit ``=`` /
38
+ ``≠`` as the target logic's own built-in identity instead — TPTP THF's native infix
39
+ ``=`` / ``!=`` and Isabelle/HOL's polymorphic ``=`` / ``\\<noteq>`` are both genuine,
40
+ axiom-free HOL identity at every type (including the uninterpreted individual type
41
+ ``$i`` / ``i``), so this is a *rendering* choice, not new machinery: no congruence or
42
+ reflexivity axiom is generated or needed, because the target format's own ``=``
43
+ already validates them. The comparison predicates ``<`` ``>`` ``≤`` ``≥`` are
44
+ unaffected by this flag and always stay uninterpreted (``flt``/``fgt``/``fle``/``fge``)
45
+ — they have no built-in counterpart in either target. The default remains ``False``
46
+ (uninterpreted ``feq``/``fneq``) so existing output is unchanged; if you want genuine
47
+ HOL identity without the flag, post-process the output or add the congruence/identity
48
+ axioms yourself.
49
+
50
+ **Honesty / scope.** This module only *emits* problems and theories; it does **not**
51
+ run Leo-III, Satallax, Vampire, Isabelle, or Sledgehammer. Classical first-order
52
+ logic is **semi-decidable only** (validity is recursively enumerable; invalidity is
53
+ not), and the HOL embedding inherits that — a prover may confirm a valid conjecture
54
+ but is not guaranteed to terminate on an invalid one. No claim of decision is made:
55
+ :func:`to_thf_fol` / :func:`to_isabelle_fol` produce a *sound* problem an external
56
+ prover may discharge, nothing more.
57
+
58
+ Public API: :func:`to_thf_fol`, :func:`to_isabelle_fol`, :func:`to_thf_msfol`,
59
+ :func:`to_isabelle_msfol`.
60
+ """
61
+
62
+ from typing import Dict, List, Sequence, Tuple
63
+
64
+ from ..fol._fol_nodes import constant_name_to_ascii
65
+ from ..fol._numeral_symbols import numerals_as_constants, prefixed_numeral_name
66
+ from ..fol._symbol_names import dedupe
67
+ from ..fol._truth_constants import truth_value
68
+ from ..fol._msfl_nodes import _reduce_nl_nodes
69
+ from ..fol.nodes import (
70
+ Node, Variable, Constant, Number, Function, Measure,
71
+ Atom, Not, And, Or, Xor, Implies, Iff, Quantifier,
72
+ SortedQuantifier, to_fol, sort_axioms, nonempty_sort_axioms,
73
+ )
74
+
75
+ # Equality / inequality are uninterpreted binary predicates in this toolkit's HOL
76
+ # layer (NOT primitive HOL identity), matching qml.to_thf_modal. These aliases give
77
+ # them valid, distinct functor / constant names in THF and Isabelle.
78
+ _PRED_ALIAS = {"=": "feq", "≠": "fneq", "<": "flt", ">": "fgt", "≤": "fle", "≥": "fge"}
79
+
80
+ # The two symbols native_equality=True renders as the target logic's own built-in
81
+ # identity (binary use only — a same-named symbol at any OTHER arity is left as an
82
+ # ordinary uninterpreted predicate via _PRED_ALIAS, same as when the flag is off).
83
+ _NATIVE_EQ_NAMES = {"=", "≠"}
84
+
85
+ _FORALL = "∀"
86
+ _EXISTS = "∃"
87
+
88
+
89
+ def _is_native_eq(name: str, arity: int, native_equality: bool) -> bool:
90
+ """True if ``(name, arity)`` is rendered as the target's built-in identity.
91
+
92
+ Only ``=``/``≠`` at their natural binary arity qualify — this bypasses the
93
+ ``feq``/``fneq`` alias (no declaration, no functor application) in favour of
94
+ THF's native infix ``=``/``!=`` or Isabelle's polymorphic ``=``/``\\<noteq>``.
95
+ """
96
+ return native_equality and name in _NATIVE_EQ_NAMES and arity == 2
97
+
98
+
99
+ # ===========================================================================
100
+ # Shared: name sanitisation and signature extraction
101
+ # ===========================================================================
102
+
103
+ def _sanitize(name: str) -> str:
104
+ """Lower-case-initial ASCII alphanumeric/underscore functor stem for THF/Isabelle.
105
+
106
+ Equality/comparison glyphs map through ``_PRED_ALIAS``. Every other name is
107
+ FIRST transliterated to ASCII with :func:`~unicode_logic_kit.fol._fol_nodes
108
+ .constant_name_to_ascii` — the same transliteration ``Constant.to_tptp`` /
109
+ ``to_prover9`` already use (ASCII passthrough; a Greek letter maps to its
110
+ conventional name; any other non-ASCII character becomes a reversible
111
+ ``uXXXX`` codepoint escape) — and only THEN run through the legacy
112
+ alnum-or-underscore filter. Since ``constant_name_to_ascii`` is the identity
113
+ on a string that is already pure ASCII, this changes nothing for a name that
114
+ was already legal: the underscore/digit-prefix behaviour below is exactly
115
+ what it was before. What it fixes is that ``str.isalnum()`` is ``True`` for
116
+ almost every Unicode letter (``ś``, ``中``, …), so the OLD filter alone let
117
+ those straight through as "harmless" — THF/Isabelle are ASCII-only formats,
118
+ so that was silent corruption, not sanitisation. A leading digit (which can
119
+ now also arise from a transliterated escape, though those always start with
120
+ the ASCII letter ``u``) is still prefixed with ``p`` so the result is a
121
+ legal lower identifier, and so is a leading underscore: the constant
122
+ ``'_sk0'``, the constant ``'-3'`` and the function ``+`` would otherwise be
123
+ written ``_sk0``, ``_3`` and ``_``, which neither THF nor Isabelle reads as
124
+ an identifier.
125
+
126
+ NOTE: this is *not* injective — neither the old filter (``'Ab'``/``'ab'``
127
+ collide) nor ``constant_name_to_ascii`` (a literal ``'theta'`` and the Greek
128
+ ``'θ'`` both fold to ``'theta'``) guarantee that. De-collision is the job of
129
+ :class:`_SymbolResolver` (predicates/functions/constants) and
130
+ :class:`_VarResolver` (variables), which route every declaration AND usage
131
+ through :func:`~unicode_logic_kit.fol._symbol_names.dedupe` to a unique, valid
132
+ identifier per symbol. Do not emit ``_sanitize`` output directly for a
133
+ declaration or a usage.
134
+ """
135
+ if name in _PRED_ALIAS:
136
+ return _PRED_ALIAS[name]
137
+ ascii_name = constant_name_to_ascii(name)
138
+ safe = "".join(c if (c.isalnum() or c == "_") else "_" for c in ascii_name)
139
+ if not safe:
140
+ return "p"
141
+ if safe[0].isdigit() or safe[0] == "_":
142
+ safe = "p" + safe
143
+ return safe[:1].lower() + safe[1:]
144
+
145
+
146
+ # Backwards-compatible alias (older call sites / tests may reference ``_safe_name``).
147
+ _safe_name = _sanitize
148
+
149
+
150
+ def _signature(formula: Node):
151
+ """Collect (predicates, functions, constants) with arities from ``formula``.
152
+
153
+ Returns ``(preds, funcs, consts)`` where ``preds`` and ``funcs`` are sets of
154
+ ``(name, arity)`` pairs — a predicate or function used at *two* arities yields
155
+ *two* distinct entries (each a distinct emitted symbol) — and ``consts`` is a
156
+ set of nullary individual names (plain constants plus numeric literals, the
157
+ latter rendered ``n<value>``). Equality / comparison atoms contribute their
158
+ glyph as an ordinary predicate.
159
+ """
160
+ preds, funcs, consts = set(), set(), set()
161
+ for n in formula.walk():
162
+ if isinstance(n, Atom):
163
+ if truth_value(n) is None: # `$true` / `$false` are written as THF's / HOL's own
164
+ preds.add((n.predicate, len(n.args)))
165
+ elif isinstance(n, Function):
166
+ funcs.add((n.name, len(n.args)))
167
+ elif isinstance(n, Measure):
168
+ # μ(entity, dimension) is the binary function ``measure`` — the same
169
+ # symbol Measure.to_z3 / to_prover9 / to_tptp emit.
170
+ funcs.add(("measure", 2))
171
+ elif isinstance(n, Constant):
172
+ consts.add(n.name)
173
+ elif isinstance(n, Number):
174
+ consts.add("n" + str(n.value))
175
+ return preds, funcs, consts
176
+
177
+
178
+ # ===========================================================================
179
+ # Global symbol resolver — DISTINCT source symbols map to DISTINCT identifiers
180
+ # ===========================================================================
181
+
182
+ # Category tags. Every emitted symbol is keyed by (category, raw_name, arity);
183
+ # predicates and functions carry their arity so the SAME raw name used at two
184
+ # arities becomes two distinct, distinctly-named emitted symbols.
185
+ _CAT_PRED = "pred"
186
+ _CAT_FUNC = "func"
187
+ _CAT_CONST = "const"
188
+
189
+
190
+ class _SymbolResolver:
191
+ """Assign a UNIQUE, valid emitted identifier to every source symbol.
192
+
193
+ Keyed by ``(category, raw_name, arity)``. Distinct source symbols — whether
194
+ they differ by category (a predicate vs. a function vs. a constant sharing a
195
+ raw name), by raw name, or by arity (a predicate used at two arities) — are
196
+ guaranteed distinct emitted identifiers. The ``_PRED_ALIAS`` targets
197
+ (``feq``/``fneq``/…) are reserved up front so a user symbol named ``feq``
198
+ cannot collide with the ``=`` alias.
199
+
200
+ Both *declarations* and *usages* must go through :meth:`name`, so a symbol is
201
+ declared and referenced under exactly the same (de-collided) identifier.
202
+
203
+ ``native_equality=True`` makes this resolver skip reserving ``feq``/``fneq``
204
+ for the binary ``=``/``≠`` entries (they render as native identity and need no
205
+ identifier at all), which also frees those stems for a user symbol literally
206
+ named ``feq``/``fneq`` — there is no longer an alias target to collide with.
207
+ """
208
+
209
+ def __init__(self, formula: Node, native_equality: bool = False):
210
+ self._used = set()
211
+ self._map: Dict[Tuple[str, str, int], str] = {}
212
+ preds, funcs, consts = _signature(formula)
213
+ # Assign the equality/comparison alias predicates (=, ≠, …) FIRST so they
214
+ # claim their natural alias names (feq, fneq, …); any user symbol that
215
+ # would sanitise to the same stem is then de-collided away from them. This
216
+ # makes the alias targets collision-proof while keeping = -> feq when there
217
+ # is no competing user 'feq'. Skip this for a (name, arity) that
218
+ # native_equality renders natively — it needs no alias/identifier.
219
+ for name, arity in sorted(preds):
220
+ if name in _PRED_ALIAS and not _is_native_eq(name, arity, native_equality):
221
+ self._assign(_CAT_PRED, name, arity)
222
+ # Deterministic assignment order so emitted problems are stable.
223
+ for name, arity in sorted(preds):
224
+ if name not in _PRED_ALIAS:
225
+ self._assign(_CAT_PRED, name, arity)
226
+ for name, arity in sorted(funcs):
227
+ self._assign(_CAT_FUNC, name, arity)
228
+ for name in sorted(consts):
229
+ self._assign(_CAT_CONST, name, 0)
230
+
231
+ def _assign(self, category: str, raw: str, arity: int) -> str:
232
+ key = (category, raw, arity)
233
+ if key in self._map:
234
+ return self._map[key]
235
+ cand = dedupe(_sanitize(raw), self._used)
236
+ self._map[key] = cand
237
+ return cand
238
+
239
+ def name(self, category: str, raw: str, arity: int) -> str:
240
+ """Return the unique emitted identifier for ``(category, raw, arity)``.
241
+
242
+ Assigns one on demand (covers any symbol not pre-seeded, e.g. an alias
243
+ target) so declarations and usages always agree.
244
+ """
245
+ key = (category, raw, arity)
246
+ if key not in self._map:
247
+ return self._assign(category, raw, arity)
248
+ return self._map[key]
249
+
250
+
251
+ class _VarResolver:
252
+ """Map DISTINCT bound/free variable names to DISTINCT emitted variable tokens.
253
+
254
+ THF upper-cases variable tokens, so distinct source names that differ only in
255
+ case (``x`` vs. ``X``) would otherwise collide on one token. De-collide by
256
+ suffixing while preserving the natural token for the first claimant.
257
+
258
+ ``used`` is the set of tokens a variable must not take. THF needs none (a variable is
259
+ upper-case and a functor lower-case, so the two never meet). Isabelle needs the
260
+ functor tokens: its binder ``\\<forall> x.`` shadows a constant ``x`` in its own
261
+ scope, so a variable spelled like a constant of the theory would capture it. The
262
+ set is shared, not copied: a symbol that the functor resolver names later is kept
263
+ off the variables' tokens as well.
264
+ """
265
+
266
+ def __init__(self, render, used=None):
267
+ self._render = render # raw_name -> base token
268
+ self._used = set() if used is None else used
269
+ self._map: Dict[str, str] = {}
270
+
271
+ def token(self, raw: str) -> str:
272
+ if raw in self._map:
273
+ return self._map[raw]
274
+ cand = dedupe(self._render(raw), self._used)
275
+ self._map[raw] = cand
276
+ return cand
277
+
278
+
279
+ def _free_variables(formula: Node) -> List[str]:
280
+ """Return the names of variables occurring free in ``formula`` (first-occurrence order).
281
+
282
+ Quantifier / SortedQuantifier bind their variable; everything else is structural.
283
+ """
284
+ out: List[str] = []
285
+ seen = set()
286
+
287
+ def rec(node: Node, bound: frozenset):
288
+ if isinstance(node, Variable):
289
+ if node.name not in bound and node.name not in seen:
290
+ seen.add(node.name)
291
+ out.append(node.name)
292
+ return
293
+ if isinstance(node, (Quantifier, SortedQuantifier)):
294
+ rec(node.formula, bound | {node.variable.name})
295
+ return
296
+ for child in node._child_nodes():
297
+ rec(child, bound)
298
+
299
+ rec(formula, frozenset())
300
+ return out
301
+
302
+
303
+ # ===========================================================================
304
+ # (A) TPTP THF export
305
+ # ===========================================================================
306
+
307
+ def _thf_term(node: Node, syms: "_SymbolResolver", vars_: "_VarResolver") -> str:
308
+ """Render an individual (``$i``) term in THF applicative (``@``) form."""
309
+ if isinstance(node, Variable):
310
+ return vars_.token(node.name)
311
+ if isinstance(node, Constant):
312
+ return syms.name(_CAT_CONST, node.name, 0)
313
+ if isinstance(node, Number):
314
+ return syms.name(_CAT_CONST, "n" + str(node.value), 0)
315
+ if isinstance(node, Function):
316
+ head = syms.name(_CAT_FUNC, node.name, len(node.args))
317
+ return "( " + " @ ".join([head] + [_thf_term(a, syms, vars_) for a in node.args]) + " )"
318
+ if isinstance(node, Measure):
319
+ head = syms.name(_CAT_FUNC, "measure", 2)
320
+ return ("( " + " @ ".join([head, _thf_term(node.entity, syms, vars_),
321
+ _thf_term(node.dimension, syms, vars_)]) + " )")
322
+ raise NotImplementedError(
323
+ f"to_thf_fol: unsupported term {type(node).__name__} (classical FOL terms only)."
324
+ )
325
+
326
+
327
+ def _thf_formula(node: Node, syms: "_SymbolResolver", vars_: "_VarResolver",
328
+ native_equality: bool = False) -> str:
329
+ """Render a classical FOL formula as a THF ``$o`` term.
330
+
331
+ Connectives use the THF/FOF operators (``~ & | => <=> <~>``); quantifiers bind
332
+ individual variables ``! [X: $i]`` / ``? [X: $i]``; atoms apply their declared
333
+ predicate constant with ``@``. Every predicate / function / constant / variable
334
+ name is routed through the resolvers so distinct source symbols stay distinct.
335
+ Anything outside the classical fragment (modal, second-order, Łukasiewicz,
336
+ lambda) raises ``NotImplementedError``.
337
+
338
+ With ``native_equality=True``, a binary ``=``/``≠`` atom renders as THF's own
339
+ infix ``( a = b )`` / ``( a != b )`` instead of applying the ``feq``/``fneq``
340
+ functor — genuine HOL identity, built in at every type, no declaration needed.
341
+ """
342
+ def f(n):
343
+ return _thf_formula(n, syms, vars_, native_equality)
344
+ if isinstance(node, Atom):
345
+ constant = truth_value(node)
346
+ if constant is not None:
347
+ return "$true" if constant else "$false" # THF's own constants
348
+ if _is_native_eq(node.predicate, len(node.args), native_equality):
349
+ op = "=" if node.predicate == "=" else "!="
350
+ left = _thf_term(node.args[0], syms, vars_)
351
+ right = _thf_term(node.args[1], syms, vars_)
352
+ return f"( {left} {op} {right} )"
353
+ head = syms.name(_CAT_PRED, node.predicate, len(node.args))
354
+ if not node.args:
355
+ return head
356
+ return "( " + " @ ".join([head] + [_thf_term(a, syms, vars_) for a in node.args]) + " )"
357
+ if isinstance(node, Not):
358
+ return f"( ~ {f(node.formula)} )"
359
+ if isinstance(node, And):
360
+ return f"( {f(node.left)} & {f(node.right)} )"
361
+ if isinstance(node, Or):
362
+ return f"( {f(node.left)} | {f(node.right)} )"
363
+ if isinstance(node, Implies):
364
+ return f"( {f(node.left)} => {f(node.right)} )"
365
+ if isinstance(node, Iff):
366
+ return f"( {f(node.left)} <=> {f(node.right)} )"
367
+ if isinstance(node, Xor):
368
+ return f"( {f(node.left)} <~> {f(node.right)} )"
369
+ if isinstance(node, Quantifier):
370
+ x = vars_.token(node.variable.name)
371
+ binder = "!" if node.type in (_FORALL, "forall") else "?"
372
+ return f"( {binder} [{x}: $i] : {f(node.formula)} )"
373
+ raise NotImplementedError(
374
+ f"to_thf_fol: {type(node).__name__} is outside the classical FOL fragment "
375
+ "supported by the THF export (no modal / second-order / Łukasiewicz / "
376
+ "substructural / lambda). Modal family → hol.thf_modal.to_thf_modal_full; "
377
+ "second-order → hol.secondorder.to_thf_so; K3/LP → hol.manyvalued; "
378
+ "ILL/Lambek derivations → hol.isabelle_substructural."
379
+ )
380
+
381
+
382
+ def _thf_signature_decls(formula: Node, syms: "_SymbolResolver",
383
+ native_equality: bool = False) -> List[str]:
384
+ """Type declarations for every predicate / function / constant in ``formula``.
385
+
386
+ Each declaration uses the resolver-assigned identifier, so a symbol is
387
+ declared under exactly the name its usages reference, and distinct symbols
388
+ (including a predicate at two arities) get distinct, single-typed decls.
389
+ A binary ``=``/``≠`` skips its declaration when ``native_equality=True`` —
390
+ THF's built-in identity is polymorphic and needs no type declaration.
391
+ """
392
+ preds, funcs, consts = _signature(formula)
393
+ decls: List[str] = []
394
+ for name, arity in sorted(preds):
395
+ if _is_native_eq(name, arity, native_equality):
396
+ continue
397
+ ident = syms.name(_CAT_PRED, name, arity)
398
+ typ = " > ".join(["$i"] * arity + ["$o"]) if arity else "$o"
399
+ decls.append(f"thf({ident}_decl, type, ( {ident} : ( {typ} ) )).")
400
+ for name, arity in sorted(funcs):
401
+ ident = syms.name(_CAT_FUNC, name, arity)
402
+ typ = " > ".join(["$i"] * (arity + 1))
403
+ decls.append(f"thf({ident}_decl, type, ( {ident} : ( {typ} ) )).")
404
+ for name in sorted(consts):
405
+ ident = syms.name(_CAT_CONST, name, 0)
406
+ decls.append(f"thf({ident}_decl, type, ( {ident} : $i )).")
407
+ return decls
408
+
409
+
410
+ def to_thf_fol(formula: Node, conjecture: bool = True, native_equality: bool = False) -> str:
411
+ """Emit a complete TPTP **THF** problem for a classical FOL ``formula``.
412
+
413
+ The problem declares the individual type ``$i`` implicitly (TPTP's built-in),
414
+ a typed constant for every predicate / function / constant in the signature,
415
+ and the formula itself. With ``conjecture=True`` (default) the formula is the
416
+ ``conjecture`` — a higher-order ATP (Leo-III, Satallax) reports ``Theorem`` iff
417
+ the formula is valid; with ``conjecture=False`` it is emitted as an ``axiom``
418
+ (e.g. to assert it as a hypothesis in a larger problem).
419
+
420
+ A free variable of a CONJECTURE is closed universally before emission (TPTP formula
421
+ roles do not admit free variables). A free variable is a parameter of the problem, one
422
+ unknown individual, and for a single formula with no premise the two readings
423
+ coincide: the formula is valid for the parameter iff it is valid for every individual.
424
+ An ASSERTED formula (``conjecture=False``) is a premise of a larger problem, and there
425
+ the closure would say more than the formula says: ``∀x P(x)`` entails ``P(a)``, the
426
+ premise ``P(x)`` does not. So an asserted formula with a free variable is refused by
427
+ name; state the parameter with a constant, or bind the variable with a quantifier.
428
+
429
+ By default, equality ``=`` / ``≠`` becomes the uninterpreted predicate ``feq`` /
430
+ ``fneq`` (see module docstring);
431
+ pass ``native_equality=True`` to instead emit THF's own built-in infix
432
+ ``=`` / ``!=`` — genuine HOL identity, so congruence/substitutivity for every
433
+ declared function and predicate is free (no axioms to add). The comparison
434
+ predicates ``<`` ``>`` ``≤`` ``≥`` are unaffected either way.
435
+
436
+ Classical FOL is *semi-decidable only*: a prover may confirm a valid conjecture
437
+ but is not guaranteed to terminate otherwise. This function only emits the
438
+ problem; it does not run any prover.
439
+
440
+ Raises:
441
+ NotImplementedError: ``conjecture`` is false and ``formula`` has a free variable;
442
+ or a numeral and a constant are spelled alike.
443
+ """
444
+ return _thf_problem(formula, conjecture, native_equality)
445
+
446
+
447
+ def _scope(formula: Node, background: Sequence[Node]) -> Node:
448
+ """``formula`` and every background fact as one tree, for signature scans only.
449
+
450
+ A problem's declarations and symbol names must cover the sort facts it states
451
+ as well as its formula — a sort that occurs only through a sorted constant is
452
+ mentioned by the fact alone. With no background this is ``formula`` itself, so
453
+ a problem without sort facts is resolved and declared exactly as it was.
454
+ """
455
+ scope = formula
456
+ for fact in background:
457
+ scope = And(scope, fact)
458
+ return scope
459
+
460
+
461
+ def _refuse_open_assertion(formula: Node, conjecture: bool, where: str) -> None:
462
+ """Refuse an ASSERTED ``formula`` that has a free variable, by name.
463
+
464
+ A free variable is a parameter of the problem: one unknown individual, the same in
465
+ every formula. Closing it universally is the same thing for a conjecture that stands
466
+ alone (valid for the parameter iff valid for every individual), but an axiom is a
467
+ premise of a larger problem, and ``∀x P(x)`` says more than ``P(x)`` does. So the
468
+ writer states no closure for an axiom and tells the caller how to state the parameter.
469
+ ``where`` is the name of the writer, which the refusal opens with.
470
+
471
+ Raises:
472
+ NotImplementedError: ``conjecture`` is false and ``formula`` has a free variable.
473
+ """
474
+ if conjecture:
475
+ return
476
+ free = _free_variables(formula)
477
+ if free:
478
+ names = ", ".join(repr(name) for name in free)
479
+ noun = "variable" if len(free) == 1 else "variables"
480
+ raise NotImplementedError(
481
+ f"{where}: the asserted formula (conjecture=False) has the free {noun} "
482
+ f"{names}, and an axiom cannot say what a free variable stands for. Closing it "
483
+ "universally would assert more than the formula says (a free variable is a "
484
+ "PARAMETER of the problem, one unknown individual shared by every formula: "
485
+ "'∀x P(x)' entails 'P(a)', the premise 'P(x)' does not). State the parameter "
486
+ "yourself: replace the variable by a constant, or bind it with a quantifier, "
487
+ "or emit the formula as the conjecture (conjecture=True), where, for one "
488
+ "formula with no premise, the closure and the parameter reading coincide.")
489
+
490
+
491
+ def _thf_problem(formula: Node, conjecture: bool, native_equality: bool,
492
+ background: Sequence[Node] = (), where: str = "to_thf_fol") -> str:
493
+ """The THF problem of ``formula``, with ``background`` as ``axiom`` lines before it.
494
+
495
+ ``background`` are closed sentences (the many-sorted reading's sort facts, see
496
+ :func:`_msfol_split`). They are named ``nonempty_sort_<i>`` (an ``∃``) and
497
+ ``sort_member_<i>`` (an atom) and come after the type declarations, so a prover
498
+ can use them but never has to prove them. ``where`` names the public writer that
499
+ is calling, for the refusal of an asserted formula that has a free variable.
500
+ """
501
+ formula = _reduce_nl_nodes(formula) # Contrast → ∧, Count → witnesses
502
+ # A numeral is a constant identified by its value (1 and 1.0 are one), named ``n1``:
503
+ # a user constant spelled like it is refused, not merged with it.
504
+ [formula], _ = numerals_as_constants([formula], where="to_thf_fol",
505
+ spell=prefixed_numeral_name)
506
+ _refuse_open_assertion(formula, conjecture, where)
507
+ closed = formula
508
+ for name in reversed(_free_variables(formula)):
509
+ closed = Quantifier(_FORALL, Variable(name), closed)
510
+ role = "conjecture" if conjecture else "axiom"
511
+ scope = _scope(closed, background)
512
+ syms = _SymbolResolver(scope, native_equality=native_equality)
513
+ vars_ = _VarResolver(lambda raw: _sanitize(raw).upper())
514
+ lines = [
515
+ "% Classical FOL embedded into THF (first-order fragment of HOL).",
516
+ f"% The formula is emitted as the {role}; '$i' is the individual type.",
517
+ ]
518
+ # Only the comment differs, and only when '=' / '≠' actually occur at their
519
+ # native binary arity — a formula without them emits byte-identical output
520
+ # whether native_equality is True or False (nothing about it would differ).
521
+ preds, _, _ = _signature(scope)
522
+ if any(_is_native_eq(n, a, native_equality) for n, a in preds):
523
+ lines.append("% '=' / '≠' are THF's native, built-in HOL identity (no axioms needed).")
524
+ else:
525
+ lines.append("% '=' / '≠' are uninterpreted predicates (feq / fneq), not HOL identity.")
526
+ if background:
527
+ lines.append("% The sort facts below are axioms of the problem, not conjuncts of the goal.")
528
+ lines += _thf_signature_decls(scope, syms, native_equality=native_equality)
529
+ nonempty = member = 0
530
+ for fact in background:
531
+ if isinstance(fact, Quantifier):
532
+ name, nonempty = f"nonempty_sort_{nonempty}", nonempty + 1
533
+ else:
534
+ name, member = f"sort_member_{member}", member + 1
535
+ lines.append(f"thf({name}, axiom, "
536
+ f"{_thf_formula(fact, syms, vars_, native_equality=native_equality)}).")
537
+ lines.append(f"thf(goal, {role}, "
538
+ f"{_thf_formula(closed, syms, vars_, native_equality=native_equality)}).")
539
+ return "\n".join(lines) + "\n"
540
+
541
+
542
+ def _msfol_split(formula: Node, conjecture: bool,
543
+ include_sort_facts: bool) -> Tuple[Node, Tuple[Node, ...]]:
544
+ """Split a many-sorted ``formula`` into its plain-FOL image and the sort facts to state beside it.
545
+
546
+ The image is :func:`~unicode_logic_kit.fol.nodes.to_fol`: each sort a unary guard
547
+ predicate, each sorted quantifier relativized, each sorted constant its plain
548
+ name. What that forgets is stated separately, by
549
+ :func:`~unicode_logic_kit.fol.nodes.sort_axioms` — no sort is empty, and a sorted
550
+ constant ``c:S`` is an element of ``S``.
551
+
552
+ For a CONJECTURE both kinds are returned as background facts, never folded into
553
+ the formula: a conjunct ``S(c) ∧ φ`` could not be proved even for a tautology
554
+ ``φ``. For an asserted formula (``conjecture=False``) the membership atoms are
555
+ part of what is asserted and stay the conjunct ``to_fol`` builds, and only the
556
+ non-emptiness facts are returned. ``include_sort_facts=False`` is the bare
557
+ relativisation, with no facts at all. A formula with no sorted node returns
558
+ ``to_fol(formula)`` and ``()``.
559
+ """
560
+ if not include_sort_facts:
561
+ return to_fol(formula), ()
562
+ if conjecture:
563
+ return to_fol(formula), tuple(sort_axioms(formula))
564
+ return to_fol(formula, include_sort_facts=True), tuple(nonempty_sort_axioms(formula))
565
+
566
+
567
+ def to_thf_msfol(formula: Node, conjecture: bool = True,
568
+ include_sort_facts: bool = True, native_equality: bool = False) -> str:
569
+ """Emit a TPTP **THF** problem for a *many-sorted* FOL ``formula`` via guard relativization.
570
+
571
+ Each sort becomes a unary guard predicate and each sorted quantifier is
572
+ relativized (``∀x:S φ ↦ ∀x. S(x) → φ``, ``∃x:S φ ↦ ∃x. S(x) ∧ φ``) by the
573
+ toolkit's :func:`~unicode_logic_kit.fol.nodes.to_fol`; the resulting plain-FOL
574
+ formula is then emitted as :func:`to_thf_fol` does. With
575
+ ``include_sort_facts=True`` (default) the two facts that reduction forgets are
576
+ stated too (:func:`~unicode_logic_kit.fol.nodes.sort_axioms`): every sort is
577
+ non-empty (``∃x S(x)``) and a sorted constant is in its sort
578
+ (``Mortal(socrates:Human)`` carries ``Human(socrates)``). For a ``conjecture``
579
+ each is a separate THF ``axiom`` (``nonempty_sort_<i>`` / ``sort_member_<i>``) —
580
+ a fact conjoined to the goal, ``Human(socrates) ∧ φ``, could never be proved,
581
+ even for a tautology ``φ`` — so the problem asks whether the formula follows from
582
+ them, which is the many-sorted question. An asserted formula
583
+ (``conjecture=False``) keeps the membership atoms as a conjunct, since they are
584
+ part of what is asserted, and gets the non-emptiness facts as ``axiom`` lines.
585
+ ``include_sort_facts=False`` emits the bare relativisation, no sort facts. All
586
+ sorts share the single THF individual type ``$i`` — the relativization, not the
587
+ type system, keeps the sorts apart. See :func:`to_thf_fol` for ``native_equality``
588
+ and for the free variable of an asserted formula, which is refused by name.
589
+
590
+ Raises:
591
+ NotImplementedError: ``conjecture`` is false and ``formula`` has a free variable.
592
+ """
593
+ plain, background = _msfol_split(formula, conjecture, include_sort_facts)
594
+ return _thf_problem(plain, conjecture, native_equality, background, where="to_thf_msfol")
595
+
596
+
597
+ # ===========================================================================
598
+ # (B) Isabelle/HOL export
599
+ # ===========================================================================
600
+
601
+ _ISA_BINOP = {And: "\\<and>", Or: "\\<or>", Implies: "\\<longrightarrow>",
602
+ Iff: "\\<longleftrightarrow>"}
603
+
604
+
605
+ def _isa_term(node: Node, syms: "_SymbolResolver", vars_: "_VarResolver") -> str:
606
+ """Render an individual term in Isabelle/HOL term syntax (curried application)."""
607
+ if isinstance(node, Variable):
608
+ return vars_.token(node.name)
609
+ if isinstance(node, Constant):
610
+ return syms.name(_CAT_CONST, node.name, 0)
611
+ if isinstance(node, Number):
612
+ return syms.name(_CAT_CONST, "n" + str(node.value), 0)
613
+ if isinstance(node, Function):
614
+ head = syms.name(_CAT_FUNC, node.name, len(node.args))
615
+ return "(" + " ".join([head] + [_isa_term(a, syms, vars_) for a in node.args]) + ")"
616
+ if isinstance(node, Measure):
617
+ head = syms.name(_CAT_FUNC, "measure", 2)
618
+ return ("(" + " ".join([head, _isa_term(node.entity, syms, vars_),
619
+ _isa_term(node.dimension, syms, vars_)]) + ")")
620
+ raise NotImplementedError(
621
+ f"to_isabelle_fol: unsupported term {type(node).__name__} (classical FOL terms only)."
622
+ )
623
+
624
+
625
+ def _isa_formula(node: Node, syms: "_SymbolResolver", vars_: "_VarResolver",
626
+ native_equality: bool = False) -> str:
627
+ """Render a classical FOL formula in Isabelle/HOL syntax (fully parenthesised).
628
+
629
+ Uses Isabelle's logical-symbol control sequences (``\\<not>``, ``\\<and>`` …)
630
+ and meta/object quantifiers ``\\<forall> x. …`` / ``\\<exists> x. …``. Atoms
631
+ apply their predicate by curried juxtaposition. Xor is rendered as the negated
632
+ biconditional ``\\<not>(l \\<longleftrightarrow> r)``. Every functor / variable
633
+ name is routed through the resolvers so distinct source symbols stay distinct.
634
+
635
+ With ``native_equality=True``, a binary ``=``/``≠`` atom renders as Isabelle's
636
+ own infix ``(a = b)`` / ``(a \\<noteq> b)`` instead of the ``feq``/``fneq``
637
+ consts — genuine, polymorphic HOL identity, no declaration needed.
638
+ """
639
+ def g(n):
640
+ return _isa_formula(n, syms, vars_, native_equality)
641
+ if isinstance(node, Atom):
642
+ constant = truth_value(node)
643
+ if constant is not None:
644
+ return "True" if constant else "False" # HOL's own constants
645
+ if _is_native_eq(node.predicate, len(node.args), native_equality):
646
+ left = _isa_term(node.args[0], syms, vars_)
647
+ right = _isa_term(node.args[1], syms, vars_)
648
+ op = "=" if node.predicate == "=" else "\\<noteq>"
649
+ return f"({left} {op} {right})"
650
+ head = syms.name(_CAT_PRED, node.predicate, len(node.args))
651
+ if not node.args:
652
+ return head
653
+ return "(" + " ".join([head] + [_isa_term(a, syms, vars_) for a in node.args]) + ")"
654
+ if isinstance(node, Not):
655
+ return f"(\\<not> {g(node.formula)})"
656
+ if type(node) in _ISA_BINOP:
657
+ op = _ISA_BINOP[type(node)]
658
+ return f"({g(node.left)} {op} {g(node.right)})"
659
+ if isinstance(node, Xor):
660
+ inner = f"({g(node.left)} \\<longleftrightarrow> {g(node.right)})"
661
+ return f"(\\<not> {inner})"
662
+ if isinstance(node, Quantifier):
663
+ binder = "\\<forall>" if node.type in (_FORALL, "forall") else "\\<exists>"
664
+ return f"({binder} {vars_.token(node.variable.name)}. {g(node.formula)})"
665
+ raise NotImplementedError(
666
+ f"to_isabelle_fol: {type(node).__name__} is outside the classical FOL fragment "
667
+ "supported by the Isabelle export (no modal / second-order / Łukasiewicz / "
668
+ "substructural / lambda). Modal family → hol.isabelle_modal.to_isabelle_modal; "
669
+ "second-order → hol.secondorder.to_isabelle_so; K3/LP → hol.manyvalued; "
670
+ "ILL/Lambek derivations → hol.isabelle_substructural."
671
+ )
672
+
673
+
674
+ def _isa_consts_block(formula: Node, syms: "_SymbolResolver",
675
+ native_equality: bool = False) -> List[str]:
676
+ """``consts`` declarations for every predicate / function / constant.
677
+
678
+ Individuals live in a single uninterpreted HOL type ``i``; a k-ary predicate
679
+ has type ``i ⇒ … ⇒ bool`` and a k-ary function ``i ⇒ … ⇒ i``. Each ``consts``
680
+ entry uses the resolver-assigned identifier, so no two declarations share a
681
+ name (no duplicate-``consts`` load error) and every name matches its usages.
682
+ A binary ``=``/``≠`` skips its declaration when ``native_equality=True`` —
683
+ Isabelle's polymorphic ``=`` is built in and needs no ``consts`` entry.
684
+ """
685
+ preds, funcs, consts = _signature(formula)
686
+ lines: List[str] = []
687
+ arrow = " \\<Rightarrow> "
688
+ for name, arity in sorted(preds):
689
+ if _is_native_eq(name, arity, native_equality):
690
+ continue
691
+ ident = syms.name(_CAT_PRED, name, arity)
692
+ typ = arrow.join(["i"] * arity + ["bool"]) if arity else "bool"
693
+ lines.append(f"consts {ident} :: \"{typ}\"")
694
+ for name, arity in sorted(funcs):
695
+ ident = syms.name(_CAT_FUNC, name, arity)
696
+ typ = arrow.join(["i"] * (arity + 1))
697
+ lines.append(f"consts {ident} :: \"{typ}\"")
698
+ for name in sorted(consts):
699
+ ident = syms.name(_CAT_CONST, name, 0)
700
+ lines.append(f"consts {ident} :: \"i\"")
701
+ return lines
702
+
703
+
704
+ def to_isabelle_fol(formula: Node, theory_name: str = "FOL_Export",
705
+ lemma_name: str = "goal", proof: str = "oops",
706
+ native_equality: bool = False) -> str:
707
+ """Emit a loadable **Isabelle/HOL** theory with ``formula`` as a real ``lemma``.
708
+
709
+ The theory declares a single uninterpreted individual type ``i`` and a
710
+ ``consts`` entry for every predicate / function / constant in the signature,
711
+ then states the formula as ``lemma <lemma_name>: "⌜formula⌝"`` over those
712
+ uninterpreted symbols. Free variables are left as Isabelle schematic/free
713
+ term variables (HOL closes them implicitly at the lemma level). The lemma is a
714
+ goal, never an assertion, so a free variable is the parameter it is: one
715
+ individual, for which the lemma holds iff it holds for every individual (the
716
+ refusal that :func:`to_thf_fol` makes for an asserted formula has no counterpart
717
+ here).
718
+
719
+ The proof line defaults to ``oops`` (the lemma is *stated* but deliberately
720
+ left open, so the theory loads without claiming a proof). Pass
721
+ ``proof="by auto"``, ``"sledgehammer"``, ``"by blast"``, … to attempt a
722
+ discharge — but note that classical FOL is semi-decidable only, so no tactic
723
+ is guaranteed to close every valid lemma, and this function does not run
724
+ Isabelle. By default, equality ``=`` / ``≠`` is the uninterpreted predicate
725
+ ``feq`` / ``fneq`` (see module docstring), not HOL ``=``; pass
726
+ ``native_equality=True`` to emit Isabelle's own polymorphic ``=`` / ``\\<noteq>``
727
+ instead — genuine HOL identity, so congruence closes by ``simp``/``auto`` with
728
+ no axioms added. The comparison predicates ``<`` ``>`` ``≤`` ``≥`` are
729
+ unaffected either way.
730
+ """
731
+ return _isabelle_problem(formula, theory_name, lemma_name, proof, native_equality)
732
+
733
+
734
+ def _isabelle_problem(formula: Node, theory_name: str, lemma_name: str, proof: str,
735
+ native_equality: bool, background: Sequence[Node] = ()) -> str:
736
+ r"""The Isabelle theory of ``formula``, with ``background`` as the HYPOTHESES of the lemma.
737
+
738
+ ``background`` are closed sentences (the many-sorted reading's sort facts, see
739
+ :func:`_msfol_split`). They are not ``axiomatization`` facts and not conjuncts of
740
+ the goal but premises of the lemma, ``\<lbrakk>f1; f2\<rbrakk> \<Longrightarrow> φ``: a premise is used by
741
+ ``blast`` / ``auto`` / ``metis`` / ``nitpick`` without a ``using`` clause, which an
742
+ ``axiomatization`` fact is not — and the proof battery of
743
+ :func:`~unicode_logic_kit.hol.isabelle_runner.isabelle_decide_fol` names no facts.
744
+ """
745
+ formula = _reduce_nl_nodes(formula) # Contrast → ∧, Count → witnesses
746
+ # A numeral is a constant identified by its value (1 and 1.0 are one), named ``n1``:
747
+ # a user constant spelled like it is refused, not merged with it.
748
+ [formula], _ = numerals_as_constants([formula], where="to_isabelle_fol",
749
+ spell=prefixed_numeral_name)
750
+ scope = _scope(formula, background)
751
+ # Only the comment differs, and only when '=' / '≠' actually occur at their
752
+ # native binary arity — a formula without them emits byte-identical output
753
+ # whether native_equality is True or False (nothing about it would differ).
754
+ preds, _, _ = _signature(scope)
755
+ if any(_is_native_eq(n, a, native_equality) for n, a in preds):
756
+ eq_comment = " '=' / '≠' are Isabelle's own built-in HOL identity (no axioms needed)."
757
+ else:
758
+ eq_comment = " '=' / '≠' are the uninterpreted predicates feq / fneq, NOT HOL identity."
759
+ lines = [
760
+ f"theory {theory_name}",
761
+ " imports Main",
762
+ "begin",
763
+ "",
764
+ "(* Classical FOL embedded into Isabelle/HOL over an uninterpreted",
765
+ " individual type and uninterpreted predicates/functions/constants.",
766
+ eq_comment,
767
+ " FOL is semi-decidable only: no tactic closes every valid lemma. *)",
768
+ "",
769
+ "typedecl i \\<comment> \\<open>uninterpreted individuals\\<close>",
770
+ ]
771
+ syms = _SymbolResolver(scope, native_equality=native_equality)
772
+ # A binder shadows the constants of its own name, so the bound variables take their
773
+ # tokens from the pool the constants, functions and predicates already took theirs from.
774
+ vars_ = _VarResolver(_sanitize, used=syms._used)
775
+ lines += _isa_consts_block(scope, syms, native_equality=native_equality)
776
+ lines.append("")
777
+ statement = _isa_formula(formula, syms, vars_, native_equality=native_equality)
778
+ if background:
779
+ facts = "; ".join(_isa_formula(fact, syms, vars_, native_equality=native_equality)
780
+ for fact in background)
781
+ statement = f"\\<lbrakk>{facts}\\<rbrakk> \\<Longrightarrow> {statement}"
782
+ lines.append(f"lemma {lemma_name}: \"{statement}\"")
783
+ lines.append(f" {proof}")
784
+ lines.append("")
785
+ lines.append("end")
786
+ return "\n".join(lines) + "\n"
787
+
788
+
789
+ def to_isabelle_msfol(formula: Node, theory_name: str = "MSFOL_Export",
790
+ lemma_name: str = "goal", proof: str = "oops",
791
+ include_sort_facts: bool = True, native_equality: bool = False) -> str:
792
+ """Emit an **Isabelle/HOL** theory for a many-sorted formula via guard relativization.
793
+
794
+ Reduces ``formula`` with :func:`~unicode_logic_kit.fol.nodes.to_fol` (each sort
795
+ becomes a unary guard predicate over the single individual type ``i``, each
796
+ sorted quantifier is relativized) and emits the result as
797
+ :func:`to_isabelle_fol` does. With ``include_sort_facts=True`` (default) the two
798
+ facts that reduction forgets are stated too
799
+ (:func:`~unicode_logic_kit.fol.nodes.sort_axioms`): every sort is non-empty
800
+ (``∃x S(x)``) and a sorted constant is in its sort (``Human(socrates)`` for
801
+ ``Mortal(socrates:Human)``). They are HYPOTHESES of the lemma, not conjuncts of
802
+ the goal — ``lemma goal: "⟦∃x. human x; human socrates⟧ ⟹ φ"`` — because a goal
803
+ ``human socrates ∧ φ`` could never be proved, even for a tautology ``φ``, the
804
+ guard being an uninterpreted predicate; and a premise needs no ``using`` clause
805
+ for ``blast`` / ``auto`` / ``nitpick`` to use it. So the lemma asks whether the
806
+ formula follows from the sort facts, which is the many-sorted question.
807
+ ``include_sort_facts=False`` is the bare relativisation, no sort facts. See
808
+ :func:`to_isabelle_fol` for ``native_equality``.
809
+ """
810
+ plain, background = _msfol_split(formula, True, include_sort_facts)
811
+ return _isabelle_problem(plain, theory_name, lemma_name, proof, native_equality,
812
+ background)