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,551 @@
1
+ """Drive the Attempto Parsing Engine (APE) — ACE text in, kit formulas out.
2
+
3
+ `Attempto Controlled English <https://github.com/Attempto/APE>`_ (ACE) is a
4
+ controlled natural language with exactly one reading per sentence: ambiguity is
5
+ resolved by CONVENTION (documented interpretation rules), not by a guesser. Its
6
+ reference parser APE translates ACE into a Discourse Representation Structure
7
+ and, from there, into several formats including TPTP. This module runs APE as a
8
+ subprocess and routes its TPTP output through the kit's own reader
9
+ (:func:`unicode_logic_kit.fol.tptp_input.parse_tptp`).
10
+
11
+ APE is DRIVEN, not reimplemented, for the same reason the kit drives Isabelle,
12
+ E and HETS instead of cloning them: a partial re-implementation of APE's DCG
13
+ grammar, lexicon and anaphora resolution would not be ACE — it would be an
14
+ ACE-shaped language with undocumented differences, which is the worst possible
15
+ property for a tool whose entire point is that the interpretation rules are
16
+ fixed. APE is LGPL and stays an external binary; nothing of it is vendored.
17
+
18
+ Version pin
19
+ -----------
20
+ Everything measured in this module and recorded in the test fixtures comes from
21
+ APE at commit ``5f4d5354a45fb772763bf1a9543f508f15b28982`` (2024-04-21, the
22
+ Attempto/APE default branch), built with ``make install`` under SWI-Prolog
23
+ (8.4.2 and 9.x both work; the saved state is bound to the exact SWI-Prolog
24
+ that built it, which is why CI rebuilds APE instead of caching the binary —
25
+ the build is seconds, unlike E's minutes-long C build). Bump the pin and the
26
+ fixtures together or not at all; the fixture filename carries the commit.
27
+
28
+ What one APE call looks like
29
+ -----------------------------
30
+ ``ape.exe -text "..." -cdrs -ctptp`` prints one XML document::
31
+
32
+ <apeResult>
33
+ <duration .../>
34
+ <drs>drs([A],[predicate(A,wait,named('John'))-1/2])</drs>
35
+ <tptp>fof(f1, axiom, (? [A] : (predicate1(A,wait,'John')))).</tptp>
36
+ <messages/>
37
+ </apeResult>
38
+
39
+ and the three outcomes this module distinguishes are all visible in it
40
+ (measured, not assumed — each of these is pinned by a recorded fixture):
41
+
42
+ - **accepted, TPTP available**: non-empty ``<tptp>``. Event predicates come
43
+ out arity-marked (``predicate1`` intransitive, ``predicate2`` transitive,
44
+ ``predicate3`` ditransitive), simple singular nouns are prettified to unary
45
+ predicates (``man(A)``), proper names become quoted constants (``'John'``).
46
+ - **accepted, TPTP unavailable**: non-trivial ``<drs>`` but EMPTY ``<tptp>``.
47
+ In XML mode that refusal is SILENT — nothing on stderr (measured); only a
48
+ ``-solo tptp`` call prints the reason, e.g. ``ERROR: DRS condition not
49
+ supported: must: tptp/4: must(drs(...))``, so this module makes one extra
50
+ ~20 ms call on exactly this path to harvest it (never on the happy path).
51
+ This is Attempto's own translator declining, and
52
+ it is exactly the set of constructs the kit can do better with than plain
53
+ FOL: the four modal boxes (``must``/``can``/``should``/``may``), negation
54
+ as failure (``~``, "it is not provable that"), ``question(...)`` and
55
+ ``command(...)`` boxes. Routing those into the kit's modal family is
56
+ milestone ACE-3; until then they surface as
57
+ :class:`AceTptpUnsupportedError` carrying the DRS text.
58
+ - **not ACE**: ``<messages>`` holds ``importance="error"`` entries with the
59
+ sentence number, the failing token, and often a repair suggestion; the DRS
60
+ is the trivial ``drs([],[])``. APE's exit code is 0 in EVERY one of these
61
+ cases, so exit codes decide nothing here — the XML does.
62
+
63
+ One caveat the numbers force on us: a non-trivial cardinality SURVIVES the
64
+ TPTP route but only reified — "At least 3 men wait." becomes
65
+ ``? [A,B] : predicate1(A,wait,B) & object(B,man,countable,na,geq,3)``, a
66
+ single witness ``B`` plus an inert 6-ary annotation. As kit-level FOL that
67
+ does NOT mean "at least three": :func:`ace_coverage` flags such sentences
68
+ (``reified_cardinality=True``) rather than letting the under-translation
69
+ pass silently; the honest Count-based translation landed with ACE-4/5 on
70
+ the DRS and formula routes (:mod:`unicode_logic_kit.ace.mapping`,
71
+ :mod:`unicode_logic_kit.ace.translate`) — this route keeps Attempto's own
72
+ export verbatim, flag included.
73
+
74
+ Discovery
75
+ ---------
76
+ Same contract as ``atp.eprover_backend``: ``$UFK_APE_CMD`` (prefix ``wsl:``
77
+ to force the WSL route, e.g. ``wsl:/home/me/APE/ape.exe``) → native
78
+ ``ape.exe``/``ape`` on PATH → ``wsl.exe which ape.exe`` → finally
79
+ ``$HOME/APE/ape.exe`` inside WSL, because ``git clone … ~/APE && make
80
+ install`` is the documented build and probing its default location makes a
81
+ fresh build work with zero configuration. Only the binary probes are cached;
82
+ the env override is read fresh on every call so a test can monkeypatch it.
83
+
84
+ The user lexicon is passed as TEXT (``-ulextext``), not as a file: a Windows
85
+ file path is meaningless inside WSL, and translating paths (``wslpath``) for
86
+ a lexicon that is typically a handful of generated lines buys nothing over
87
+ handing APE the lines themselves.
88
+ """
89
+
90
+ from __future__ import annotations
91
+
92
+ import os
93
+ import re
94
+ import shutil
95
+ import subprocess
96
+ import xml.etree.ElementTree as _ET
97
+ from dataclasses import dataclass, field
98
+ from typing import List, Optional, Sequence, Tuple
99
+
100
+ from ..fol.nodes import Node
101
+
102
+ __all__ = [
103
+ "ape_available", "run_ape", "ace_to_fol", "ace_coverage",
104
+ "ApeResult", "ApeMessage", "CoverageRow",
105
+ "AceError", "ApeUnavailableError", "AceParseError",
106
+ "AceTptpUnsupportedError", "AceTptpUnreadError",
107
+ ]
108
+
109
+ #: The APE commit everything here was measured against (see module docstring).
110
+ APE_PINNED_COMMIT = "5f4d5354a45fb772763bf1a9543f508f15b28982"
111
+
112
+ #: The DRS of an empty (or entirely rejected) text — APE's "nothing".
113
+ _TRIVIAL_DRS = "drs([],[])"
114
+
115
+
116
+ class AceError(Exception):
117
+ """Base class for everything this module raises on purpose."""
118
+
119
+
120
+ class ApeUnavailableError(AceError):
121
+ """No APE binary reachable (env, PATH, WSL) — see the module docstring."""
122
+
123
+
124
+ class AceParseError(AceError):
125
+ """The text is not ACE. ``messages`` carries APE's own diagnosis:
126
+ sentence number, failing token, and (for unknown words) a repair
127
+ suggestion — surfaced verbatim because APE's repair hints ("waitz" →
128
+ "wait") are the single most useful thing to show a caller."""
129
+
130
+ def __init__(self, message: str, messages: Tuple["ApeMessage", ...]):
131
+ super().__init__(message)
132
+ self.messages = messages
133
+
134
+
135
+ class AceTptpUnsupportedError(AceError):
136
+ """The text IS ACE but Attempto's own TPTP translator cannot express it
137
+ (modal boxes, negation as failure, questions, commands — see the module
138
+ docstring). ``drs`` carries APE's DRS term so a caller — and, from
139
+ milestone ACE-3 on, the kit's own DRS reader — can still work with the
140
+ sentence; ``reason`` is APE's stderr line naming the unsupported
141
+ condition."""
142
+
143
+ def __init__(self, message: str, drs: str, reason: str):
144
+ super().__init__(message)
145
+ self.drs = drs
146
+ self.reason = reason
147
+
148
+
149
+ class AceTptpUnreadError(AceError):
150
+ """APE DID produce TPTP, but the kit's TPTP reader does not accept it.
151
+
152
+ The measured case: "1 + 2 = 3." comes out as ``fof(f1, axiom, (1+2=3)).``
153
+ — infix arithmetic over plain integers, which is neither standard FOF
154
+ (that would be TFA's ``$sum``) nor inside the fragment
155
+ :func:`unicode_logic_kit.fol.tptp_input.parse_tptp` covers. Arithmetic
156
+ reaches the kit in milestone ACE-4 through the DRS (``formula``/``expr``
157
+ conditions → the z3_arith fragment), not by teaching the TPTP reader
158
+ Attempto's private dialect. ``tptp`` carries APE's raw output, ``cause``
159
+ the reader's error.
160
+ """
161
+
162
+ def __init__(self, message: str, tptp: str, cause: Exception):
163
+ super().__init__(message)
164
+ self.tptp = tptp
165
+ self.cause = cause
166
+
167
+
168
+ @dataclass(frozen=True)
169
+ class ApeMessage:
170
+ """One ``<message>`` element from APE's output.
171
+
172
+ ``importance`` is ``"error"`` or ``"warning"``; ``type`` is APE's own
173
+ category (``"word"``, ``"sentence"``, ...). ``sentence``/``token`` are
174
+ ``None`` when APE printed them empty (word-level messages carry no token
175
+ index). ``repair`` is APE's suggestion and may be empty.
176
+ """
177
+
178
+ importance: str
179
+ type: str
180
+ sentence: Optional[int]
181
+ token: Optional[int]
182
+ value: str
183
+ repair: str
184
+
185
+ @property
186
+ def is_error(self) -> bool:
187
+ return self.importance == "error"
188
+
189
+
190
+ @dataclass(frozen=True)
191
+ class ApeResult:
192
+ """One APE run, undigested: the raw DRS term, the raw TPTP text, every
193
+ message, and stderr (where APE reports TPTP-translator gaps)."""
194
+
195
+ drs: str
196
+ tptp: str
197
+ messages: Tuple[ApeMessage, ...]
198
+ stderr: str
199
+
200
+ @property
201
+ def error_messages(self) -> Tuple[ApeMessage, ...]:
202
+ return tuple(m for m in self.messages if m.is_error)
203
+
204
+ @property
205
+ def accepted(self) -> bool:
206
+ """True iff APE parsed the whole text as ACE (no error messages)."""
207
+ return not self.error_messages
208
+
209
+ @property
210
+ def tptp_supported(self) -> bool:
211
+ """True iff Attempto's own TPTP translator produced output."""
212
+ return bool(self.tptp.strip())
213
+
214
+
215
+ # (argv-prefix, use_wsl) — or None when nothing was found. Cached because
216
+ # discovery may spawn WSL probe processes; the env override is NOT cached.
217
+ _DISCOVERY_CACHE: dict = {}
218
+
219
+
220
+ def _probe_wsl(argv: Sequence[str]) -> Optional[str]:
221
+ """Run a short WSL probe; first stdout line or None. Never raises."""
222
+ try:
223
+ result = subprocess.run(list(argv), capture_output=True, text=True,
224
+ encoding="utf-8", errors="replace", timeout=15)
225
+ except (OSError, subprocess.TimeoutExpired):
226
+ return None
227
+ line = (result.stdout or "").strip().splitlines()
228
+ return line[0] if result.returncode == 0 and line else None
229
+
230
+
231
+ def _discover() -> Optional[Tuple[List[str], bool]]:
232
+ """Resolve APE to ``(argv_prefix, use_wsl)``, or ``None``.
233
+
234
+ ``argv_prefix`` is what to put before APE's own arguments — either
235
+ ``[command]`` natively or ``["wsl.exe", "-e", command]``. ``-e`` matters:
236
+ without it wsl.exe hands the command line to the default LOGIN SHELL,
237
+ which re-parses quoting, and ACE text is full of apostrophes ("John's
238
+ dog"); ``-e`` executes the binary directly with argv passed verbatim.
239
+ """
240
+ override = os.environ.get("UFK_APE_CMD")
241
+ if override:
242
+ if override.startswith("wsl:"):
243
+ return (["wsl.exe", "-e", override[4:]], True)
244
+ return ([override], False)
245
+ if "probe" in _DISCOVERY_CACHE:
246
+ return _DISCOVERY_CACHE["probe"]
247
+ found: Optional[Tuple[List[str], bool]] = None
248
+ for name in ("ape.exe", "ape"):
249
+ native = shutil.which(name)
250
+ if native:
251
+ found = ([native], False)
252
+ break
253
+ if found is None:
254
+ # The documented build location (`git clone … ~/APE && make install`
255
+ # leaves ape.exe in the checkout) — probed so a fresh build works
256
+ # with zero configuration. Natively first (a Linux/macOS home), then
257
+ # the same two probes inside WSL (a Windows host driving a WSL
258
+ # build; on a wsl.exe-less machine the probes fail fast to None).
259
+ home_build = os.path.join(os.path.expanduser("~"), "APE", "ape.exe")
260
+ if os.access(home_build, os.X_OK):
261
+ found = ([home_build], False)
262
+ if found is None:
263
+ path = _probe_wsl(["wsl.exe", "-e", "sh", "-c", "which ape.exe"])
264
+ if path is None:
265
+ path = _probe_wsl(["wsl.exe", "-e", "sh", "-c",
266
+ 'test -x "$HOME/APE/ape.exe" && echo "$HOME/APE/ape.exe"'])
267
+ if path is not None:
268
+ found = (["wsl.exe", "-e", path], True)
269
+ _DISCOVERY_CACHE["probe"] = found
270
+ return found
271
+
272
+
273
+ def ape_available() -> bool:
274
+ """Pure discovery: is an APE binary reachable (env, PATH, or WSL)?"""
275
+ return _discover() is not None
276
+
277
+
278
+ def _parse_ape_xml(stdout: str) -> Tuple[str, str, Tuple[ApeMessage, ...]]:
279
+ """``<apeResult>`` XML → ``(drs, tptp, messages)``; raises on non-XML."""
280
+ try:
281
+ root = _ET.fromstring(stdout)
282
+ except _ET.ParseError as exc:
283
+ raise RuntimeError(
284
+ f"APE printed something that is not the expected <apeResult> "
285
+ f"XML — broken installation or changed CLI? First 200 chars: "
286
+ f"{stdout[:200]!r}") from exc
287
+
288
+ def _index(raw: Optional[str]) -> Optional[int]:
289
+ return int(raw) if raw and raw.isdigit() else None
290
+
291
+ messages = tuple(
292
+ ApeMessage(
293
+ importance=m.get("importance", ""),
294
+ type=m.get("type", ""),
295
+ sentence=_index(m.get("sentence")),
296
+ token=_index(m.get("token")),
297
+ value=m.get("value", ""),
298
+ repair=m.get("repair", ""),
299
+ )
300
+ for m in root.iter("message"))
301
+ drs = (root.findtext("drs") or "").strip()
302
+ tptp = (root.findtext("tptp") or "").strip()
303
+ return drs, tptp, messages
304
+
305
+
306
+ def run_ape(text: str, *, ulex: Optional[str] = None, guess: bool = False,
307
+ timeout: float = 30.0) -> ApeResult:
308
+ """Run APE on ``text``; the undigested :class:`ApeResult`.
309
+
310
+ ``ulex`` is user-lexicon TEXT in APE's ``-ulextext`` format (one Prolog
311
+ clause per line, e.g. ``noun_sg(molecule, molecule, neutr).``) — see the
312
+ module docstring for why there is no file variant. ``guess=True`` turns
313
+ on APE's unknown-word guessing; it defaults to off because guessing
314
+ trades the "not ACE" verdict for a silently different lexicon, and this
315
+ kit's callers want the verdict.
316
+
317
+ Raises :class:`ApeUnavailableError` when discovery finds no binary, and
318
+ ``subprocess.TimeoutExpired`` when APE (a ~20 ms tool, measured) blows a
319
+ ``timeout`` that generous — at which point something is genuinely wrong
320
+ with the installation, not with the text.
321
+ """
322
+ found = _discover()
323
+ if found is None:
324
+ raise ApeUnavailableError(
325
+ "ape: no binary found ($UFK_APE_CMD, PATH, WSL, ~/APE/ape.exe). "
326
+ "Build one: git clone https://github.com/Attempto/APE ~/APE && "
327
+ f"cd ~/APE && git checkout {APE_PINNED_COMMIT[:7]} && make install "
328
+ "(needs swi-prolog).")
329
+ prefix, _use_wsl = found
330
+ argv = [*prefix, "-text", text, "-cdrs", "-ctptp"]
331
+ if ulex is not None:
332
+ argv += ["-ulextext", ulex]
333
+ if guess:
334
+ argv += ["-guess"]
335
+ result = subprocess.run(argv, capture_output=True, text=True,
336
+ encoding="utf-8", errors="replace",
337
+ timeout=timeout)
338
+ drs, tptp, messages = _parse_ape_xml(result.stdout or "")
339
+ return ApeResult(drs=drs, tptp=tptp, messages=messages,
340
+ stderr=(result.stderr or "").strip())
341
+
342
+
343
+ # APE at the pinned commit has one pretty-printer bug in its TPTP output:
344
+ # in a COLLECTIVE reading ("John and Mary lift a table.") the noun atom for
345
+ # the lifted object is printed juxtaposed — ``(table C)`` — instead of
346
+ # applied — ``table(C)`` — while every other sentence shape prints ``man(A)``
347
+ # correctly. The intent is unambiguous (there is no legal TPTP in which a
348
+ # lower-word is followed by a bare variable inside parentheses, so this
349
+ # pattern can ONLY match APE's malformation, never legal output), hence the
350
+ # repair below rather than a refusal. The raw, unrepaired text stays
351
+ # available on :class:`ApeResult`; :class:`CoverageRow` reports the repair.
352
+ _JUXTAPOSED_ATOM = re.compile(r"\(([a-z][a-zA-Z0-9_]*) ([A-Z][A-Za-z0-9]*)\)")
353
+
354
+
355
+ def _repair_ape_tptp(tptp: str) -> Tuple[str, int]:
356
+ """``(table C)`` → ``(table(C))``; returns (text, number of repairs)."""
357
+ return _JUXTAPOSED_ATOM.subn(r"(\1(\2))", tptp)
358
+
359
+
360
+ def _formulas_from(result: ApeResult) -> Tuple[List[Node], bool]:
361
+ """TPTP text of an accepted result → kit formulas, plus whether the
362
+ upstream-bug repair above had to fire.
363
+
364
+ Role guard: a yes/no question ("Does John wait?") survives APE's TPTP
365
+ route — as ``fof(f1, conjecture, …)``, a statement to PROVE (measured;
366
+ wh-questions do not survive at all, their ``query/2`` condition is
367
+ refused). :func:`ace_to_fol` returns assertions, and dropping the role
368
+ would silently turn the question into the claim that John waits — so a
369
+ non-axiom role raises instead, same category as the constructs Attempto
370
+ itself refuses."""
371
+ from ..fol.tptp_input import TptpParsingError, parse_tptp
372
+
373
+ tptp_text, n_repairs = _repair_ape_tptp(result.tptp)
374
+ try:
375
+ parsed = parse_tptp(tptp_text)
376
+ except TptpParsingError as exc:
377
+ raise AceTptpUnreadError(
378
+ f"APE produced TPTP the kit reader does not accept: {exc} — "
379
+ "raw output on .tptp (arithmetic reaches the kit via the DRS "
380
+ "in milestone ACE-4)",
381
+ tptp=result.tptp, cause=exc) from exc
382
+ non_axiom = [tf for tf in parsed if tf.role != "axiom"]
383
+ if non_axiom:
384
+ raise AceTptpUnsupportedError(
385
+ "the text contains a yes/no question: APE renders it as "
386
+ f"fof role {non_axiom[0].role!r} — a statement to PROVE, which "
387
+ "ace_to_fol must not flatten into an assertion (question "
388
+ "routing is milestone ACE-3); the DRS is on .drs",
389
+ drs=result.drs,
390
+ reason=f"non-axiom fof role {non_axiom[0].role!r} "
391
+ f"({len(non_axiom)} of {len(parsed)} clauses)")
392
+ return [tf.formula for tf in parsed], n_repairs > 0
393
+
394
+
395
+ def ace_to_fol(text: str, *, ulex: Optional[str] = None,
396
+ guess: bool = False, timeout: float = 30.0) -> List[Node]:
397
+ """ACE text → kit formulas, via APE's own TPTP output.
398
+
399
+ Returns one :class:`~unicode_logic_kit.fol.nodes.Node` per ``fof`` clause
400
+ APE emitted, in order. APE materializes cross-sentence anaphora into the
401
+ clauses themselves, so the formulas are self-contained; an empty text
402
+ yields ``[]``.
403
+
404
+ Symbol conventions are the TPTP reader's, documented there: predicates
405
+ upper-cased into the kit convention (``man`` → ``Man``, and events keep
406
+ APE's arity marker: ``Predicate1``/``Predicate2``/``Predicate3``),
407
+ quoted proper names read as constants.
408
+
409
+ Raises:
410
+ AceParseError: the text is not ACE (carries APE's messages).
411
+ AceTptpUnsupportedError: the text is ACE, but its DRS uses a
412
+ condition Attempto's own TPTP translator refuses — modality,
413
+ negation as failure, a question or a command (carries the DRS
414
+ and APE's stderr reason). These become kit-expressible in
415
+ milestone ACE-3, not silently mistranslated today.
416
+ ApeUnavailableError: no APE binary reachable.
417
+ """
418
+ return _ace_to_fol_detailed(text, ulex=ulex, guess=guess,
419
+ timeout=timeout)[0]
420
+
421
+
422
+ def _ace_to_fol_detailed(text: str, *, ulex: Optional[str], guess: bool,
423
+ timeout: float) -> Tuple[List[Node], bool]:
424
+ """:func:`ace_to_fol` plus the did-the-upstream-bug-repair-fire bit,
425
+ which :func:`ace_coverage` reports and the public signature drops."""
426
+ result = run_ape(text, ulex=ulex, guess=guess, timeout=timeout)
427
+ if not result.accepted:
428
+ first = result.error_messages[0]
429
+ where = (f"sentence {first.sentence}" if first.sentence is not None
430
+ else "input")
431
+ hint = f" (repair hint: {first.repair!r})" if first.repair else ""
432
+ raise AceParseError(
433
+ f"not ACE at {where}: {first.value!r}{hint} — "
434
+ f"{len(result.error_messages)} error message(s) in .messages",
435
+ result.error_messages)
436
+ if not result.tptp_supported:
437
+ if result.drs == _TRIVIAL_DRS:
438
+ return [], False # empty text: nothing asserted
439
+ reason = result.stderr or _tptp_refusal_reason(text, ulex=ulex,
440
+ guess=guess,
441
+ timeout=timeout)
442
+ raise AceTptpUnsupportedError(
443
+ "ACE accepted, but Attempto's TPTP translator does not cover "
444
+ f"this construct: {reason or 'reason not reported'} — the DRS "
445
+ "is on .drs (modality/naf/question/command routing is "
446
+ "milestone ACE-3)",
447
+ drs=result.drs, reason=reason)
448
+ return _formulas_from(result)
449
+
450
+
451
+ def _tptp_refusal_reason(text: str, *, ulex: Optional[str], guess: bool,
452
+ timeout: float) -> str:
453
+ """One ``-solo tptp`` call, stderr only — the refusal reason.
454
+
455
+ In XML mode APE's TPTP translator declines SILENTLY (empty ``<tptp>``,
456
+ empty stderr — measured); the diagnostic line naming the unsupported
457
+ condition (``must``, ``query/2``, ``~``, ``command``) is printed only in
458
+ solo mode. Runs exclusively on the already-failed path, so the happy
459
+ path stays a single process spawn. Never raises: worst case is an empty
460
+ reason on an exception that is being raised anyway.
461
+ """
462
+ found = _discover()
463
+ if found is None:
464
+ return ""
465
+ prefix, _use_wsl = found
466
+ argv = [*prefix, "-text", text, "-solo", "tptp"]
467
+ if ulex is not None:
468
+ argv += ["-ulextext", ulex]
469
+ if guess:
470
+ argv += ["-guess"]
471
+ try:
472
+ result = subprocess.run(argv, capture_output=True, text=True,
473
+ encoding="utf-8", errors="replace",
474
+ timeout=timeout)
475
+ except (OSError, subprocess.TimeoutExpired):
476
+ return ""
477
+ return (result.stderr or "").strip()
478
+
479
+
480
+ @dataclass(frozen=True)
481
+ class CoverageRow:
482
+ """One corpus sentence's fate on the APE → TPTP → kit route.
483
+
484
+ ``status`` is one of ``"ok"`` (kit formulas produced),
485
+ ``"tptp_unsupported"`` (ACE, but Attempto's TPTP translator declined —
486
+ detail carries the stderr reason), ``"tptp_unread"`` (ACE and TPTP
487
+ produced, but outside the kit reader's fragment — Attempto's infix
488
+ arithmetic, see :class:`AceTptpUnreadError`), ``"not_ace"`` (rejected —
489
+ detail carries APE's first error), or ``"infra"`` (APE itself failed).
490
+ ``reified_cardinality`` is the honesty flag from the module docstring:
491
+ the TPTP contains a reified ``object(...)`` atom, so a plural/cardinality
492
+ was under-translated by Attempto's own export and the kit formula does
493
+ NOT carry its intended counting force (the DRS and formula routes do,
494
+ since ACE-4/5).
495
+ ``tptp_repaired`` reports that the juxtaposed-atom repair (see
496
+ ``_JUXTAPOSED_ATOM``) had to fire on APE's raw TPTP.
497
+ """
498
+
499
+ sentence: str
500
+ status: str
501
+ reified_cardinality: bool = False
502
+ tptp_repaired: bool = False
503
+ detail: str = ""
504
+ formulas: Tuple[Node, ...] = field(default=())
505
+
506
+
507
+ def ace_coverage(sentences: Sequence[str], *, ulex: Optional[str] = None,
508
+ timeout: float = 30.0) -> List[CoverageRow]:
509
+ """Classify each sentence's fate on the TPTP route — the ACE-1 report.
510
+
511
+ Mechanical, one APE call per sentence (~20 ms each, measured): no
512
+ judgment calls beyond the three-way outcome split plus the
513
+ ``reified_cardinality`` flag, both defined at :class:`CoverageRow`.
514
+ """
515
+ rows: List[CoverageRow] = []
516
+ for sentence in sentences:
517
+ try:
518
+ formulas, repaired = _ace_to_fol_detailed(
519
+ sentence, ulex=ulex, guess=False, timeout=timeout)
520
+ except AceParseError as exc:
521
+ first = exc.messages[0]
522
+ rows.append(CoverageRow(sentence, "not_ace",
523
+ detail=f"{first.value!r}"
524
+ + (f" -> {first.repair!r}"
525
+ if first.repair else "")))
526
+ except AceTptpUnsupportedError as exc:
527
+ rows.append(CoverageRow(sentence, "tptp_unsupported",
528
+ detail=exc.reason))
529
+ except AceTptpUnreadError as exc:
530
+ rows.append(CoverageRow(sentence, "tptp_unread",
531
+ detail=str(exc.cause).splitlines()[0]))
532
+ except (RuntimeError, subprocess.TimeoutExpired) as exc:
533
+ rows.append(CoverageRow(sentence, "infra", detail=str(exc)))
534
+ else:
535
+ rows.append(CoverageRow(sentence, "ok",
536
+ reified_cardinality=_has_reified_object(formulas),
537
+ tptp_repaired=repaired,
538
+ formulas=tuple(formulas)))
539
+ return rows
540
+
541
+
542
+ def _has_reified_object(formulas: Sequence[Node]) -> bool:
543
+ """True iff any formula contains a reified ``Object`` atom — Attempto's
544
+ TPTP export keeps ``object/6`` verbatim exactly when it could not
545
+ prettify a noun phrase into a unary predicate (any cardinality other
546
+ than a simple singular), so this atom's presence IS the
547
+ under-translation marker :class:`CoverageRow` documents."""
548
+ from ..fol.nodes import Atom
549
+
550
+ return any(isinstance(sub, Atom) and sub.predicate == "Object"
551
+ for formula in formulas for sub in formula.walk())