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,687 @@
1
+ """Second-order logic → HOL/THF export (predicate quantification mapped natively).
2
+
3
+ Classical second-order logic (SOL) extends first-order logic with quantification
4
+ over *predicate* variables: ``∀P φ`` / ``∃P φ`` where ``P`` ranges over relations
5
+ of a fixed arity on the domain (the toolkit's finite-model partner is
6
+ :func:`unicode_logic_kit.semantics.secondorder.satisfies_so`, which brute-forces
7
+ every relation ``R ⊆ domain ** arity``). A higher-order logic prover already *has*
8
+ predicate quantification, so the embedding here is direct rather than the shallow
9
+ world-lifting used for modal logic:
10
+
11
+ - an object variable ``x`` has THF type ``$i`` (Isabelle: an uninterpreted ``i``);
12
+ - a predicate variable ``P`` of arity ``k`` has THF type ``$i > … > $i > $o``
13
+ (``k`` copies of ``$i``), i.e. Isabelle ``i ⇒ … ⇒ i ⇒ bool``;
14
+ - a first-order atom ``A(t̄)`` is the application ``( a @ t1 @ … @ tk )``;
15
+ - an object quantifier ``∀x`` / ``∃x`` is a THF ``! [X:$i]`` / ``? [X:$i]``;
16
+ - a **second-order** quantifier ``∀P/k`` / ``∃P/k`` is a THF
17
+ ``! [P: ($i>…>$i>$o)]`` / ``? [P: …]`` — a quantifier over a predicate-typed
18
+ variable, exactly mirroring the SOL reading.
19
+
20
+ This is the **monadic-and-up relational** second order: predicate (relation)
21
+ variables of any arity, including arity 0 (a Boolean / propositional variable,
22
+ type ``$o``). It does NOT cover function-variable quantification or third order
23
+ and up — out of scope, matching ``satisfies_so``.
24
+
25
+ **Free symbols.** In the toolkit's ``second_order`` parser mode a bare lowercase
26
+ name parses to a :class:`~unicode_logic_kit.fol.nodes.Variable`. Any object
27
+ variable that is *not* bound by an enclosing object quantifier is a **free**
28
+ individual; in a closed THF problem / Isabelle theory a free variable is not
29
+ admissible, so such free variables are emitted as object **constants** (declared
30
+ ``$i`` / ``consts … :: i``) — the natural reading of a free individual symbol, and
31
+ exactly how the finite-model evaluator would have to interpret it (via the
32
+ structure's ``constants``). Bound object variables render as THF uppercase
33
+ variables. A predicate name bound by some ``SecondOrderQuantifier`` is a bound
34
+ predicate variable everywhere it occurs (it is never declared as a problem
35
+ symbol); any *other* predicate is a free predicate and is declared in the
36
+ signature.
37
+
38
+ **Constants and free variables.** A free object variable is a PARAMETER of the problem (one
39
+ unknown element, the same in every formula, see :mod:`unicode_logic_kit.fol._free_parameters`)
40
+ and a constant is a particular element that its name stands for, so ``P(x)`` with a free ``x``
41
+ and ``P('x')`` are two statements. Both are declared as individuals, but as two symbols under
42
+ two functors even when they are spelled alike (``x`` and ``x_2``: the variable claims the plain
43
+ name). A constant is never looked up among the bound variables, so no binder, of any name,
44
+ captures it. A constant of any name has a legal identifier in both targets: a non-ASCII letter
45
+ is transliterated, any other character that is no letter, digit or underscore becomes an
46
+ underscore, a name that would start with a digit or an underscore gets a ``p`` in front, and the
47
+ functors are made unique over every kind of symbol, so two names that share a stem (``a b`` and
48
+ ``a_b``) are still two symbols.
49
+
50
+ **Standard vs Henkin semantics.** In a real HOL prover the predicate quantifiers
51
+ range over the prover's full function space, i.e. **standard (full) second-order
52
+ semantics** — the same semantics ``satisfies_so`` realises on a finite domain.
53
+ SOL validity under standard semantics is **not semi-decidable** (no complete
54
+ proof procedure exists), so an external HOL prover (Leo-III, Satallax,
55
+ Sledgehammer) may fail to discharge a valid conjecture even though it is sound.
56
+ If you want Henkin (general-models) semantics instead — under which SOL is
57
+ effectively first-order and *is* axiomatisable — that is the user's job: add
58
+ comprehension axioms and restrict the predicate domain explicitly; this module
59
+ does not do it for you. The toolkit only **emits** the THF problem / Isabelle
60
+ theory; it does **not** run any prover.
61
+
62
+ Equality ``=`` / ``≠`` follows the toolkit convention used by the modal THF
63
+ export: an ordinary uninterpreted predicate over ``$i`` (rendered ``feq`` /
64
+ ``fneq``), **not** primitive HOL identity. (The finite-model ``satisfies_so`` does
65
+ read ``=`` as identity via the structure; if you need that in the prover, add the
66
+ reflexivity / Leibniz axioms, or post-edit to primitive ``=``.)
67
+
68
+ **Numerals.** A numeral is an individual constant identified by its VALUE, written
69
+ ``n1``, ``n2.5`` (``n2_5`` once sanitised): ``1`` and ``1.0`` are one constant, and
70
+ nothing else is known about it. ``+ - * /`` are free function symbols and ``< > ≤ ≥`` free
71
+ predicates. A numeral spelled like another symbol of the formula (the constant ``n1`` next
72
+ to the number ``1``, or a free object variable ``n1``, which this export declares as an
73
+ individual constant) is refused by name, never merged with it. The one numeral that stays
74
+ a number is the operand of a comparison with a cardinality, which :func:`to_isabelle_so`
75
+ reads over the naturals (see above).
76
+
77
+ Public API: :func:`to_thf_so`, :func:`to_isabelle_so`.
78
+ """
79
+
80
+ from typing import Dict, FrozenSet, List, Set, Tuple
81
+
82
+ from ..fol.nodes import (
83
+ Node, Variable, Constant, Number, Function,
84
+ Atom, Not, And, Or, Xor, Implies, Iff, Quantifier,
85
+ SecondOrderQuantifier, Cardinality, SortedCardinality,
86
+ )
87
+ from ..fol._numeral_symbols import numeral_name, numerals_as_constants, prefixed_numeral_name
88
+ from ..fol._symbol_names import dedupe as _dedupe # shared de-collision helper
89
+ from ..fol._truth_constants import truth_value
90
+ from ._ho_common import FREE_VARIABLE, CONSTANT, constant_name, functor_stem
91
+ from ._isabelle_binders import (
92
+ PREDICATE, VARIABLE, binder_tokens, collect_binders, declared_names,
93
+ )
94
+
95
+ _FORALL = "∀"
96
+ _EXISTS = "∃"
97
+
98
+ # `=` / `≠` are uninterpreted structure-relative predicates in this toolkit, NOT
99
+ # primitive HOL identity. Give them valid distinct THF/Isabelle functors.
100
+ _PRED_ALIAS = {"=": "feq", "≠": "fneq"}
101
+
102
+
103
+ # ===========================================================================
104
+ # Name sanitising (shared spirit with qml._thf_name)
105
+ # ===========================================================================
106
+
107
+ def _functor(name: str) -> str:
108
+ """Turn a free predicate/constant/function name into a HOL functor stem (NOT injective).
109
+
110
+ The stem is the kit's ASCII lower word (:func:`~unicode_logic_kit.hol._ho_common.functor_stem`):
111
+ a non-ASCII letter is transliterated, any other character that is no letter, digit or
112
+ underscore becomes an underscore, and a name that would start with a digit or an underscore
113
+ gets a ``p`` in front, so every name has a stem that both THF and Isabelle read as one
114
+ identifier. A name that is already a lower-case word is its own stem.
115
+ """
116
+ if name in _PRED_ALIAS:
117
+ return _PRED_ALIAS[name]
118
+ return functor_stem(name)
119
+
120
+
121
+ # ===========================================================================
122
+ # De-colliding name resolvers (THF only; the Isabelle path keeps _functor)
123
+ # ===========================================================================
124
+ #
125
+ # Two distinct collision hazards motivate these resolvers, and BOTH are specific
126
+ # to THF (where bound variables are uppercase and free symbols are lowercase
127
+ # functors, and every functor needs exactly one type):
128
+ #
129
+ # (1) FREE-SYMBOL COLLAPSE (soundness). A free predicate ``P`` and a free
130
+ # individual ``p`` both pass through :func:`_functor` to the SAME functor
131
+ # ``p`` but need DIFFERENT types (``$i>$o`` vs ``$i``); likewise ``Foo`` and
132
+ # ``foo``. Emitting both yields a duplicate ``p_decl`` with conflicting types
133
+ # and an ill-typed ``( p @ p )``. :class:`_FreeNames` assigns every free
134
+ # symbol a globally UNIQUE functor across the predicate / individual /
135
+ # function categories, and BOTH the declarations and the usages are routed
136
+ # through it so they always agree.
137
+ #
138
+ # (2) BOUND-VARIABLE CAPTURE (faithfulness). A bound predicate variable renders
139
+ # as ``name.upper()`` and a bound object variable renders as ``name.upper()``
140
+ # too, so case-folding-equal names in nested binders collide: ``∃P ∀p P(p)``
141
+ # would emit ``? [P:($i>$o)] : ! [P:$i] : ( P @ P )`` — the object binder
142
+ # shadows the predicate binder and ``P @ P`` is ill-typed. :class:`_Scope`
143
+ # gives bound predicate-variables and bound object-variables ONE shared,
144
+ # never-reused pool of uppercase tokens, so distinct bound ``(kind, name)``
145
+ # pairs always get distinct tokens.
146
+
147
+
148
+ class _FreeNames:
149
+ """Resolve every FREE symbol to a globally-unique THF functor.
150
+
151
+ Built once from :func:`_signature` so that declarations (:func:`_thf_signature`)
152
+ and usages (:func:`_thf`, :func:`_thf_term`) draw the SAME functor for the
153
+ same symbol. Predicates, individuals (constants / numbers / free object
154
+ variables) and functions live in three lookup tables but share ONE pool of
155
+ emitted functors, so a free predicate ``P`` and a free individual ``p`` — both
156
+ naively ``_functor``-ing to ``p`` — get distinct functors (``p`` and, say,
157
+ ``p_1``). Iteration is sorted within each category and categories are claimed
158
+ predicate → individual → function, so the result is deterministic.
159
+
160
+ A constant (``ind``) and a free object variable (``var``) are two symbols even when
161
+ they are spelled alike: the variable is a parameter of the problem, one unknown element,
162
+ and the constant a particular one, so ``P(x)`` with a free ``x`` and ``P('x')`` are two
163
+ statements. Both are declared as individuals of type ``$i``, under two functors; the free
164
+ variable claims the shared name first (``x``), the constant gets the next one (``x_2``).
165
+ """
166
+
167
+ def __init__(self, formula: Node):
168
+ pred_arity, const_names, func_arity, free_vars = _signature(formula)
169
+ used = set()
170
+ self.pred: Dict[str, str] = {}
171
+ self.ind: Dict[str, str] = {}
172
+ self.var: Dict[str, str] = {}
173
+ self.func: Dict[str, str] = {}
174
+ # The names the Isabelle text gives its binders, ``{(kind, name): token}``: filled by
175
+ # :func:`to_isabelle_so` once the declarations are known (THF names its binders itself).
176
+ self.binder: Dict[Tuple[str, str], str] = {}
177
+ # Predicates first (they tend to keep the more meaningful name), then
178
+ # individuals, then functions — each de-colliding against the shared pool.
179
+ for name in sorted(pred_arity):
180
+ self.pred[name] = _dedupe(_functor(name), used)
181
+ for kind, name in _individual_symbols(const_names, free_vars):
182
+ (self.var if kind == FREE_VARIABLE else self.ind)[name] = _dedupe(_functor(name), used)
183
+ for name in sorted(func_arity):
184
+ self.func[name] = _dedupe(_functor(name), used)
185
+
186
+
187
+ class _Scope:
188
+ """A shared, never-reused token pool for bound predicate AND object variables.
189
+
190
+ A binder calls :meth:`bind`, which returns a *new* :class:`_Scope` whose
191
+ mapping is the parent's extended with a fresh uppercase token for this
192
+ ``(kind, name)`` — fresh against EVERY token in scope, regardless of kind, so
193
+ a predicate binder ``P`` and an inner object binder ``p`` never both render as
194
+ ``P``. The token is looked up by ``(kind, name)`` so a bound variable resolves
195
+ to the same token at its binder and at every applied occurrence.
196
+ """
197
+
198
+ __slots__ = ("_tokens", "_used")
199
+
200
+ def __init__(self, tokens=None, used=None):
201
+ self._tokens: Dict = dict(tokens or {})
202
+ self._used = set(used or ())
203
+
204
+ def bind(self, kind: str, name: str) -> "_Scope":
205
+ used = set(self._used)
206
+ base = _functor(name).upper() or "V"
207
+ # An all-non-alnum name could upper-case to something starting with a
208
+ # digit / underscore; guarantee a legal THF variable head.
209
+ if not base[:1].isalpha():
210
+ base = "V" + base
211
+ token = _dedupe(base, used)
212
+ tokens = dict(self._tokens)
213
+ tokens[(kind, name)] = token
214
+ return _Scope(tokens, used)
215
+
216
+ def token(self, kind: str, name: str) -> str:
217
+ return self._tokens[(kind, name)]
218
+
219
+
220
+ # ===========================================================================
221
+ # Scope analysis: bound predicate vars, bound object vars, free symbols
222
+ # ===========================================================================
223
+
224
+ def _bound_preds(formula: Node) -> FrozenSet[str]:
225
+ """Names bound by some ``SecondOrderQuantifier`` (-> bound predicate vars)."""
226
+ return frozenset(
227
+ n.predicate for n in formula.walk()
228
+ if isinstance(n, SecondOrderQuantifier)
229
+ )
230
+
231
+
232
+ def _so_arity(formula: Node) -> Dict[str, int]:
233
+ """Map each second-order-bound predicate name to its binder-recorded arity."""
234
+ return {
235
+ n.predicate: n.arity
236
+ for n in formula.walk()
237
+ if isinstance(n, SecondOrderQuantifier)
238
+ }
239
+
240
+
241
+ def _free_object_vars(formula: Node, bound=frozenset()) -> FrozenSet[str]:
242
+ """Object-variable names occurring free (not under an enclosing ∀x/∃x).
243
+
244
+ Recurses tracking the enclosing object-quantifier scope. SecondOrderQuantifier
245
+ binds a *predicate* name, not an object variable, so it does not change the
246
+ object-binding scope.
247
+ """
248
+ if isinstance(formula, Variable):
249
+ return frozenset() if formula.name in bound else frozenset({formula.name})
250
+ if isinstance(formula, Quantifier):
251
+ return _free_object_vars(formula.formula, bound | {formula.variable.name})
252
+ if isinstance(formula, (Cardinality, SortedCardinality)):
253
+ # |{v : φ}| BINDS v over its matrix — a generic child walk would report
254
+ # the bound v as free (the binder-blind defect class fixed in 0.16.0).
255
+ return _free_object_vars(formula.formula, bound | {formula.variable.name})
256
+ free = set()
257
+ for child in formula._child_nodes():
258
+ free |= _free_object_vars(child, bound)
259
+ return frozenset(free)
260
+
261
+
262
+ def _numerals_as_constants(formula: Node, where: str) -> Node:
263
+ """``formula`` with every numeral written as the constant ``n1``, ``n2.5``, ... of its VALUE.
264
+
265
+ A numeral is a constant identified by its value (``1`` and ``1.0`` are one constant), and
266
+ ``+ - * /`` and ``< > ≤ ≥`` are ordinary function and predicate symbols here. The one
267
+ numeral left as a ``Number`` is the operand of a comparison with a cardinality
268
+ (``|{x : P(x)}| ≥ 2``): that comparison is arithmetic over the naturals (HOL's ``card``),
269
+ and the operand is the number it compares the count with, not an individual.
270
+
271
+ Raises:
272
+ NotImplementedError: a constant, a function or a free object variable (which this
273
+ export reads as an individual constant) is spelled like the constant of a
274
+ numeral of the formula (``n1`` next to the number ``1``): they would be ONE
275
+ symbol, and a numeral is not the constant of the same spelling.
276
+ """
277
+ [rewritten], names = numerals_as_constants([formula], where=where, spell=prefixed_numeral_name,
278
+ counting_comparisons=True)
279
+ clash = sorted(names & _free_object_vars(rewritten))
280
+ if clash:
281
+ raise NotImplementedError(
282
+ f"{where}: the free object variable {clash[0]!r} is exported as an individual "
283
+ "constant of that name, and so is the numeral whose constant is spelled alike, so "
284
+ "the two would read as one name. A numeral is a constant of its own, identified by "
285
+ f"its value, and the kit does not put it next to a variable of its spelling; "
286
+ f"rename the variable {clash[0]!r}.")
287
+ return rewritten
288
+
289
+
290
+ def _signature(formula: Node):
291
+ """Collect free predicates, constants, functions and free object variables.
292
+
293
+ Returns ``(pred_arity, const_names, func_arity, free_vars)``:
294
+
295
+ - ``pred_arity``: free predicate name -> arity (atoms whose predicate is not a
296
+ bound second-order variable);
297
+ - ``const_names``: explicit constants (a numeral is the constant of its value, see
298
+ :func:`_numerals_as_constants`);
299
+ - ``func_arity``: function name -> arity;
300
+ - ``free_vars``: the free object variables, which the closed export declares as
301
+ individual constants too but which stay apart from a constant of their spelling.
302
+ """
303
+ so_names = _bound_preds(formula)
304
+ free_vars = _free_object_vars(formula)
305
+ pred_arity: Dict[str, int] = {}
306
+ const_names: Set[str] = set()
307
+ func_arity: Dict[str, int] = {}
308
+ for n in formula.walk():
309
+ if isinstance(n, Atom):
310
+ if n.predicate in so_names or truth_value(n) is not None:
311
+ continue
312
+ pred_arity[n.predicate] = len(n.args)
313
+ elif isinstance(n, SortedCardinality):
314
+ # The sort guard becomes a free unary predicate over the matrix.
315
+ pred_arity.setdefault(n.sort, 1)
316
+ elif isinstance(n, Constant):
317
+ const_names.add(constant_name(n))
318
+ elif isinstance(n, Function):
319
+ func_arity[n.name] = len(n.args)
320
+ return pred_arity, const_names, func_arity, free_vars
321
+
322
+
323
+ def _individual_symbols(const_names, free_vars) -> List[Tuple[str, str]]:
324
+ """The individual symbols as ``(kind, name)``, sorted by name, a free variable before a
325
+ constant of its spelling (for a formula without such a pair: the sorted names)."""
326
+ symbols = ([(FREE_VARIABLE, name) for name in free_vars]
327
+ + [(CONSTANT, name) for name in const_names])
328
+ return sorted(symbols, key=lambda symbol: (symbol[1], symbol[0] != FREE_VARIABLE))
329
+
330
+
331
+ # ===========================================================================
332
+ # (A) TPTP THF export
333
+ # ===========================================================================
334
+
335
+ def _thf_pred_type(arity: int) -> str:
336
+ """THF type of an arity-``k`` predicate variable: ``$i > … > $i > $o``.
337
+
338
+ Arity 0 is the Boolean case, type ``$o``.
339
+ """
340
+ return " > ".join(["$i"] * arity + ["$o"]) if arity else "$o"
341
+
342
+
343
+ def _thf_term(node: Node, scope: "_Scope", free: "_FreeNames") -> str:
344
+ """Render an individual term in THF.
345
+
346
+ A ``Variable`` bound by an enclosing object quantifier renders as its scope
347
+ token (an uppercase THF variable, de-collided against every bound symbol);
348
+ otherwise it is a free individual functor drawn from ``free``. Constants /
349
+ numbers / functions resolve through ``free`` so a usage always matches its
350
+ declaration. A constant is never looked up among the bound variables.
351
+ """
352
+ if isinstance(node, Variable):
353
+ try:
354
+ return scope.token("obj", node.name)
355
+ except KeyError:
356
+ return free.var[node.name] # free individual functor
357
+ if isinstance(node, Constant):
358
+ return free.ind[node.name]
359
+ if isinstance(node, Number):
360
+ raise NotImplementedError(
361
+ f"to_thf_so: the number {numeral_name(node.value)} is compared with a cardinality, "
362
+ "and TH0 has no built-in finite-set theory to state that — use to_isabelle_so, "
363
+ "which embeds |{v : φ}| as HOL's ``card {v. φ}`` over the naturals.")
364
+ if isinstance(node, Function):
365
+ head = free.func[node.name]
366
+ return "( " + " @ ".join([head] + [_thf_term(a, scope, free) for a in node.args]) + " )"
367
+ if isinstance(node, (Cardinality, SortedCardinality)):
368
+ raise NotImplementedError(
369
+ "to_thf_so: TH0 has no built-in finite-set theory, so a faithful "
370
+ "cardinality encoding is not available here — use to_isabelle_so, "
371
+ "which embeds |{v : φ}| as HOL's ``card {v. φ}``.")
372
+ raise NotImplementedError(
373
+ f"to_thf_so: unsupported term {type(node).__name__}.")
374
+
375
+
376
+ def _thf(node: Node, scope: "_Scope", free: "_FreeNames") -> str:
377
+ """Render a second-order formula as a THF term of type ``$o``.
378
+
379
+ ``scope`` resolves currently-bound predicate-variable and object-variable
380
+ names to their (mutually de-collided) uppercase THF tokens; ``free`` resolves
381
+ free symbols to their unique functors.
382
+ """
383
+ if isinstance(node, Atom):
384
+ if truth_value(node) is not None:
385
+ return "$true" if truth_value(node) else "$false"
386
+ try:
387
+ head = scope.token("pred", node.predicate) # bound predicate VARIABLE
388
+ except KeyError:
389
+ head = free.pred[node.predicate] # free predicate functor
390
+ if not node.args:
391
+ return head
392
+ return "( " + " @ ".join([head] + [_thf_term(a, scope, free) for a in node.args]) + " )"
393
+ if isinstance(node, Not):
394
+ return f"( ~ {_thf(node.formula, scope, free)} )"
395
+ if isinstance(node, And):
396
+ return f"( {_thf(node.left, scope, free)} & {_thf(node.right, scope, free)} )"
397
+ if isinstance(node, Or):
398
+ return f"( {_thf(node.left, scope, free)} | {_thf(node.right, scope, free)} )"
399
+ if isinstance(node, Xor):
400
+ return f"( {_thf(node.left, scope, free)} <~> {_thf(node.right, scope, free)} )"
401
+ if isinstance(node, Implies):
402
+ return f"( {_thf(node.left, scope, free)} => {_thf(node.right, scope, free)} )"
403
+ if isinstance(node, Iff):
404
+ return f"( {_thf(node.left, scope, free)} <=> {_thf(node.right, scope, free)} )"
405
+ if isinstance(node, Quantifier):
406
+ x = node.variable.name
407
+ q = "!" if node.type in (_FORALL, "forall") else "?"
408
+ inner_scope = scope.bind("obj", x)
409
+ inner = _thf(node.formula, inner_scope, free)
410
+ return f"( {q} [{inner_scope.token('obj', x)}: $i] : {inner} )"
411
+ if isinstance(node, SecondOrderQuantifier):
412
+ # Predicate quantifier mapped DIRECTLY to a THF quantifier over a
413
+ # predicate-typed variable of the inferred arity.
414
+ p = node.predicate
415
+ q = "!" if node.type in (_FORALL, "forall") else "?"
416
+ typ = _thf_pred_type(node.arity)
417
+ inner_scope = scope.bind("pred", p)
418
+ inner = _thf(node.formula, inner_scope, free)
419
+ return f"( {q} [{inner_scope.token('pred', p)}: ( {typ} )] : {inner} )"
420
+ raise NotImplementedError(
421
+ f"to_thf_so: {type(node).__name__} is outside the second-order fragment "
422
+ "supported by the THF export.")
423
+
424
+
425
+ def _thf_signature(formula: Node, free: "_FreeNames") -> List[str]:
426
+ """Type declarations for free predicates / constants / functions.
427
+
428
+ Each free symbol is declared under the UNIQUE functor ``free`` assigned it, so
429
+ a free predicate and a free individual that would otherwise both lower-case to
430
+ the same name get distinct, single-typed declarations (no duplicate ``_decl``,
431
+ no conflicting types). Second-order-bound predicate variables are omitted:
432
+ they are THF bound variables introduced by their binders, not problem-level
433
+ symbols.
434
+ """
435
+ pred_arity, const_names, func_arity, free_vars = _signature(formula)
436
+ decls = []
437
+ for name, arity in sorted(pred_arity.items()):
438
+ f = free.pred[name]
439
+ typ = _thf_pred_type(arity)
440
+ decls.append(f"thf({f}_decl, type, ( {f} : ( {typ} ) )).")
441
+ for kind, name in _individual_symbols(const_names, free_vars):
442
+ f = (free.var if kind == FREE_VARIABLE else free.ind)[name]
443
+ decls.append(f"thf({f}_decl, type, ( {f} : $i )).")
444
+ for name, arity in sorted(func_arity.items()):
445
+ f = free.func[name]
446
+ typ = " > ".join(["$i"] * (arity + 1))
447
+ decls.append(f"thf({f}_decl, type, ( {f} : ( {typ} ) )).")
448
+ return decls
449
+
450
+
451
+ def to_thf_so(formula: Node, conjecture: bool = True) -> str:
452
+ """Emit a complete TPTP **THF** problem for a second-order formula.
453
+
454
+ Object individuals inhabit ``$i``; a predicate variable ``P`` of arity ``k``
455
+ is quantified at THF type ``$i > … > $i > $o``, so each ``∀P`` / ``∃P`` maps
456
+ one-to-one to a higher-order quantifier — the natural SOL → HOL reading. The
457
+ output declares every free predicate / constant / function symbol (free object
458
+ variables are emitted as ``$i`` constants) and ends with the formula as a
459
+ ``conjecture`` (``conjecture=True``) or an ``axiom`` (``conjecture=False``),
460
+ ready for a higher-order ATP (Leo-III, Satallax).
461
+
462
+ Semantics is **standard / full** second order in such a prover (predicate
463
+ quantifiers range over the full function space), matching ``satisfies_so``.
464
+ SOL validity under standard semantics is **not semi-decidable**, so a sound
465
+ prover may still fail to close a valid conjecture; the toolkit only emits the
466
+ problem, it does not run any prover. For Henkin (general-models) semantics,
467
+ add your own comprehension axioms.
468
+
469
+ Equality ``=`` / ``≠`` is emitted as an ordinary uninterpreted relation over
470
+ ``$i`` (``feq`` / ``fneq``), not primitive HOL identity.
471
+ """
472
+ formula = _numerals_as_constants(formula, "to_thf_so")
473
+ role = "conjecture" if conjecture else "axiom"
474
+ lines = [
475
+ "% Direct second-order -> HOL embedding (predicate quantifiers are native).",
476
+ "% Standard (full) second-order semantics in a HOL prover; SOL validity is",
477
+ "% NOT semi-decidable, so a sound prover may fail to close a valid goal.",
478
+ ]
479
+ free = _FreeNames(formula)
480
+ lines += _thf_signature(formula, free)
481
+ lines.append(f"thf(goal, {role}, ( {_thf(formula, _Scope(), free)} )).")
482
+ return "\n".join(lines) + "\n"
483
+
484
+
485
+ # ===========================================================================
486
+ # (B) Isabelle/HOL export
487
+ # ===========================================================================
488
+
489
+ # Isabelle symbol macros (ASCII \<...> forms render as the Unicode glyphs).
490
+ _ISA = {
491
+ "not": "\\<not>",
492
+ "and": "\\<and>",
493
+ "or": "\\<or>",
494
+ "imp": "\\<longrightarrow>",
495
+ "iff": "\\<longleftrightarrow>",
496
+ "forall": "\\<forall>",
497
+ "exists": "\\<exists>",
498
+ "Rightarrow": "\\<Rightarrow>",
499
+ }
500
+ _ARROW = _ISA["Rightarrow"]
501
+
502
+
503
+ def _isa_pred_type(arity: int) -> str:
504
+ """Isabelle type of an arity-``k`` predicate variable: ``i ⇒ … ⇒ i ⇒ bool``."""
505
+ return (" " + _ARROW + " ").join(["i"] * arity + ["bool"]) if arity else "bool"
506
+
507
+
508
+ def _isa_fun_type(arity: int) -> str:
509
+ """Isabelle type of an arity-``k`` function: ``i ⇒ … ⇒ i`` (k+1 copies of i)."""
510
+ return (" " + _ARROW + " ").join(["i"] * (arity + 1))
511
+
512
+
513
+ def _isa_term(node: Node, bvars: FrozenSet[str], free: "_FreeNames") -> str:
514
+ """Render an individual term in Isabelle (curried application, no commas).
515
+
516
+ A bound object variable keeps its name unless a symbol of the theory is spelled like it
517
+ (``free.binder``, see :mod:`unicode_logic_kit.hol._isabelle_binders`); a free object
518
+ variable is a declared individual constant whose functor is drawn from ``free`` — the SAME
519
+ de-colliding resolver the declarations use, so a free individual never collides
520
+ with a free predicate (which would emit two ``consts`` of the same name).
521
+ """
522
+ if isinstance(node, Variable):
523
+ return free.binder.get((VARIABLE, node.name), node.name) if node.name in bvars else free.var[node.name]
524
+ if isinstance(node, Constant):
525
+ return free.ind[node.name]
526
+ if isinstance(node, Number):
527
+ raise NotImplementedError(
528
+ f"to_isabelle_so: the number {numeral_name(node.value)} is an operand of a comparison "
529
+ "with a cardinality (a natural number), not an individual: it cannot be the argument "
530
+ "of a predicate or a function.")
531
+ if isinstance(node, Function):
532
+ head = free.func[node.name]
533
+ return "(" + " ".join([head] + [_isa_term(a, bvars, free) for a in node.args]) + ")"
534
+ if isinstance(node, (Cardinality, SortedCardinality)):
535
+ raise NotImplementedError(
536
+ "to_isabelle_so: a cardinality term is supported only as an operand "
537
+ "of a comparison (= ≠ < > ≤ ≥), where it embeds as HOL's "
538
+ "``card {v. φ}``; as an argument of an uninterpreted predicate or "
539
+ "function it would need type nat where individuals are expected.")
540
+ raise NotImplementedError(
541
+ f"to_isabelle_so: unsupported term {type(node).__name__}.")
542
+
543
+
544
+ #: HOL spellings of the numeric comparison operators used for cardinalities.
545
+ _CARD_COMPARE = {"=": "=", "<": "<", ">": ">",
546
+ "≤": "\\<le>", "≥": "\\<ge>"}
547
+
548
+
549
+ def _isa_card_operand(node: Node, bpreds: FrozenSet[str], bvars: FrozenSet[str],
550
+ free: "_FreeNames") -> str:
551
+ """Render one operand of a numeric cardinality comparison (type ``nat``).
552
+
553
+ A cardinality embeds as HOL's native finite-set cardinality
554
+ ``card {v. φ}`` (``Finite_Set.card``, available from ``Main``); the sorted
555
+ variant guards the matrix with its sort predicate. A number is a ``nat``
556
+ literal. Anything else is a category error — the comparison is numeric, so
557
+ an individual-typed operand cannot appear (the same rule the Tarskian
558
+ evaluator enforces).
559
+ """
560
+ if isinstance(node, (Cardinality, SortedCardinality)):
561
+ v = free.binder.get((VARIABLE, node.variable.name), node.variable.name)
562
+ matrix = _isa(node.formula, bpreds, bvars | {node.variable.name}, free)
563
+ if isinstance(node, SortedCardinality):
564
+ guard = free.pred[node.sort]
565
+ matrix = f"(({guard} {v}) \\<and> {matrix})"
566
+ return f"(card {{{v}. {matrix}}})"
567
+ if isinstance(node, Number):
568
+ return numeral_name(node.value) # a whole-number float is the natural it equals: 2.0 is 2
569
+ raise NotImplementedError(
570
+ "to_isabelle_so: a comparison with a cardinality operand is NUMERIC — "
571
+ "the other operand must be a Number or another cardinality term, not an "
572
+ f"individual ({type(node).__name__}).")
573
+
574
+
575
+ def _isa(node: Node, bpreds: FrozenSet[str], bvars: FrozenSet[str], free: "_FreeNames") -> str:
576
+ """Render a second-order formula as an Isabelle/HOL boolean term."""
577
+ if isinstance(node, Atom):
578
+ if truth_value(node) is not None:
579
+ return "True" if truth_value(node) else "False"
580
+ if (node.predicate in ("=", "≠") or node.predicate in _CARD_COMPARE) \
581
+ and len(node.args) == 2 \
582
+ and any(isinstance(a, (Cardinality, SortedCardinality))
583
+ for a in node.args):
584
+ # Numeric reading: a cardinality IS a natural number (HOL's card),
585
+ # so the comparison is arithmetic over nat, not an uninterpreted
586
+ # relation over individuals — mirroring semantics.tarski.
587
+ left = _isa_card_operand(node.args[0], bpreds, bvars, free)
588
+ right = _isa_card_operand(node.args[1], bpreds, bvars, free)
589
+ if node.predicate == "≠":
590
+ return f"(\\<not> ({left} = {right}))"
591
+ return f"({left} {_CARD_COMPARE[node.predicate]} {right})"
592
+ head = (free.binder.get((PREDICATE, node.predicate), node.predicate) if node.predicate in bpreds
593
+ else free.pred[node.predicate])
594
+ if not node.args:
595
+ return head
596
+ return "(" + " ".join([head] + [_isa_term(a, bvars, free) for a in node.args]) + ")"
597
+ if isinstance(node, Not):
598
+ return f"({_ISA['not']} {_isa(node.formula, bpreds, bvars, free)})"
599
+ if isinstance(node, And):
600
+ return f"({_isa(node.left, bpreds, bvars, free)} {_ISA['and']} {_isa(node.right, bpreds, bvars, free)})"
601
+ if isinstance(node, Or):
602
+ return f"({_isa(node.left, bpreds, bvars, free)} {_ISA['or']} {_isa(node.right, bpreds, bvars, free)})"
603
+ if isinstance(node, Xor):
604
+ # x XOR y ≡ ¬(x ↔ y)
605
+ return f"({_ISA['not']} ({_isa(node.left, bpreds, bvars, free)} {_ISA['iff']} {_isa(node.right, bpreds, bvars, free)}))"
606
+ if isinstance(node, Implies):
607
+ return f"({_isa(node.left, bpreds, bvars, free)} {_ISA['imp']} {_isa(node.right, bpreds, bvars, free)})"
608
+ if isinstance(node, Iff):
609
+ return f"({_isa(node.left, bpreds, bvars, free)} {_ISA['iff']} {_isa(node.right, bpreds, bvars, free)})"
610
+ if isinstance(node, Quantifier):
611
+ x = node.variable.name
612
+ q = _ISA["forall"] if node.type in (_FORALL, "forall") else _ISA["exists"]
613
+ inner = _isa(node.formula, bpreds, bvars | {x}, free)
614
+ return f"({q}{free.binder.get((VARIABLE, x), x)}::i. {inner})"
615
+ if isinstance(node, SecondOrderQuantifier):
616
+ p = node.predicate
617
+ q = _ISA["forall"] if node.type in (_FORALL, "forall") else _ISA["exists"]
618
+ typ = _isa_pred_type(node.arity)
619
+ inner = _isa(node.formula, bpreds | {p}, bvars, free)
620
+ return f"({q}{free.binder.get((PREDICATE, p), p)}::{typ}. {inner})"
621
+ raise NotImplementedError(
622
+ f"to_isabelle_so: {type(node).__name__} is outside the second-order "
623
+ "fragment supported by the Isabelle export.")
624
+
625
+
626
+ def _isa_signature(formula: Node, free: "_FreeNames") -> List[str]:
627
+ """``consts`` declarations for free predicates / constants / functions.
628
+
629
+ Each symbol is declared under the unique functor ``free`` assigned it, so a free
630
+ predicate and a free individual that both lower-case to the same name get two
631
+ DISTINCT ``consts`` (Isabelle rejects duplicate constant declarations).
632
+ """
633
+ pred_arity, const_names, func_arity, free_vars = _signature(formula)
634
+ decls = []
635
+ for kind, name in _individual_symbols(const_names, free_vars):
636
+ decls.append(f"consts {(free.var if kind == FREE_VARIABLE else free.ind)[name]} :: \"i\"")
637
+ for name, arity in sorted(func_arity.items()):
638
+ decls.append(f"consts {free.func[name]} :: \"{_isa_fun_type(arity)}\"")
639
+ for name, arity in sorted(pred_arity.items()):
640
+ decls.append(f"consts {free.pred[name]} :: \"{_isa_pred_type(arity)}\"")
641
+ return decls
642
+
643
+
644
+ def to_isabelle_so(formula: Node, name: str = "SO_Goal") -> str:
645
+ """Emit a self-contained Isabelle/HOL theory for a second-order formula.
646
+
647
+ Individuals inhabit an uninterpreted type ``i``; a predicate variable ``P`` of
648
+ arity ``k`` is bound at type ``i ⇒ … ⇒ i ⇒ bool``, so each ``∀P`` / ``∃P``
649
+ becomes a native HOL ``∀`` / ``∃`` over a predicate type — the direct SOL → HOL
650
+ reading. Free predicate / constant / function symbols (and free object
651
+ variables, as individual constants) are introduced with ``consts``; the
652
+ formula is stated as a ``lemma`` left ``oops`` (replace with ``by auto`` /
653
+ ``by blast`` / ``sledgehammer``).
654
+
655
+ As with :func:`to_thf_so`, this is **standard (full)** second-order semantics
656
+ in Isabelle/HOL; SOL validity is not semi-decidable, so Sledgehammer may fail
657
+ on a valid lemma. The toolkit emits the theory only — it does not run
658
+ Isabelle. Equality ``=`` / ``≠`` is an uninterpreted relation (``feq`` /
659
+ ``fneq``), not HOL ``=``.
660
+
661
+ ``name`` becomes the theory name; it must be a legal Isabelle identifier (it is
662
+ also conventionally the ``.thy`` file's base name).
663
+ """
664
+ formula = _numerals_as_constants(formula, "to_isabelle_so")
665
+ free = _FreeNames(formula)
666
+ declarations = _isa_signature(formula, free)
667
+ # a binder shadows a constant of its own spelling inside its scope: it is printed under a
668
+ # name that no declared symbol has
669
+ free.binder = binder_tokens(collect_binders([formula]), declared_names(declarations))
670
+ body = _isa(formula, frozenset(), frozenset(), free)
671
+ lines = [
672
+ "(* Direct second-order -> HOL embedding (predicate quantifiers native). *)",
673
+ "(* Standard (full) second-order semantics; SOL validity is NOT *)",
674
+ "(* semi-decidable, so Sledgehammer may fail on a valid lemma. *)",
675
+ f"theory {name}",
676
+ " imports Main",
677
+ "begin",
678
+ "",
679
+ "typedecl i \\<comment> \\<open>individuals\\<close>",
680
+ ]
681
+ lines += declarations
682
+ lines.append("")
683
+ lines.append(f"lemma \"{body}\"")
684
+ lines.append(" oops \\<comment> \\<open>try: by auto / by blast / sledgehammer\\<close>")
685
+ lines.append("")
686
+ lines.append("end")
687
+ return "\n".join(lines) + "\n"