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,566 @@
1
+ """Kit AST -> Prolog / Datalog clauses.
2
+
3
+ The missing return leg of :mod:`unicode_logic_kit.fol.prolog_input`: that module
4
+ reads a fact or a definite/normal clause into a kit formula; this module goes
5
+ back the other way, for a formula that was BUILT to look like one — a rule
6
+ mined from a structure, a hand-written class definition, or the classical
7
+ reading of a clause that came from somewhere else and needs to travel back out
8
+ as text a Prolog engine (or a rule learner such as Popper) can consume.
9
+
10
+ **Not a ``Node.to_prolog()`` method.** Every other exporter in this package
11
+ (``to_tptp``, ``to_prover9``, ``to_latex``, ``to_z3``) is a total syntactic
12
+ recursion: every node type has *some* rendering in the target syntax, so
13
+ spreading the method across every AST class is the natural shape. Prolog is
14
+ different — it can only express a narrow shape (a fact, or a single-headed
15
+ implication whose body combines facts with ``,``/``;``/opt-in ``\\+``), and
16
+ most formulas have **no** Prolog reading at all. A method on every node class
17
+ would need to raise from nearly every one of them; a single function that
18
+ inspects the *whole* clause shape up front and refuses by name is both more
19
+ honest about what this format can hold and mirrors how
20
+ :func:`~unicode_logic_kit.fol.prolog_input.parse_prolog_clause` reads it back in
21
+ one place rather than one grammar rule per node type.
22
+
23
+ **Equivalence-preserving by construction, not by clausification.** The
24
+ accepted fragment is the EXACT syntactic mirror of
25
+ :func:`~unicode_logic_kit.fol.prolog_input.parse_prolog_clause`'s ``mode="clause"``
26
+ reading:
27
+
28
+ - a bare (or ``∀``-closed) :class:`~unicode_logic_kit.fol.nodes.Atom` — a fact,
29
+ ``pred(args).``;
30
+ - a (possibly ``∀``-prefixed)
31
+ :class:`~unicode_logic_kit.fol.nodes.Implies`\\ ``(body, head)`` where ``head``
32
+ is a single atom and ``body`` is built only from
33
+ :class:`~unicode_logic_kit.fol.nodes.And`/:class:`~unicode_logic_kit.fol.nodes.Or`/
34
+ :class:`~unicode_logic_kit.fol.nodes.Atom`/``Not(Atom)`` — ``Head :- Body.``.
35
+
36
+ Nothing else is rendered — it is REFUSED, by name, the same way
37
+ :func:`unicode_logic_kit.fol.normalforms._unsupported_hint` points a caller at
38
+ the right tool for a node the classical normal forms cannot take either. The
39
+ direct shape check ALONE is enough to refuse everything outside the fragment
40
+ soundly; :func:`~unicode_logic_kit.fol.normalforms.is_horn` is not used as a
41
+ gate on top of it (a formula using ``\\+`` classically is typically not even
42
+ a classical Horn clause — ``Q(x) ∧ ¬R(x) → P(x)`` clausifies to
43
+ ``¬Q(x) ∨ R(x) ∨ P(x)``, two positive literals — so gating acceptance on
44
+ ``is_horn`` would refuse exactly the clauses the negation-as-failure opt-in
45
+ below exists to allow). It is used for exactly one thing: when the direct
46
+ shape check has ALREADY failed (the formula is not directly a fact or a
47
+ ``body → head`` implication), ``is_horn`` checks whether the formula's
48
+ skolemised/CNF clausal form would nonetheless be Horn, and if so the refusal
49
+ says so explicitly — because that is the dangerous NEAR MISS the original
50
+ version of this feature would have silently mis-rendered: skolemize is
51
+ satisfiability-, not equivalence-preserving (see
52
+ :mod:`unicode_logic_kit.fol.normalforms`'s own docstring), so a naive
53
+ "``is_horn(node)`` is True, so clausify and emit" exporter would have swapped
54
+ equivalence for mere equisatisfiability. That check can only ever ADD a more
55
+ specific reason to a refusal that was already happening — it never turns a
56
+ refusal into an acceptance.
57
+
58
+ **Negation as failure is opt-in, and is NOT classical negation** — the same
59
+ discipline the importer applies, inverted. ``Not(Atom)`` in the body only
60
+ renders as ``\\+ Atom`` when the caller passes
61
+ ``negation_as_failure="classical"``; otherwise it is refused, naming the same
62
+ "closed world assumption" reason
63
+ :func:`~unicode_logic_kit.fol.prolog_input.parse_prolog_clause` gives on the way
64
+ in. Passing the opt-in makes the round trip syntactically faithful (the
65
+ emitted ``\\+`` reads back as the same ``Not(Atom)``), but it does **not**
66
+ make ``\\+`` and ``¬`` mean the same thing when the emitted program is
67
+ actually *run*: ``\\+ G`` succeeds whenever Prolog FAILS TO PROVE ``G``, which
68
+ agrees with ``¬G`` only when the program is COMPLETE for ``G`` — every ground
69
+ instance that is true (in whatever structure the program is meant to model)
70
+ is also derivable from the program. When the program is incomplete — some
71
+ true fact about ``G`` was never asserted — ``\\+ G`` succeeds where ``¬G`` is
72
+ in fact false. That disagreement (not a hypothetical: see
73
+ ``tests/test_prolog_export.py::test_naf_disagrees_with_classical_negation_on_an_incomplete_program``,
74
+ which exhibits it with the kit's own resolution prover as the independent
75
+ oracle) is exactly why the opt-in exists rather than a silent default: passing
76
+ ``negation_as_failure="classical"`` is the caller asserting that their program
77
+ is, or will be run as, complete for every negated predicate.
78
+
79
+ **Naming inverts the importer's fold exactly.** A predicate is folded to
80
+ Prolog's lower-case-initial spelling (only the FIRST character — mirroring
81
+ :func:`unicode_logic_kit.ilp.task.to_prolog_atom` and
82
+ :func:`unicode_logic_kit.fol.prolog_input._cap`/``_lower`` rather than importing
83
+ either, so a divergence between the three shows up as a failing round-trip
84
+ test instead of leaking silently); a name that does not fold back to the
85
+ EXACT original spelling (i.e. does not start with an upper-case letter — the
86
+ kit's own signal for predicate-hood, see
87
+ :mod:`unicode_logic_kit.fol._identifiers`) is refused rather than exported under
88
+ a spelling that would not read back to itself. A constant or function name is
89
+ emitted VERBATIM (the importer never folds one), quoted with Prolog's
90
+ ``'...'`` syntax whenever it is not already a legal bare atom. Every variable
91
+ in a clause is renamed to a fresh, upper-case-initial Prolog spelling
92
+ (``V0``, ``V1``, ...) — the same convention
93
+ :meth:`~unicode_logic_kit.fol.nodes.Variable.to_prover9` uses under
94
+ ``set(prolog_style_variables)`` — assigned in the SAME alphabetical order
95
+ :func:`~unicode_logic_kit.fol.prolog_input.parse_prolog_clause` itself closes
96
+ variables in, so the round trip lands on the identical quantifier nesting,
97
+ not merely an alpha-equivalent one. Prolog variable spelling is scoped to one
98
+ clause and carries no meaning beyond identity, so the original kit name is
99
+ never preserved (nor does it need to be).
100
+
101
+ **Numbers and zero-arity function terms are refused, not approximated,
102
+ when Prolog has no exact reading for them.** A :class:`Number` whose value
103
+ would print outside :func:`~unicode_logic_kit.fol.prolog_input.parse_prolog_clause`'s
104
+ numeral grammar — scientific notation for a very large/small float, or the
105
+ non-finite ``nan``/``inf``/``-inf`` — is refused rather than emitted as text
106
+ the importer cannot read back (``nan``/``inf`` are worse than a parse error:
107
+ they would silently reparse as a *different* node type, :class:`Constant`,
108
+ with no error at all). A zero-argument :class:`Function` is refused for the
109
+ same reason: Prolog syntax cannot write ``f()`` distinct from the bare atom
110
+ ``f``, so it would reparse as a :class:`Constant`, not a :class:`Function` —
111
+ confirmed with the kit's own resolution prover that the two are not
112
+ logically equivalent.
113
+
114
+ **One numeral per value.** ``Number(1)`` and ``Number(1.0)`` are equal nodes, one constant,
115
+ but Prolog keeps the integer ``1`` and the float ``1.0`` apart (they do not unify), so a
116
+ float with a whole value is written as the integer it equals: ``p(1.0)`` is exported as
117
+ ``p(1)``, and ``p(1) :- p(1.0)`` as ``p(1) :- p(1)``. A :class:`Constant` named ``'1'``
118
+ is the quoted atom ``'1'``, another Prolog term than the number ``1``, and is read back as
119
+ the constant.
120
+
121
+ Public API: :func:`formula_to_prolog_clause` (one clause) and
122
+ :func:`formula_to_prolog_program` (several, one per line, splitting a
123
+ top-level conjunction the way
124
+ :func:`~unicode_logic_kit.fol.prolog_input.parse_prolog_program` returns several
125
+ clauses rather than one).
126
+ """
127
+
128
+ import re
129
+ from typing import Iterable, List, Union
130
+
131
+ from .nodes import (
132
+ Node, Variable, Constant, Number, Function, Atom, Not, And, Or, Implies,
133
+ Quantifier, free_variables,
134
+ )
135
+ from .normalforms import is_horn, _unsupported_hint
136
+ from ._numeral_symbols import numeral_name
137
+ from ._truth_constants import refuse_truth_constants
138
+
139
+ __all__ = [
140
+ "PrologExportError", "formula_to_prolog_clause", "formula_to_prolog_program",
141
+ ]
142
+
143
+ _FORALL = ("∀", "forall")
144
+
145
+ #: Mirrors :data:`unicode_logic_kit.fol.prolog_input._ATOM_RE` exactly (a legal
146
+ #: BARE Prolog atom) rather than importing it — the same "duplicate a tiny
147
+ #: naming rule so a divergence fails a test instead of leaking silently"
148
+ #: convention every importer/exporter pair in this package already follows
149
+ #: (see :func:`unicode_logic_kit.fol.prolog_input._cap`'s docstring).
150
+ _ATOM_RE = re.compile(r"^[a-z][A-Za-z0-9_]*$")
151
+
152
+ #: Mirrors :data:`unicode_logic_kit.fol.prolog_input._NUMBER_RE` exactly (the
153
+ #: only numeral shape ``parse_prolog_clause`` reads back — a plain, optionally
154
+ #: signed, optionally decimal literal; no exponent, no ``nan``/``inf``), same
155
+ #: duplicate-rather-than-import convention as ``_ATOM_RE`` above. Python's
156
+ #: ``str()`` of a :class:`~unicode_logic_kit.fol.nodes.Number`'s ``value`` falls
157
+ #: outside this grammar for very large/small floats (``str(1e20) ==
158
+ #: '1e+20'``) and for the non-finite floats ``nan``/``inf``/``-inf`` — those
159
+ #: are refused in :func:`_render_term` rather than emitted as text the kit's
160
+ #: own importer could not read back (or, worse for ``nan``/``inf``, would
161
+ #: silently read back as a *different* node type, :class:`Constant`, with no
162
+ #: error at all).
163
+ _NUMBER_RE = re.compile(r"^-?\d+(?:\.\d+)?$")
164
+
165
+
166
+ class PrologExportError(ValueError):
167
+ """Raised when a formula has no sound Prolog clause reading.
168
+
169
+ Not a :class:`~unicode_logic_kit.fol.naming.ParsingError` subclass — this
170
+ module never parses anything; it is the exporter's own refusal, in the
171
+ same spirit as :mod:`unicode_logic_kit.fol.casl_export`'s ``ValueError``
172
+ exceptions and :class:`~unicode_logic_kit.fol.tptp_repair.TptpRepairError`.
173
+ Every message names the offending construct or reason, never a bare "invalid
174
+ formula".
175
+ """
176
+
177
+
178
+ # ---------------------------------------------------------------------------
179
+ # Naming
180
+ # ---------------------------------------------------------------------------
181
+
182
+ def _quote_if_needed(text: str) -> str:
183
+ """Return ``text`` bare if it is already a legal Prolog atom, else quoted.
184
+
185
+ The escaping is the exact inverse of
186
+ :func:`unicode_logic_kit.fol.prolog_input._unquote`: a backslash or a single
187
+ quote inside the content is backslash-escaped, nothing else is touched.
188
+ """
189
+ if _ATOM_RE.fullmatch(text):
190
+ return text
191
+ escaped = text.replace("\\", "\\\\").replace("'", "\\'")
192
+ return f"'{escaped}'"
193
+
194
+
195
+ def _predicate_text(name: str) -> str:
196
+ """The Prolog spelling of a kit PREDICATE name — fold the first character
197
+ down, then quote if the result is not a bare atom.
198
+
199
+ :func:`~unicode_logic_kit.fol.prolog_input.parse_prolog_clause` folds a
200
+ Prolog functor's first character UP to build a predicate name (``_cap``),
201
+ on both the bare and the quoted route (see that module's docstring for
202
+ why a quoted ``'1,2-diacyl'`` still "keeps its exact characters" despite
203
+ the unconditional fold: ``_cap`` is a no-op on a non-letter first
204
+ character). Inverting that fold is only sound when the kit's own name
205
+ starts with the upper-case half of the SAME fold — i.e. when it is
206
+ already a legal kit predicate spelling in the first place (the
207
+ first-letter-is-upper-case convention every kit predicate is parsed
208
+ under; see :mod:`unicode_logic_kit.fol._identifiers`). A name that does not
209
+ satisfy this is refused rather than exported under a spelling that would
210
+ not read back to itself.
211
+ """
212
+ folded = name[:1].lower() + name[1:] if name else name
213
+ refolded = folded[:1].upper() + folded[1:] if folded else folded
214
+ if refolded != name:
215
+ raise PrologExportError(
216
+ f"formula_to_prolog_clause: the predicate name {name!r} does not "
217
+ "start with an upper-case letter, so it is not a legal kit "
218
+ "predicate spelling in the first place (every kit predicate is "
219
+ "parsed as starting upper-case — see "
220
+ "unicode_logic_kit.fol._identifiers) and folding it to a Prolog "
221
+ "functor would not read back to this exact name.")
222
+ return _quote_if_needed(folded)
223
+
224
+
225
+ def _term_text(name: str) -> str:
226
+ """The Prolog spelling of a kit CONSTANT or FUNCTION name — verbatim,
227
+ quoted if it is not already a legal bare atom.
228
+
229
+ Unlike a predicate, :func:`~unicode_logic_kit.fol.prolog_input.parse_prolog_clause`
230
+ never folds a constant or function name's case (see its module
231
+ docstring's naming section and ``tests/test_prolog_input.py``'s
232
+ ``test_a_compound_term_becomes_a_function_not_a_predicate``), so no
233
+ inversion is needed here — only quoting.
234
+ """
235
+ return _quote_if_needed(name)
236
+
237
+
238
+ def _variable_names(variables) -> dict:
239
+ """``{kit Variable name -> fresh Prolog spelling}`` for one clause.
240
+
241
+ Every variable becomes ``V0``, ``V1``, ... — upper-case-initial, the
242
+ convention :meth:`~unicode_logic_kit.fol.nodes.Variable.to_prover9` also
243
+ uses under ``set(prolog_style_variables)`` — assigned in the SAME
244
+ alphabetical order (by the ORIGINAL kit name) that
245
+ :func:`~unicode_logic_kit.fol.prolog_input.parse_prolog_clause`'s own
246
+ ``_close`` helper uses to re-quantify them on the way back in. Because
247
+ the fresh names are zero-padded to a common width, sorting them
248
+ LEXICALLY (which is what ``_close`` does on re-import) reproduces this
249
+ exact assignment order regardless of how many variables the clause has —
250
+ so the round trip lands on the identical quantifier nesting, not merely
251
+ an alpha-equivalent one.
252
+ """
253
+ ordered = sorted(variables, key=lambda v: v.name)
254
+ width = max(1, len(str(max(len(ordered) - 1, 0))))
255
+ return {v.name: f"V{i:0{width}d}" for i, v in enumerate(ordered)}
256
+
257
+
258
+ # ---------------------------------------------------------------------------
259
+ # Terms
260
+ # ---------------------------------------------------------------------------
261
+
262
+ def _render_term(node: Node, names: dict) -> str:
263
+ if isinstance(node, Variable):
264
+ try:
265
+ return names[node.name]
266
+ except KeyError: # pragma: no cover — every free variable of the
267
+ # clause is registered by the caller before rendering starts.
268
+ raise PrologExportError(
269
+ f"formula_to_prolog_clause: internal error — variable "
270
+ f"{node.name!r} was not in the clause's own free-variable set")
271
+ if isinstance(node, Constant):
272
+ return _term_text(node.name)
273
+ if isinstance(node, Number):
274
+ text = str(node.value)
275
+ if not _NUMBER_RE.fullmatch(text):
276
+ raise PrologExportError(
277
+ f"formula_to_prolog_clause: the number {node.value!r} would "
278
+ f"render as {text!r}, which is outside "
279
+ "parse_prolog_clause's numeral grammar (-?\\d+(\\.\\d+)?, no "
280
+ "exponent, no nan/inf) and would not read back to this exact "
281
+ "value — refused rather than emitted as text the kit's own "
282
+ "importer could not parse (or, for nan/inf, would silently "
283
+ "misread as a Constant instead of a Number)")
284
+ # A numeral is identified by its VALUE: Prolog keeps the integer 1 and the float 1.0 apart
285
+ # (they do not unify), the kit's Number(1) == Number(1.0) does not, so a float with a
286
+ # whole value is written as the integer it equals.
287
+ return numeral_name(node.value)
288
+ if isinstance(node, Function):
289
+ if not node.args:
290
+ raise PrologExportError(
291
+ f"formula_to_prolog_clause: {node.name!r} is a 0-arity "
292
+ "Function term — Prolog syntax has no way to write 'f()' "
293
+ "distinct from the bare atom 'f', so parse_prolog_clause "
294
+ "would read the emitted text back as a Constant, not a "
295
+ "Function (a genuine change of node type, not merely of "
296
+ "spelling — the kit's own resolution prover confirms "
297
+ "Atom('p', [Function('f', [])]) and Atom('p', "
298
+ "[Constant('f')]) are not logically equivalent in either "
299
+ "direction). Use Constant instead if a bare atom is what is "
300
+ "meant.")
301
+ args = ", ".join(_render_term(a, names) for a in node.args)
302
+ return f"{_term_text(node.name)}({args})"
303
+ raise PrologExportError(
304
+ f"formula_to_prolog_clause: {type(node).__name__} has no Prolog term "
305
+ f"reading{_unsupported_hint(node)}")
306
+
307
+
308
+ # ---------------------------------------------------------------------------
309
+ # Atoms (predicate applications)
310
+ # ---------------------------------------------------------------------------
311
+
312
+ def _check_not_comparison(atom: Atom) -> None:
313
+ """Refuse ``=``/``≠``/``<``/``>``/``≤``/``≥`` — Prolog reads none of
314
+ them (see :mod:`unicode_logic_kit.fol.prolog_input`'s module docstring:
315
+ arithmetic comparison is one of the constructs it refuses on the way
316
+ in), so there is no sound way back out either.
317
+ """
318
+ if atom.predicate in Atom.INFIX_PREDS_P9:
319
+ raise PrologExportError(
320
+ f"formula_to_prolog_clause: {atom.predicate!r} is a comparison/"
321
+ "equality predicate — parse_prolog_clause has no reading for "
322
+ "'=', '≠', '<', '>', '≤', or '≥' (they are not part of the "
323
+ "accepted Prolog fragment on the way in either), so exporting it "
324
+ "would not round trip")
325
+
326
+
327
+ def _render_atom(atom: Atom, names: dict) -> str:
328
+ refuse_truth_constants(
329
+ [atom], "formula_to_prolog_clause",
330
+ "Prolog's own true / fail are control goals that cannot head a clause, "
331
+ "and parse_prolog_clause reads them back as the ordinary predicates "
332
+ "True / Fail, so the clause would not round trip",
333
+ error=PrologExportError)
334
+ _check_not_comparison(atom)
335
+ predicate = _predicate_text(atom.predicate)
336
+ if not atom.args:
337
+ return predicate
338
+ args = ", ".join(_render_term(a, names) for a in atom.args)
339
+ return f"{predicate}({args})"
340
+
341
+
342
+ # ---------------------------------------------------------------------------
343
+ # Bodies: And / Or / Atom / Not(Atom)
344
+ # ---------------------------------------------------------------------------
345
+
346
+ def _render_body(node: Node, names: dict, negation_as_failure: str) -> str:
347
+ if isinstance(node, Atom):
348
+ return _render_atom(node, names)
349
+ if isinstance(node, Not):
350
+ if negation_as_failure != "classical":
351
+ raise PrologExportError(
352
+ "formula_to_prolog_clause: the body contains classical "
353
+ f"negation ({node.to_unicode_str()!r}), which has no sound "
354
+ "'\\+' reading unless the caller asserts the closed world "
355
+ "assumption holds for the program the clause will run in — "
356
+ "pass negation_as_failure='classical' to opt in (see the "
357
+ "module docstring for exactly when '\\+' and classical ¬ "
358
+ "agree, and when they do not)")
359
+ if not isinstance(node.formula, Atom):
360
+ raise PrologExportError(
361
+ "formula_to_prolog_clause: only '\\+' applied to a single "
362
+ f"atom is exported; {type(node.formula).__name__} inside "
363
+ f"'\\+' ({node.to_unicode_str()!r}) is outside the accepted "
364
+ "fragment (parse_prolog_clause's own body grammar allows "
365
+ "'\\+' to nest further, but this exporter deliberately "
366
+ "narrows to '\\+ Atom' only)")
367
+ return f"\\+ {_render_atom(node.formula, names)}"
368
+ if isinstance(node, And):
369
+ left = _render_body_operand(node.left, names, negation_as_failure, "and")
370
+ right = _render_body_operand(node.right, names, negation_as_failure, "and")
371
+ return f"{left}, {right}"
372
+ if isinstance(node, Or):
373
+ left = _render_body_operand(node.left, names, negation_as_failure, "or")
374
+ right = _render_body_operand(node.right, names, negation_as_failure, "or")
375
+ return f"{left} ; {right}"
376
+ raise PrologExportError(
377
+ f"formula_to_prolog_clause: {type(node).__name__} has no Prolog body "
378
+ f"reading{_unsupported_hint(node)} — a body may only combine facts "
379
+ "with ',' (∧), ';' (∨), and opt-in '\\+' (see the module docstring "
380
+ "for the accepted fragment)")
381
+
382
+
383
+ def _render_body_operand(node: Node, names: dict, negation_as_failure: str,
384
+ parent: str) -> str:
385
+ """Render one operand of And/Or, parenthesising only where Prolog's
386
+ precedence (',' binds tighter than ';') would otherwise change the
387
+ parse: an Or nested inside an And's operand position.
388
+
389
+ Every other nesting (And-in-And, Or-in-Or, either under a parent Or, a
390
+ bare Atom or '\\+ Atom' anywhere) already parses back to the intended
391
+ shape without parentheses — and even where the exact associativity
392
+ differs from the input tree, that difference is one
393
+ :func:`unicode_logic_kit.eval.canonical.canonicalize` already quotients
394
+ out (And/Or are flattened and re-sorted there), so it is never a
395
+ round-trip risk.
396
+ """
397
+ text = _render_body(node, names, negation_as_failure)
398
+ if parent == "and" and isinstance(node, Or):
399
+ return f"({text})"
400
+ return text
401
+
402
+
403
+ # ---------------------------------------------------------------------------
404
+ # Clauses
405
+ # ---------------------------------------------------------------------------
406
+
407
+ def _strip_foralls(node: Node) -> Node:
408
+ """Peel off every leading ``∀`` and return the matrix underneath.
409
+
410
+ The peeled variables are not inspected: whatever free variables remain
411
+ in the matrix (whether they were bound here, were free in the input
412
+ all along, or a caller's vacuous ``∀`` bound nothing at all) are exactly
413
+ what :func:`~unicode_logic_kit.fol.prolog_input.parse_prolog_clause`
414
+ re-quantifies on the way back in, so re-deriving them from the matrix
415
+ itself — rather than trusting the caller's prefix — is what makes both
416
+ a bare formula (``Implies(body, head)``, free variables and all) and an
417
+ already ``∀``-closed one valid input.
418
+ """
419
+ matrix = node
420
+ while isinstance(matrix, Quantifier) and matrix.type in _FORALL:
421
+ matrix = matrix.formula
422
+ return matrix
423
+
424
+
425
+ def formula_to_prolog_clause(node: Node, *, negation_as_failure: str = "refuse") -> str:
426
+ """Render ``node`` as ONE Prolog clause: a fact or a definite/normal rule.
427
+
428
+ The accepted fragment is the exact mirror of
429
+ :func:`~unicode_logic_kit.fol.prolog_input.parse_prolog_clause`'s
430
+ ``mode="clause"`` reading — see the module docstring. Anything else is
431
+ refused, naming the construct.
432
+
433
+ Args:
434
+ node: a bare or ``∀``-closed :class:`~unicode_logic_kit.fol.nodes.Atom`
435
+ (a fact), or a bare or ``∀``-closed
436
+ :class:`~unicode_logic_kit.fol.nodes.Implies`\\ ``(body, head)``
437
+ (a rule) whose ``head`` is a single atom and whose ``body`` is
438
+ built from And/Or/Atom/``Not(Atom)`` only.
439
+ negation_as_failure: ``"refuse"`` (default) — any ``Not`` in the body
440
+ is refused — or ``"classical"`` to render ``Not(Atom)`` as
441
+ ``\\+ Atom``, asserting that ``\\+`` and classical ¬ agree for
442
+ this clause's program (see the module docstring for exactly
443
+ when that holds).
444
+
445
+ Returns:
446
+ The clause text, ending in ``"."``.
447
+
448
+ Raises:
449
+ PrologExportError: ``node`` is outside the accepted fragment, its
450
+ head has a variable not bound by its body (not range-restricted),
451
+ a predicate/constant/function name cannot be rendered soundly, or
452
+ ``negation_as_failure`` is not one of the documented values.
453
+
454
+ Example:
455
+ >>> from unicode_logic_kit.fol.nodes import Atom, Variable, Implies, Quantifier
456
+ >>> from unicode_logic_kit.fol.prolog_export import formula_to_prolog_clause
457
+ >>> x = Variable("x")
458
+ >>> clause = Quantifier("∀", x, Implies(Atom("Human", [x]), Atom("Mortal", [x])))
459
+ >>> formula_to_prolog_clause(clause)
460
+ 'mortal(V0) :- human(V0).'
461
+ """
462
+ if negation_as_failure not in ("refuse", "classical"):
463
+ raise PrologExportError(
464
+ "formula_to_prolog_clause: unknown negation_as_failure="
465
+ f"{negation_as_failure!r}, expected 'refuse' or 'classical'")
466
+
467
+ matrix = _strip_foralls(node)
468
+
469
+ if isinstance(matrix, Atom):
470
+ names = _variable_names(free_variables(matrix))
471
+ return _render_atom(matrix, names) + "."
472
+
473
+ if isinstance(matrix, Implies):
474
+ body, head = matrix.left, matrix.right
475
+ if not isinstance(head, Atom):
476
+ raise PrologExportError(
477
+ "formula_to_prolog_clause: the head of a rule must be a "
478
+ f"single atom, got {type(head).__name__} "
479
+ f"({head.to_unicode_str()!r}) — a disjunctive or otherwise "
480
+ "compound head has no Prolog reading (a Prolog rule has "
481
+ "exactly one head literal)")
482
+ unbound = free_variables(head) - free_variables(body)
483
+ if unbound:
484
+ unbound_str = ", ".join(sorted(v.name for v in unbound))
485
+ raise PrologExportError(
486
+ f"formula_to_prolog_clause: the head variable(s) {unbound_str} "
487
+ "do not occur in the body — a Prolog rule with an unbound "
488
+ "head variable does not mean what the universally-quantified "
489
+ "formula meant (Prolog would leave it ranging over every "
490
+ "value at that argument position instead)")
491
+ names = _variable_names(free_variables(matrix))
492
+ body_text = _render_body(body, names, negation_as_failure)
493
+ head_text = _render_atom(head, names)
494
+ return f"{head_text} :- {body_text}."
495
+
496
+ hint = _unsupported_hint(matrix)
497
+ horn_note = ""
498
+ try:
499
+ if is_horn(node):
500
+ horn_note = (
501
+ " Note: is_horn(node) is True — the SKOLEMISED/CNF clausal "
502
+ "form happens to be Horn — but skolemize() is "
503
+ "satisfiability-preserving, not equivalence-preserving (see "
504
+ "unicode_logic_kit.fol.normalforms's docstring), so emitting "
505
+ "those clauses instead of this formula would silently swap "
506
+ "equivalence for mere equisatisfiability. "
507
+ "formula_to_prolog_clause only exports a formula that is "
508
+ "DIRECTLY a fact or a possibly ∀-prefixed body → head "
509
+ "implication; rewrite the formula into that shape by hand "
510
+ "if it should be exported.")
511
+ except Exception:
512
+ pass
513
+ raise PrologExportError(
514
+ f"formula_to_prolog_clause: {type(matrix).__name__} "
515
+ f"({node.to_unicode_str()!r}) has no Prolog clause reading{hint} — "
516
+ "only a fact (a bare atom) or a definite/normal clause (a possibly "
517
+ "∀-prefixed body → head implication) is exported; see the module "
518
+ f"docstring for the accepted fragment.{horn_note}")
519
+
520
+
521
+ def _conjuncts(node: Node) -> List[Node]:
522
+ """Flatten a top-level ``∧`` tree, left to right — the same shallow
523
+ split :mod:`unicode_logic_kit.ilp.readback` and
524
+ :mod:`unicode_logic_kit.fol.normalforms` each keep a private copy of
525
+ rather than importing, so a change to the convention shows up as a
526
+ failing test in each user instead of leaking silently."""
527
+ if isinstance(node, And):
528
+ return _conjuncts(node.left) + _conjuncts(node.right)
529
+ return [node]
530
+
531
+
532
+ def formula_to_prolog_program(nodes_or_conjunction: Union[Node, Iterable[Node]],
533
+ *, negation_as_failure: str = "refuse") -> str:
534
+ """Render several clauses as one Prolog program, one clause per line.
535
+
536
+ Args:
537
+ nodes_or_conjunction: either a single formula whose top level is an
538
+ ``∧`` of several fact/rule-shaped conjuncts (split the same way
539
+ :func:`~unicode_logic_kit.fol.prolog_input.parse_prolog_program`'s
540
+ multi-clause reading returns them separately rather than
541
+ disjoined), or any iterable of such formulas directly.
542
+ negation_as_failure: forwarded to :func:`formula_to_prolog_clause`
543
+ for every clause.
544
+
545
+ Returns:
546
+ The clauses' text, one per line, each ending in ``"."``.
547
+
548
+ Raises:
549
+ PrologExportError: as :func:`formula_to_prolog_clause`, for whichever
550
+ clause fails first, with that clause's position and text appended
551
+ — mirroring :func:`~unicode_logic_kit.fol.prolog_input.parse_prolog_program`'s
552
+ own ``"(in clause: ...)"`` suffix on the way in, so a failure in a
553
+ program of several clauses names which one, not just why.
554
+ """
555
+ clauses = (_conjuncts(nodes_or_conjunction) if isinstance(nodes_or_conjunction, Node)
556
+ else list(nodes_or_conjunction))
557
+ rendered = []
558
+ for index, clause in enumerate(clauses):
559
+ try:
560
+ rendered.append(formula_to_prolog_clause(
561
+ clause, negation_as_failure=negation_as_failure))
562
+ except PrologExportError as exc:
563
+ raise PrologExportError(
564
+ f"{exc} (in clause {index + 1} of {len(clauses)}: "
565
+ f"{clause.to_unicode_str()!r})")
566
+ return "\n".join(rendered)