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,505 @@
1
+ """Prolog / Datalog clauses -> kit AST.
2
+
3
+ The missing leg of the importer family (TPTP, Prover9, SMT-LIB, LaTeX, CASL).
4
+ Its immediate use is reading back what a rule learner (an ILP system such as
5
+ Popper or Aleph, or a hand-written Prolog program) produced, so the result can
6
+ be checked against a structure, proved with, exported to TPTP, or compared
7
+ against a reference formula — instead of being eyeballed.
8
+
9
+ **Two readings, and the caller must choose.** A definite clause
10
+
11
+ amide(A) :- carbon(C), nitrogen(N), single_bond(C, N), in(A, C).
12
+
13
+ says two different things depending on what you want from it:
14
+
15
+ ``mode="clause"`` (default) — the standard logical reading of the clause:
16
+ every variable universally quantified over the whole implication,
17
+
18
+ ∀a ∀c ∀n (Carbon(c) ∧ Nitrogen(n) ∧ Single_bond(c, n) ∧ In(a, c) → Amide(a))
19
+
20
+ ``mode="body"`` — only the CONDITION, with the variables that occur solely in
21
+ the body existentially closed and the head's own variables left free:
22
+
23
+ ∃c ∃n (Carbon(c) ∧ Nitrogen(n) ∧ Single_bond(c, n) ∧ In(a, c))
24
+
25
+ That second form is what a *class definition* is: the property a thing must
26
+ have, ready to model-check against one structure. The two are not the same
27
+ formula and this module will not guess which you meant — the same discipline
28
+ :func:`unicode_logic_kit.fol.tptp_repair.repair_tptp_formula` applies to a free
29
+ variable it could close two ways.
30
+
31
+ (Body-only variables are existentially closed in ``"body"`` mode and
32
+ universally closed in ``"clause"`` mode, and both are correct: for a variable
33
+ occurring only in the antecedent, ``∀x (P(x) → Q)`` and ``(∃x P(x)) → Q`` are
34
+ logically equivalent. ``"clause"`` mode emits the prenex form because that is
35
+ the shape every prover expects.)
36
+
37
+ **Negation as failure is not classical negation.** ``\\+ G`` succeeds when
38
+ Prolog fails to prove ``G``, which coincides with ``¬G`` only under the closed
39
+ world assumption on a stratified program. Reading it as ``¬`` silently would
40
+ turn "not derivable here" into "false everywhere", so it is REFUSED unless the
41
+ caller opts in with ``negation_as_failure="classical"`` and thereby states
42
+ that the assumption holds for their program.
43
+
44
+ **Naming is inverted on import**, exactly as in
45
+ :mod:`unicode_logic_kit.fol.tptp_input`: Prolog spells a predicate lower-case
46
+ and a variable upper-case, the kit does the opposite, so ``carbon(A)`` becomes
47
+ ``Carbon(a)``. A quoted atom (``'1,2-diacyl'``) keeps its exact characters,
48
+ which may not be a legal kit token — run
49
+ :func:`unicode_logic_kit.fol.sanitize_names` over the result before rendering it
50
+ back to text, the same route the TPTP importer documents.
51
+
52
+ **The accepted fragment**, and nothing beyond it: facts, definite and normal
53
+ clauses, bodies joined by ``,`` and ``;``, ``\\+`` (opt-in), compound terms,
54
+ numbers, and the anonymous variable ``_`` (each occurrence a fresh variable).
55
+ Lists, operators (``is``, ``=..``, arithmetic comparison), the cut ``!``, and
56
+ if-then ``->`` are REFUSED with a message naming what was found — a parser
57
+ that quietly dropped a cut would change what the program means.
58
+ """
59
+
60
+ import re
61
+ from typing import List, Optional, Tuple
62
+
63
+ from ._fol_nodes import _numeral_from_text
64
+ from .naming import ParsingError
65
+ from .nodes import (
66
+ Node, Variable, Constant, Number, Function, Atom, Not, And, Or, Implies,
67
+ Quantifier, free_variables,
68
+ )
69
+
70
+ __all__ = [
71
+ "PrologParsingError", "parse_prolog_clause", "parse_prolog_program",
72
+ "load_prolog",
73
+ ]
74
+
75
+
76
+ class PrologParsingError(ParsingError):
77
+ """A Prolog import failure carrying a plain message (subclasses
78
+ :class:`~unicode_logic_kit.fol.naming.ParsingError`, like every other
79
+ importer's error type).
80
+
81
+ The base class builds its message from a Lark exception; this importer has
82
+ no Lark parser to hand one, so the constructor is overridden to take the
83
+ message directly — the same shape
84
+ :class:`~unicode_logic_kit.fol.prover9_input.Prover9ParsingError` uses, for
85
+ the same reason.
86
+ """
87
+
88
+ def __init__(self, message: str):
89
+ self.args = (message,)
90
+
91
+ def __str__(self) -> str:
92
+ return self.args[0]
93
+
94
+
95
+ #: Constructs this module refuses rather than approximates, mapped to the
96
+ #: reason. Each would change the meaning of a clause if silently dropped or
97
+ #: guessed at, which is the one thing an importer must never do.
98
+ _REFUSED = {
99
+ "!": "the cut (!) — a control construct with no truth-conditional reading",
100
+ "->": "if-then (->) — Prolog's is a committed choice, not material "
101
+ "implication, and the two disagree when the condition fails",
102
+ "is": "arithmetic evaluation (is) — a computation, not a relation",
103
+ "=..": "the univ operator (=..) — term reflection has no first-order form",
104
+ }
105
+
106
+ _VAR_RE = re.compile(r"^(?:[A-Z_][A-Za-z0-9_]*)$")
107
+ _ATOM_RE = re.compile(r"^[a-z][A-Za-z0-9_]*$")
108
+ _NUMBER_RE = re.compile(r"^-?\d+(?:\.\d+)?$")
109
+
110
+
111
+ def _cap(name: str) -> str:
112
+ """Capitalise the first character — the kit's predicate spelling.
113
+
114
+ Mirrors :func:`unicode_logic_kit.fol.tptp_input._cap` rather than importing
115
+ it, so the two importers' conventions are visibly the same rule and a
116
+ change to either shows up as a failing round-trip test rather than as a
117
+ silent divergence.
118
+ """
119
+ return name[:1].upper() + name[1:] if name else name
120
+
121
+
122
+ def _lower(name: str) -> str:
123
+ """Lower-case the first character — the kit's variable spelling."""
124
+ return name[:1].lower() + name[1:] if name else name
125
+
126
+
127
+ # ---------------------------------------------------------------------------
128
+ # Tokeniser / reader
129
+ # ---------------------------------------------------------------------------
130
+ #
131
+ # A hand-written reader rather than a Lark grammar, for one reason: Prolog's
132
+ # surface syntax is operator-driven and a grammar that accepted "enough of it"
133
+ # would accept operators this module has no reading for. Reading the exact
134
+ # fragment by hand makes every refusal explicit and located.
135
+
136
+ _TOKEN_RE = re.compile(r"""
137
+ (?P<quoted>'(?:\\.|[^'\\])*') # 'quoted atom'
138
+ | (?P<neg>\\\+) # \+ (negation as failure)
139
+ | (?P<neck>:-) # :-
140
+ | (?P<arrow>->) # -> (refused; tokenised so the
141
+ # refusal message can name it)
142
+ | (?P<univ>=\.\.) # =..
143
+ | (?P<punct>[(),.;\[\]|!]) # structural
144
+ | (?P<name>[A-Za-z_][A-Za-z0-9_]*) # atom or variable
145
+ | (?P<number>-?\d+(?:\.\d+)?) # number
146
+ | (?P<other>\S) # anything else -> refused, located
147
+ """, re.VERBOSE)
148
+
149
+
150
+ _LINE_BREAK_RE = re.compile(r"\r\n?|\n")
151
+
152
+
153
+ def _past_line_comment(text: str, index: int) -> int:
154
+ """The index just past the '%' line comment starting at ``index``: past
155
+ the first line break of ANY convention (LF, CRLF or a bare CR -- text
156
+ handed over as a string, e.g. a learner's hypothesis, may use any), or
157
+ the end of ``text``."""
158
+ match = _LINE_BREAK_RE.search(text, index)
159
+ return len(text) if match is None else match.end()
160
+
161
+
162
+ class _Reader:
163
+ """Position-tracking token cursor over one clause's text."""
164
+
165
+ def __init__(self, text: str):
166
+ self.text = text
167
+ self.tokens: List[Tuple[str, str, int]] = []
168
+ index = 0
169
+ while index < len(text):
170
+ if text[index] in " \t\r\n":
171
+ index += 1
172
+ continue
173
+ if text[index] == "%": # line comment
174
+ index = _past_line_comment(text, index)
175
+ continue
176
+ match = _TOKEN_RE.match(text, index)
177
+ if match is None: # pragma: no cover
178
+ raise PrologParsingError(
179
+ f"SYNTAX_ERROR: unreadable character at position {index}")
180
+ kind = match.lastgroup
181
+ self.tokens.append((kind, match.group(), match.start()))
182
+ index = match.end()
183
+ self.position = 0
184
+
185
+ def peek(self) -> Optional[Tuple[str, str, int]]:
186
+ return (self.tokens[self.position]
187
+ if self.position < len(self.tokens) else None)
188
+
189
+ def next(self) -> Tuple[str, str, int]:
190
+ token = self.peek()
191
+ if token is None:
192
+ raise PrologParsingError(
193
+ "SYNTAX_ERROR: the clause ended unexpectedly")
194
+ self.position += 1
195
+ return token
196
+
197
+ def expect(self, value: str) -> Tuple[str, str, int]:
198
+ token = self.next()
199
+ if token[1] != value:
200
+ raise PrologParsingError(
201
+ f"SYNTAX_ERROR: expected {value!r} at position {token[2]}, "
202
+ f"found {token[1]!r}")
203
+ return token
204
+
205
+
206
+ def _unquote(text: str) -> str:
207
+ """Strip a quoted atom's quotes and undo its escapes."""
208
+ return re.sub(r"\\(.)", r"\1", text[1:-1])
209
+
210
+
211
+ class _Parser:
212
+ """One clause, read with an explicit fresh-variable counter for ``_``."""
213
+
214
+ def __init__(self, reader: _Reader, negation_as_failure: str):
215
+ self.reader = reader
216
+ self.negation_as_failure = negation_as_failure
217
+ self._anonymous = 0
218
+
219
+ # -- terms ---------------------------------------------------------------
220
+
221
+ def term(self) -> Node:
222
+ kind, value, position = self.reader.next()
223
+ if value in _REFUSED:
224
+ raise PrologParsingError(
225
+ f"SYNTAX_ERROR: unsupported construct at position {position}: "
226
+ f"{_REFUSED[value]}")
227
+ if kind == "punct" and value == "[":
228
+ raise PrologParsingError(
229
+ f"SYNTAX_ERROR: list syntax at position {position} — this "
230
+ "importer has no first-order reading for '.'/2 list terms; "
231
+ "rewrite the clause without lists")
232
+ if kind == "number":
233
+ try:
234
+ return Number(_numeral_from_text(value))
235
+ except ValueError as exc:
236
+ raise PrologParsingError(
237
+ f"SYNTAX_ERROR: at position {position}: {exc}") from None
238
+ if kind == "quoted":
239
+ name = _unquote(value)
240
+ return self._compound_or_constant(name, quoted=True)
241
+ if kind != "name":
242
+ raise PrologParsingError(
243
+ f"SYNTAX_ERROR: expected a term at position {position}, found "
244
+ f"{value!r}")
245
+ if value == "_":
246
+ # Each anonymous variable is a DISTINCT fresh one; collapsing them
247
+ # into a single '_' would equate positions Prolog keeps apart.
248
+ self._anonymous += 1
249
+ return Variable(f"_g{self._anonymous}")
250
+ if _VAR_RE.match(value):
251
+ return Variable(_lower(value))
252
+ return self._compound_or_constant(value, quoted=False)
253
+
254
+ def _compound_or_constant(self, name: str, *, quoted: bool) -> Node:
255
+ token = self.reader.peek()
256
+ if token is not None and token[1] == "(":
257
+ self.reader.next()
258
+ arguments = self._arguments()
259
+ return Function(name, arguments)
260
+ return Constant(name)
261
+
262
+ def _arguments(self) -> List[Node]:
263
+ arguments = [self.term()]
264
+ while True:
265
+ token = self.reader.next()
266
+ if token[1] == ")":
267
+ return arguments
268
+ if token[1] != ",":
269
+ raise PrologParsingError(
270
+ f"SYNTAX_ERROR: expected ',' or ')' at position "
271
+ f"{token[2]}, found {token[1]!r}")
272
+ arguments.append(self.term())
273
+
274
+ # -- literals and goals --------------------------------------------------
275
+
276
+ def literal(self) -> Node:
277
+ """One predicate application, as an :class:`Atom`."""
278
+ kind, value, position = self.reader.next()
279
+ if value in _REFUSED:
280
+ raise PrologParsingError(
281
+ f"SYNTAX_ERROR: unsupported construct at position {position}: "
282
+ f"{_REFUSED[value]}")
283
+ if kind == "quoted":
284
+ name = _unquote(value)
285
+ elif kind == "name" and not _VAR_RE.match(value):
286
+ name = value
287
+ else:
288
+ raise PrologParsingError(
289
+ f"SYNTAX_ERROR: expected a predicate name at position "
290
+ f"{position}, found {value!r}" +
291
+ (" — a variable cannot be a goal in this fragment"
292
+ if kind == "name" else ""))
293
+ token = self.reader.peek()
294
+ if token is None or token[1] != "(":
295
+ return Atom(_cap(name), [])
296
+ self.reader.next()
297
+ return Atom(_cap(name), self._arguments())
298
+
299
+ def goal(self) -> Node:
300
+ token = self.reader.peek()
301
+ if token is not None and token[0] == "neg":
302
+ self.reader.next()
303
+ if self.negation_as_failure != "classical":
304
+ raise PrologParsingError(
305
+ f"SYNTAX_ERROR: negation as failure (\\+) at position "
306
+ f"{token[2]} — it means 'not derivable', which coincides "
307
+ "with classical ¬ only under the closed world assumption "
308
+ "on a stratified program. Pass "
309
+ "negation_as_failure='classical' to read it as ¬ and "
310
+ "assert that assumption, or rewrite the clause.")
311
+ return Not(self.goal())
312
+ if token is not None and token[1] == "(":
313
+ self.reader.next()
314
+ inner = self.disjunction()
315
+ self.reader.expect(")")
316
+ return inner
317
+ return self.literal()
318
+
319
+ def conjunction(self) -> Node:
320
+ result = self.goal()
321
+ while True:
322
+ token = self.reader.peek()
323
+ if token is None or token[1] != ",":
324
+ return result
325
+ self.reader.next()
326
+ result = And(result, self.goal())
327
+
328
+ def disjunction(self) -> Node:
329
+ result = self.conjunction()
330
+ while True:
331
+ token = self.reader.peek()
332
+ if token is None or token[1] != ";":
333
+ return result
334
+ self.reader.next()
335
+ result = Or(result, self.conjunction())
336
+
337
+
338
+ def _close(formula: Node, variables, quantifier: str) -> Node:
339
+ """Wrap ``formula`` in one quantifier per variable, alphabetically —
340
+ order among quantifiers of the same kind never changes truth, so this is
341
+ purely for deterministic output."""
342
+ result = formula
343
+ for variable in sorted(variables, key=lambda v: v.name, reverse=True):
344
+ result = Quantifier(quantifier, variable, result)
345
+ return result
346
+
347
+
348
+ def parse_prolog_clause(text: str, *, mode: str = "clause",
349
+ negation_as_failure: str = "refuse") -> Node:
350
+ """Parse ONE Prolog clause (with or without the trailing ``.``).
351
+
352
+ Args:
353
+ text: a fact (``p(a).``) or a rule (``h(A) :- b1(A), b2(A).``).
354
+ mode: ``"clause"`` for the standard logical reading (every variable
355
+ universally quantified over the implication) or ``"body"`` for
356
+ just the condition, with body-only variables existentially closed
357
+ and the head's variables left free — see the module docstring for
358
+ why this is the caller's choice and not a default.
359
+ negation_as_failure: ``"refuse"`` (default) or ``"classical"`` to read
360
+ ``\\+`` as ``¬``, asserting the closed world assumption holds.
361
+
362
+ Returns:
363
+ A :class:`~unicode_logic_kit.fol.nodes.Node`. A FACT returns its head
364
+ atom, universally closed over its variables in ``"clause"`` mode and
365
+ returned bare in ``"body"`` mode (a fact has no condition, and
366
+ inventing ``⊤`` would be a formula the caller never wrote).
367
+
368
+ Raises:
369
+ ~unicode_logic_kit.fol.prolog_input.PrologParsingError: the text is
370
+ outside the accepted fragment (see
371
+ the module docstring), or ``mode``/``negation_as_failure`` is not
372
+ one of the documented values.
373
+ """
374
+ if mode not in ("clause", "body"):
375
+ raise PrologParsingError(
376
+ f"parse_prolog_clause: unknown mode={mode!r}, expected 'clause' "
377
+ "or 'body'")
378
+ if negation_as_failure not in ("refuse", "classical"):
379
+ raise PrologParsingError(
380
+ "parse_prolog_clause: unknown negation_as_failure="
381
+ f"{negation_as_failure!r}, expected 'refuse' or 'classical'")
382
+
383
+ stripped = text.strip()
384
+ if stripped.endswith("."):
385
+ stripped = stripped[:-1]
386
+ if not stripped.strip():
387
+ raise PrologParsingError("SYNTAX_ERROR: empty clause")
388
+
389
+ parser = _Parser(_Reader(stripped), negation_as_failure)
390
+ head = parser.literal()
391
+
392
+ token = parser.reader.peek()
393
+ body: Optional[Node] = None
394
+ if token is not None and token[0] == "neck":
395
+ parser.reader.next()
396
+ body = parser.disjunction()
397
+ leftover = parser.reader.peek()
398
+ if leftover is not None:
399
+ # A refused OPERATOR shows up here rather than inside a goal (it sits
400
+ # between two well-formed goals), so the documented reason has to be
401
+ # consulted at this point too — otherwise `p :- q -> r.` would be
402
+ # reported as a stray token instead of as if-then, and the reader
403
+ # would go looking for a typo.
404
+ if leftover[1] in _REFUSED:
405
+ raise PrologParsingError(
406
+ f"SYNTAX_ERROR: unsupported construct at position "
407
+ f"{leftover[2]}: {_REFUSED[leftover[1]]}")
408
+ raise PrologParsingError(
409
+ f"SYNTAX_ERROR: unexpected {leftover[1]!r} at position "
410
+ f"{leftover[2]} — one clause per call; use parse_prolog_program "
411
+ "for a file")
412
+
413
+ if body is None:
414
+ if mode == "body":
415
+ return head
416
+ return _close(head, free_variables(head), "∀")
417
+
418
+ if mode == "body":
419
+ head_variables = free_variables(head)
420
+ body_only = [v for v in free_variables(body) if v not in head_variables]
421
+ return _close(body, body_only, "∃")
422
+ return _close(Implies(body, head), free_variables(Implies(body, head)), "∀")
423
+
424
+
425
+ def parse_prolog_program(text: str, *, mode: str = "clause",
426
+ negation_as_failure: str = "refuse") -> List[Node]:
427
+ """Parse every clause in ``text``, in order.
428
+
429
+ Clauses are split on a ``.`` that ends a clause — a period followed by
430
+ whitespace or end of input — so a decimal point inside a number and a
431
+ period inside a quoted atom do not split anything.
432
+
433
+ Several clauses sharing one head are ALTERNATIVES (Prolog tries them in
434
+ turn), i.e. their bodies are implicitly disjoined. This function returns
435
+ them separately and does NOT combine them: the disjunction only holds
436
+ under the completion of the program, which is an assumption about the
437
+ whole program rather than a fact about these clauses, and asserting it
438
+ silently would strengthen what the caller wrote.
439
+
440
+ Raises:
441
+ ~unicode_logic_kit.fol.prolog_input.PrologParsingError: as
442
+ :func:`parse_prolog_clause`, with the failing
443
+ clause's text in the message.
444
+ """
445
+ clauses = []
446
+ for chunk in _split_clauses(text):
447
+ try:
448
+ clauses.append(parse_prolog_clause(
449
+ chunk, mode=mode, negation_as_failure=negation_as_failure))
450
+ except PrologParsingError as exc:
451
+ raise PrologParsingError(f"{exc} (in clause: {chunk.strip()!r})")
452
+ return clauses
453
+
454
+
455
+ def _split_clauses(text: str) -> List[str]:
456
+ """Clause-terminating periods only: not a decimal point, not inside a
457
+ quoted atom, not inside a line comment."""
458
+ chunks: List[str] = []
459
+ current: List[str] = []
460
+ index = 0
461
+ in_quote = False
462
+ while index < len(text):
463
+ character = text[index]
464
+ if in_quote:
465
+ current.append(character)
466
+ if character == "\\" and index + 1 < len(text):
467
+ current.append(text[index + 1])
468
+ index += 2
469
+ continue
470
+ if character == "'":
471
+ in_quote = False
472
+ index += 1
473
+ continue
474
+ if character == "'":
475
+ in_quote = True
476
+ current.append(character)
477
+ index += 1
478
+ continue
479
+ if character == "%":
480
+ index = _past_line_comment(text, index)
481
+ continue
482
+ if character == ".":
483
+ following = text[index + 1] if index + 1 < len(text) else " "
484
+ if following in " \t\r\n":
485
+ chunk = "".join(current).strip()
486
+ if chunk:
487
+ chunks.append(chunk)
488
+ current = []
489
+ index += 1
490
+ continue
491
+ current.append(character)
492
+ index += 1
493
+ tail = "".join(current).strip()
494
+ if tail:
495
+ chunks.append(tail)
496
+ return chunks
497
+
498
+
499
+ def load_prolog(path: str, *, mode: str = "clause",
500
+ negation_as_failure: str = "refuse") -> List[Node]:
501
+ """Read a ``.pl`` file and parse every clause in it (see
502
+ :func:`parse_prolog_program`)."""
503
+ with open(path, encoding="utf-8") as handle:
504
+ return parse_prolog_program(
505
+ handle.read(), mode=mode, negation_as_failure=negation_as_failure)