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,1333 @@
1
+ """TSTP/SZS: read prover output into the kit's :mod:`atp.protocol` vocabulary.
2
+
3
+ TPTP-family provers (Vampire, E, Prover9-successors, ...) report their result
4
+ through the **SZS ontology** (Sutcliffe's "Standard for Success" status
5
+ values) on a single stdout line:
6
+
7
+ % SZS status <value> for <problem>[ : <free-text>]
8
+
9
+ and, when asked for a proof, print the derivation itself as a sequence of
10
+ annotated TSTP statements:
11
+
12
+ fof(name, role, formula, inference(rule, [status(thm)], [parent, ...])).
13
+ cnf(name, role, formula, inference(rule, [status(thm)], [parent, ...])).
14
+
15
+ This module is mostly the read side of both: :func:`extract_szs_status` pulls
16
+ the first status line out of raw prover output, :func:`szs_to_verdict_fields`
17
+ maps an SZS value onto :mod:`atp.protocol`'s ``(status, reason)`` pair, and
18
+ :func:`parse_tstp_derivation` turns the annotated fof/cnf lines into a proof
19
+ DAG of :class:`TstpStep` records (reusing :mod:`fol.tptp_input` to parse each
20
+ step's formula body back into the toolkit AST). :func:`to_tstp` is the write
21
+ side's one entry point — see its own docstring for what it serialises and
22
+ why that is narrower than "the kit's own resolution search": it turns an
23
+ already-:func:`~atp.resolution_check.verify_resolution_proof`-CERTIFIED
24
+ :class:`~atp.resolution_check.ResolutionDerivation` into the annotated
25
+ ``cnf(...).`` text this module's own :func:`parse_tstp_derivation` reads back
26
+ (the round trip is the write side's own test oracle), regardless of who
27
+ built that derivation.
28
+
29
+ Sources (verified against the primary spec before writing this module):
30
+
31
+ * SZS status line syntax and the full ontology of values —
32
+ https://tptp.org/UserDocs/SZSOntology/
33
+ * TSTP annotated-formula / ``inference(rule, info, parents)`` derivation
34
+ syntax — https://tptp.org/UserDocs/QuickGuide/Derivations.html
35
+
36
+ Conjecture vs. refutation framing
37
+ ----------------------------------
38
+ The SZS *success* branch splits into two parallel vocabularies depending on
39
+ whether the TPTP problem carries a ``conjecture`` role or not:
40
+
41
+ * **with a conjecture** (``Ax ⊢ C``): ``Theorem`` (all models of Ax model C),
42
+ ``CounterSatisfiable`` (some model of Ax models ¬C — a genuine
43
+ countermodel), ``ContradictoryAxioms`` (Ax alone has no model, so C follows
44
+ vacuously by ex falso quodlibet).
45
+ * **without a conjecture** (a bare clause/axiom set): ``Unsatisfiable`` (Ax
46
+ has no model) / ``Satisfiable`` (Ax has a model).
47
+
48
+ :func:`szs_to_verdict_fields` takes a ``query`` argument spelling out which
49
+ framing produced the SZS value, because the *same* Verdict status can follow
50
+ from *different* SZS values depending on it: a resolution-refutation route
51
+ that folds ``premises ∧ ¬conclusion`` into one clause set and asks "is this
52
+ unsatisfiable?" (``query="refutation"``) proves the entailment on
53
+ ``Unsatisfiable``, whereas a direct-conjecture route (``query="conjecture"``)
54
+ proves it on ``Theorem``. Feeding a conjecture-framing status to a
55
+ refutation query (or vice versa) is a mismatch this module refuses to
56
+ resolve by guessing — it comes back ``(UNKNOWN, "incomplete")``, and any SZS
57
+ value entirely outside the recognised table comes back ``(UNKNOWN, None)``;
58
+ either way the raw SZS string itself is never lost — it is whatever the
59
+ caller already passed in, only the (status, reason) pair is decided here.
60
+ """
61
+
62
+ import functools
63
+ import re
64
+ from dataclasses import dataclass
65
+ from typing import Dict, FrozenSet, Iterator, List, Optional, Tuple
66
+
67
+ from ..fol._fol_nodes import constant_name_to_ascii, tptp_fold_first_letter
68
+ from ..fol._numeral_symbols import numeral_name, numerals_as_constants
69
+ from ..fol._tptp_symbols import is_tptp_boolean_atom
70
+ from ..fol.nodes import Atom, Constant, Function, Node, Number, Or
71
+ from ..fol.naming import ParsingError
72
+ from ..fol.tptp_input import parse_tptp_formula
73
+ from ._tptp_problem import (
74
+ TptpNameMap, apply_reverse_tptp,
75
+ _check_no_symbol_collisions, _is_fixed_atom, _is_tptp_safe, _separate_term_names,
76
+ _Renamer, _predicate_base_case, _term_base_case,
77
+ )
78
+ from .protocol import ERROR, PROVED, REFUTED, UNKNOWN
79
+ from .resolution_check import ResolutionDerivation, _lit_key, verify_resolution_proof
80
+
81
+ __all__ = [
82
+ "extract_szs_status", "szs_to_verdict_fields",
83
+ "TstpStep", "TstpDerivation", "parse_tstp_derivation",
84
+ "reverse_map_derivation",
85
+ "relevant_premises_from_tstp",
86
+ "to_tstp",
87
+ ]
88
+
89
+ # ---------------------------------------------------------------------------
90
+ # extract_szs_status
91
+ # ---------------------------------------------------------------------------
92
+
93
+ # "% SZS status <value> for <problem>[ : free text]" — tolerant of extra
94
+ # leading comment characters and surrounding whitespace (some tools double
95
+ # the marker or indent it), and deliberately NOT anchored on "for ..." so a
96
+ # status line lacking the problem name still yields its value. The comment
97
+ # marker set is [%#]: Vampire/Zipperposition print "% SZS status ...",
98
+ # E prints "# SZS status ..." (E's TSTP comments use '#').
99
+ # A line starts at the text start or after ANY line break; (?m)^ alone only
100
+ # knows LF, so a bare-CR transcript would hide its status line.
101
+ _SZS_LINE_RE = re.compile(r"(?m)(?:^|(?<=\r))[ \t]*[%#]+[ \t]*SZS\s+status\s+(\S+)")
102
+
103
+
104
+ def extract_szs_status(output: str) -> Optional[str]:
105
+ """Return the FIRST ``SZS status`` value found in ``output``, or ``None``.
106
+
107
+ Args:
108
+ output: raw stdout (or stdout+stderr) from a TPTP-family prover.
109
+
110
+ Returns:
111
+ The bare status token (e.g. ``"Theorem"``, ``"CounterSatisfiable"``,
112
+ ``"GaveUp"``) from the first matching line, or ``None`` if the text
113
+ has no ``SZS status`` line at all.
114
+ """
115
+ match = _SZS_LINE_RE.search(output)
116
+ return match.group(1) if match else None
117
+
118
+
119
+ # ---------------------------------------------------------------------------
120
+ # szs_to_verdict_fields
121
+ # ---------------------------------------------------------------------------
122
+
123
+ # reason values reused from atp.protocol's UNKNOWN/ERROR vocabulary; see
124
+ # that module's docstring for what each one asserts.
125
+ _TIMEOUT = "timeout"
126
+ _BOUND_HIT = "bound_hit"
127
+ _INCOMPLETE = "incomplete"
128
+ _UNSUPPORTED = "unsupported"
129
+ _INFRA = "infra"
130
+
131
+ # Values shared by both framings: no-success outcomes and the SZS values
132
+ # that mean the same thing regardless of whether a conjecture was posed.
133
+ _COMMON = {
134
+ "Timeout": (UNKNOWN, _TIMEOUT),
135
+ "ResourceOut": (UNKNOWN, _BOUND_HIT),
136
+ "GaveUp": (UNKNOWN, _INCOMPLETE),
137
+ "Inappropriate": (UNKNOWN, _UNSUPPORTED),
138
+ "Error": (ERROR, _INFRA),
139
+ "InputError": (ERROR, _INFRA),
140
+ "SyntaxError": (ERROR, _INFRA),
141
+ # "no success value has ever been established for this problem" — the
142
+ # prover said nothing informative; distinct from a hit budget.
143
+ "Unknown": (UNKNOWN, None),
144
+ # Reserved for open mathematical conjectures / accepted-on-faith results
145
+ # (TPTP problem-library ratings, not something a live prover run
146
+ # emits) — never seen from Vampire/E in practice, mapped honestly to
147
+ # UNKNOWN rather than silently dropped.
148
+ "Open": (UNKNOWN, None),
149
+ "Assumed": (UNKNOWN, None),
150
+ }
151
+
152
+ # query="conjecture": Ax ⊢ C was posed directly (a "conjecture" role formula
153
+ # in the TPTP problem). See module docstring for the semantics.
154
+ _CONJECTURE_TABLE = {
155
+ "Theorem": (PROVED, None),
156
+ "CounterSatisfiable": (REFUTED, None),
157
+ # Ax alone is inconsistent -> Ax ⊢ C holds for EVERY C (ex falso
158
+ # quodlibet); a legitimate, if degenerate, proof of the entailment.
159
+ "ContradictoryAxioms": (PROVED, None),
160
+ # "Satisfiable"/"Unsatisfiable" are the NO-conjecture branch's success
161
+ # values (see docstring); seeing them here means the problem this
162
+ # status came from did not actually carry a conjecture the way the
163
+ # caller's query claims — refuse to guess proved/refuted from it.
164
+ "Satisfiable": (UNKNOWN, _INCOMPLETE),
165
+ "Unsatisfiable": (UNKNOWN, _INCOMPLETE),
166
+ }
167
+
168
+ # query="refutation": the caller already folded premises ∧ ¬conclusion into
169
+ # one axiom/clause set with NO conjecture and asked "is this satisfiable?".
170
+ _REFUTATION_TABLE = {
171
+ # No model of (premises ∧ ¬conclusion) -> the entailment holds.
172
+ "Unsatisfiable": (PROVED, None),
173
+ # A model of (premises ∧ ¬conclusion) exists -> a genuine countermodel.
174
+ "Satisfiable": (REFUTED, None),
175
+ # "Theorem"/"CounterSatisfiable"/"ContradictoryAxioms" are the
176
+ # WITH-conjecture branch's values; seeing them under a refutation query
177
+ # is the mirror-image mismatch of the case above.
178
+ "Theorem": (UNKNOWN, _INCOMPLETE),
179
+ "CounterSatisfiable": (UNKNOWN, _INCOMPLETE),
180
+ "ContradictoryAxioms": (UNKNOWN, _INCOMPLETE),
181
+ }
182
+
183
+ def szs_to_verdict_fields(szs: str, *, query: str) -> Tuple[str, Optional[str]]:
184
+ """Map an SZS status value onto ``atp.protocol``'s ``(status, reason)``.
185
+
186
+ Args:
187
+ szs: a bare SZS status token, e.g. from :func:`extract_szs_status`
188
+ (``"Theorem"``, ``"CounterSatisfiable"``, ``"GaveUp"``, ...).
189
+ query: which framing produced ``szs`` — ``"conjecture"`` (the prover
190
+ was handed premises plus a ``conjecture``-role formula and asked
191
+ to prove it) or ``"refutation"`` (the prover was handed
192
+ ``premises ∧ ¬conclusion`` as one clause/axiom set with no
193
+ conjecture and asked whether it is satisfiable). See the module
194
+ docstring for why the same Verdict status follows from different
195
+ SZS values under the two framings.
196
+
197
+ Returns:
198
+ A ``(status, reason)`` pair drawn from :mod:`atp.protocol`'s
199
+ vocabulary. ``reason`` is only ever non-``None`` when
200
+ ``status == UNKNOWN`` or ``status == ERROR``, matching
201
+ :class:`atp.protocol.Verdict`'s contract.
202
+
203
+ Raises:
204
+ ValueError: ``query`` is neither ``"conjecture"`` nor ``"refutation"``.
205
+
206
+ Any ``szs`` value this table does not recognise — including a
207
+ recognised value fed the WRONG ``query`` (see module docstring) — comes
208
+ back as ``(UNKNOWN, None)`` or ``(UNKNOWN, "incomplete")`` rather than a
209
+ guessed proved/refuted; this function never invents a definitive verdict
210
+ from an unfamiliar or mismatched SZS token.
211
+ """
212
+ if query == "conjecture":
213
+ table, other_table = _CONJECTURE_TABLE, _REFUTATION_TABLE
214
+ elif query == "refutation":
215
+ table, other_table = _REFUTATION_TABLE, _CONJECTURE_TABLE
216
+ else:
217
+ raise ValueError(
218
+ f"szs_to_verdict_fields: query must be 'conjecture' or 'refutation', got {query!r}")
219
+
220
+ if szs in table:
221
+ return table[szs]
222
+ if szs in _COMMON:
223
+ return _COMMON[szs]
224
+ if szs in other_table:
225
+ # Recognised, but only under the OTHER framing — an explicit
226
+ # query/status mismatch (see module docstring); reported the same
227
+ # as an unrecognised value except for the reason, so a caller that
228
+ # logs UNKNOWN/incomplete can tell "mismatch" from "no info" apart
229
+ # from the SZS string it already has in hand.
230
+ return (UNKNOWN, _INCOMPLETE)
231
+ return (UNKNOWN, None)
232
+
233
+
234
+ # ---------------------------------------------------------------------------
235
+ # TSTP derivation parsing
236
+ # ---------------------------------------------------------------------------
237
+
238
+ @dataclass(frozen=True)
239
+ class TstpStep:
240
+ """One annotated TSTP statement: ``language(name, role, formula, source).``
241
+
242
+ Fields:
243
+
244
+ ``name``
245
+ the statement's TSTP name (``"f1"``, ``"c_0_7"``, a bare number, …),
246
+ verbatim.
247
+ ``language``
248
+ ``"fof"`` or ``"cnf"`` (the only two this module reads — ``tff`` /
249
+ ``thf`` lines are skipped, see :func:`parse_tstp_derivation`).
250
+ ``role``
251
+ the TSTP role verbatim (``"axiom"``, ``"plain"``,
252
+ ``"negated_conjecture"``, ``"lemma"``, …), not validated against a
253
+ fixed vocabulary.
254
+ ``formula_text``
255
+ the formula's TSTP source text, verbatim, always present regardless
256
+ of whether it parsed.
257
+ ``formula``
258
+ ``formula_text`` parsed into a toolkit :class:`Node` via
259
+ :func:`fol.tptp_input.parse_tptp_formula`, or ``None`` if that parse
260
+ failed (a TSTP formula shape the grammar does not cover, e.g. one
261
+ carrying ``$distinct`` or other constructs outside its scope) — the
262
+ raw text is kept either way, nothing is lost.
263
+ ``rule``
264
+ the inference rule name from an ``inference(rule, info, parents)``
265
+ source record, or ``None`` for a leaf statement (a ``file(…)``
266
+ source, no source field at all, or a source form this module does not
267
+ interpret, e.g. ``introduced(…)``).
268
+ ``parents``
269
+ names of the statements this one was derived from, read from an
270
+ ``inference(…)`` source's parent list; empty for a leaf statement or
271
+ when every parent-list entry is itself a compound term (e.g.
272
+ ``theory(equality)``, or a prover's inline-nested ``inference(…)``
273
+ parent) rather than a bare name/number.
274
+ """
275
+
276
+ name: str
277
+ language: str
278
+ role: str
279
+ formula_text: str
280
+ formula: Optional[Node]
281
+ rule: Optional[str]
282
+ parents: Tuple[str, ...] = ()
283
+
284
+ def to_dict(self) -> dict:
285
+ """Serialise to a JSON-compatible dict (``formula`` via ``Node.to_dict``)."""
286
+ return {
287
+ "name": self.name,
288
+ "language": self.language,
289
+ "role": self.role,
290
+ "formula_text": self.formula_text,
291
+ "formula": self.formula.to_dict() if self.formula is not None else None,
292
+ "rule": self.rule,
293
+ "parents": list(self.parents),
294
+ }
295
+
296
+
297
+ @dataclass(frozen=True)
298
+ class TstpDerivation:
299
+ """An ordered sequence of :class:`TstpStep`, in the order they appeared."""
300
+
301
+ steps: Tuple[TstpStep, ...]
302
+
303
+ def to_dict(self) -> dict:
304
+ """Serialise to a JSON-compatible dict."""
305
+ return {"steps": [s.to_dict() for s in self.steps]}
306
+
307
+
308
+ # A "fof(" / "cnf(" statement start: the keyword must not be preceded by an
309
+ # identifier character (so it never fires inside a longer name), matched
310
+ # case-sensitively since TPTP keywords are lowercase by the standard.
311
+ _STMT_START_RE = re.compile(r"(?<![A-Za-z0-9_])(fof|cnf)\(")
312
+
313
+ # The same, for the readers of a proof's axiom leaves, which must also see the typed
314
+ # statements (``tff`` / ``tcf``) a prover prints for a TF0 or TFA problem. The
315
+ # derivation reader keeps to ``fof`` / ``cnf``.
316
+ _TYPED_STMT_START_RE = re.compile(r"(?<![A-Za-z0-9_])(fof|cnf|tff|tcf)\(")
317
+
318
+ _OPEN = "(["
319
+ _CLOSE = ")]"
320
+
321
+
322
+ # A '%' comment ends at the first line break of ANY convention: text handed
323
+ # over as a string (not read through a text-mode file) may use bare CR.
324
+ _LINE_BREAK_RE = re.compile(r"\r\n?|\n")
325
+
326
+
327
+ def _skip_comment(text: str, i: int, n: int) -> int:
328
+ """If ``text[i:]`` starts a ``%`` line comment or ``/* */`` block comment,
329
+ return the index just past it; otherwise return ``i`` unchanged."""
330
+ if text[i] == "%":
331
+ m = _LINE_BREAK_RE.search(text, i)
332
+ return n if m is None else m.end()
333
+ if text.startswith("/*", i):
334
+ j = text.find("*/", i + 2)
335
+ return n if j == -1 else j + 2
336
+ return i
337
+
338
+
339
+ def _skip_quoted(text: str, i: int, n: int) -> int:
340
+ """If ``text[i]`` opens a ``'...'`` or ``"..."`` TPTP quoted token
341
+ (``\\`` escapes the next character), return the index just past its
342
+ closing quote; otherwise return ``i`` unchanged."""
343
+ quote = text[i]
344
+ if quote not in ("'", '"'):
345
+ return i
346
+ j = i + 1
347
+ while j < n and text[j] != quote:
348
+ j += 2 if text[j] == "\\" else 1
349
+ return min(j + 1, n)
350
+
351
+
352
+ def _iter_tstp_statements(text: str, *, typed: bool = False):
353
+ """Yield ``(language, statement_text)`` for every top-level ``fof(...)./
354
+ cnf(...).`` statement in ``text``, comments and quoted tokens skipped (with
355
+ ``typed=True`` also the ``tff`` and ``tcf`` ones).
356
+
357
+ ``statement_text`` is the exact source span from the ``fof``/``cnf``
358
+ keyword through the terminating ``.`` inclusive. Bracket depth is
359
+ tracked over BOTH ``()`` and ``[]`` (TPTP variable/argument lists use
360
+ ``[]``, and a naive parens-only count would mis-split on the commas
361
+ inside one) so nested source/inference records are captured whole. A
362
+ statement whose closing bracket is never followed by ``.`` (malformed
363
+ input, or the text was truncated mid-statement) is silently skipped —
364
+ :func:`parse_tstp_derivation` only ever sees whole statements.
365
+ """
366
+ n = len(text)
367
+ i = 0
368
+ start_re = _TYPED_STMT_START_RE if typed else _STMT_START_RE
369
+ while i < n:
370
+ c = text[i]
371
+ if c in "%" or text.startswith("/*", i):
372
+ i = _skip_comment(text, i, n)
373
+ continue
374
+ if c.isspace():
375
+ i += 1
376
+ continue
377
+ match = start_re.match(text, i)
378
+ if not match:
379
+ i += 1
380
+ continue
381
+ language = match.group(1)
382
+ start = i
383
+ k = match.end() # just past the opening '('
384
+ depth = 1
385
+ while k < n and depth > 0:
386
+ c = text[k]
387
+ if c == "%" or text.startswith("/*", k):
388
+ k = _skip_comment(text, k, n)
389
+ continue
390
+ if c in ("'", '"'):
391
+ k = _skip_quoted(text, k, n)
392
+ continue
393
+ if c in _OPEN:
394
+ depth += 1
395
+ elif c in _CLOSE:
396
+ depth -= 1
397
+ k += 1
398
+ p = k
399
+ while p < n and text[p] in " \t":
400
+ p += 1
401
+ if p < n and text[p] == ".":
402
+ yield language, text[start:p + 1]
403
+ i = p + 1
404
+ else:
405
+ i = match.end() # malformed: resume scanning past the keyword
406
+
407
+
408
+ def _split_top_level(s: str) -> List[str]:
409
+ """Split ``s`` on commas at bracket-depth 0 (``()``/``[]``, quotes-aware)."""
410
+ parts: List[str] = []
411
+ buf: List[str] = []
412
+ n = len(s)
413
+ depth = 0
414
+ i = 0
415
+ while i < n:
416
+ c = s[i]
417
+ if c in ("'", '"'):
418
+ j = _skip_quoted(s, i, n)
419
+ buf.append(s[i:j])
420
+ i = j
421
+ continue
422
+ if c in _OPEN:
423
+ depth += 1
424
+ buf.append(c)
425
+ i += 1
426
+ continue
427
+ if c in _CLOSE:
428
+ depth -= 1
429
+ buf.append(c)
430
+ i += 1
431
+ continue
432
+ if c == "," and depth == 0:
433
+ parts.append("".join(buf))
434
+ buf = []
435
+ i += 1
436
+ continue
437
+ buf.append(c)
438
+ i += 1
439
+ parts.append("".join(buf))
440
+ return [p.strip() for p in parts]
441
+
442
+
443
+ def _parse_source(source_text: Optional[str]) -> Tuple[Optional[str], Tuple[str, ...]]:
444
+ """Read a statement's 4th field into ``(rule, parents)``.
445
+
446
+ Only the ``inference(rule, useful_info, parents)`` source form is
447
+ interpreted (rule = its 1st field, parents = the bare-name entries of
448
+ its LAST field, which is the parent list in every arity TSTP writers
449
+ use — 2-field ``inference(rule, parents)`` and 3-field
450
+ ``inference(rule, info, parents)`` alike). Any other source
451
+ (``file(...)``, ``introduced(...)``, absent) yields ``(None, ())`` — this
452
+ module does not guess a rule name for forms it was not asked to parse.
453
+ """
454
+ if not source_text or not source_text.startswith("inference("):
455
+ return None, ()
456
+ inner = source_text[len("inference("):-1]
457
+ fields = _split_top_level(inner)
458
+ if not fields:
459
+ return None, ()
460
+ rule = fields[0].strip() or None
461
+ parents_field = fields[-1].strip()
462
+ if not (parents_field.startswith("[") and parents_field.endswith("]")):
463
+ return rule, ()
464
+ items = _split_top_level(parents_field[1:-1])
465
+ parents = tuple(it for it in (item.strip() for item in items) if it and "(" not in it)
466
+ return rule, parents
467
+
468
+
469
+ def parse_tstp_derivation(output: str) -> TstpDerivation:
470
+ """Parse every top-level ``fof``/``cnf`` statement out of prover output.
471
+
472
+ Args:
473
+ output: raw prover stdout containing zero or more TSTP-annotated
474
+ ``fof(...).``/``cnf(...).`` statements (interspersed with SZS
475
+ status lines, banners, timing info, ... — all of that is
476
+ skipped; only lines that are themselves whole ``fof``/``cnf``
477
+ statements become a :class:`TstpStep`).
478
+
479
+ Returns:
480
+ A :class:`TstpDerivation` with one :class:`TstpStep` per statement,
481
+ in source order. ``steps`` is empty (not an error) when ``output``
482
+ contains no ``fof``/``cnf`` statement at all — e.g. a bare
483
+ ``SZS status Theorem`` line with proof output not requested.
484
+ """
485
+ steps = []
486
+ for language, stmt_text in _iter_tstp_statements(output):
487
+ inner = stmt_text[stmt_text.index("(") + 1:-2] # strip 'LANG(' and ').'
488
+ fields = _split_top_level(inner)
489
+ if len(fields) < 3:
490
+ continue # malformed statement
491
+ name, role, formula_text = fields[0], fields[1], fields[2]
492
+ source_text = fields[3] if len(fields) > 3 else None
493
+ try:
494
+ formula: Optional[Node] = parse_tptp_formula(formula_text)
495
+ except ParsingError:
496
+ formula = None
497
+ rule, parents = _parse_source(source_text)
498
+ steps.append(TstpStep(
499
+ name=name, language=language, role=role,
500
+ formula_text=formula_text, formula=formula,
501
+ rule=rule, parents=parents,
502
+ ))
503
+ return TstpDerivation(steps=tuple(steps))
504
+
505
+
506
+ # ---------------------------------------------------------------------------
507
+ # Rückweg: translate a derivation's formulas back to kit-level symbol names.
508
+ # ---------------------------------------------------------------------------
509
+
510
+ def reverse_map_derivation(derivation: TstpDerivation, mapping: TptpNameMap) -> TstpDerivation:
511
+ """Return a copy of ``derivation`` with every step's ``formula`` rewritten
512
+ from the sanitised TPTP-ASCII identifiers :func:`atp._tptp_problem
513
+ .generate_tptp_problem_with_mapping` chose back to the original
514
+ kit-level names, via ``mapping`` (see :func:`atp._tptp_problem
515
+ .apply_reverse_tptp`).
516
+
517
+ ``formula_text`` — the prover's own verbatim printed form of the step —
518
+ is deliberately left UNTOUCHED: it stays a truthful record of exactly
519
+ what the prover said, while ``formula`` becomes the structured form a
520
+ caller actually wants to read symbol names off of. A step whose formula
521
+ failed to parse (``formula is None``) passes through unchanged, and a
522
+ symbol the prover introduced itself (a Skolem constant, a
523
+ clausification name — ``sK1``, ``esk1_0``, ...) was never one of ours
524
+ to begin with, so :func:`apply_reverse_tptp` leaves it exactly as
525
+ printed rather than guessing at it.
526
+ """
527
+ new_steps = tuple(
528
+ step if step.formula is None else
529
+ TstpStep(name=step.name, language=step.language, role=step.role,
530
+ formula_text=step.formula_text,
531
+ formula=apply_reverse_tptp(step.formula, mapping),
532
+ rule=step.rule, parents=step.parents)
533
+ for step in derivation.steps
534
+ )
535
+ return TstpDerivation(steps=new_steps)
536
+
537
+
538
+ # ---------------------------------------------------------------------------
539
+ # Premise relevance: which axiom-role leaves does a derivation's refutation
540
+ # actually rest on?
541
+ #
542
+ # This is a SEPARATE walk from parse_tstp_derivation/TstpStep.parents above,
543
+ # deliberately — _parse_source (and therefore TstpStep.parents) intentionally
544
+ # DROPS a parent-list entry that is itself a compound term (a prover's own
545
+ # unnamed, nested `inference(...)` sub-step, or a `theory(equality)` marker —
546
+ # pinned by test_nested_inference_parents_that_are_compound_terms_are_dropped
547
+ # in tests/test_tstp.py), which is the right choice for a proof-DAG *display*
548
+ # (an unnamed intermediate has nothing else to call it) but would silently
549
+ # UNDER-count a derivation's real axiom leaves for a relevance query: E's own
550
+ # output nests almost every real inference this way (administrative steps
551
+ # like fof_nnf/variable_rename that never get their own c_0_N name — see
552
+ # tests/fixtures/eprover_3_5_1_theorem.txt). So the functions below re-scan
553
+ # the raw statement text directly (via _iter_tstp_statements/_split_top_level,
554
+ # the same low-level splitters parse_tstp_derivation itself uses) rather than
555
+ # going through TstpStep at all, and recurse into nested inference(...) parent
556
+ # entries instead of dropping them. TstpStep.parents/_parse_source are left
557
+ # completely untouched by this section.
558
+ # ---------------------------------------------------------------------------
559
+
560
+ def _iter_statement_fields(output: str) -> Iterator[Tuple[str, str, str, Optional[str]]]:
561
+ """Yield ``(name, role, formula_text, source_text)`` for every top-level
562
+ ``fof``/``cnf`` statement in ``output`` — the same fields
563
+ :func:`parse_tstp_derivation` extracts per :class:`TstpStep`, except
564
+ ``source_text`` is kept RAW (the unparsed 4th field, or ``None``)
565
+ instead of being reduced to ``(rule, parents)`` — :func:`_deep_ancestor_names`
566
+ below needs the raw text to recurse into nested ``inference(...)`` terms. The
567
+ statements of a typed problem's proof (``tff`` / ``tcf``) are read too: the
568
+ leaves of a TF0 or TFA proof are axioms like any other.
569
+ """
570
+ for _language, stmt_text in _iter_tstp_statements(output, typed=True):
571
+ inner = stmt_text[stmt_text.index("(") + 1:-2] # strip 'LANG(' and ').'
572
+ fields = _split_top_level(inner)
573
+ if len(fields) < 3:
574
+ continue # malformed statement
575
+ name, role, formula_text = fields[0], fields[1], fields[2]
576
+ source_text = fields[3] if len(fields) > 3 else None
577
+ yield name, role, formula_text, source_text
578
+
579
+
580
+ def _deep_ancestor_names(source_text: Optional[str]) -> FrozenSet[str]:
581
+ """Recursively collect every bare leaf name reachable from ``source_text``
582
+ (one statement's raw 4th field), descending into nested ``inference(...)``
583
+ parent-list entries instead of dropping them the way :func:`_parse_source`
584
+ does (see this section's module comment).
585
+
586
+ A bare name/number parent-list entry is a leaf name; an
587
+ ``inference(rule, info, parents)`` entry is descended into (its own LAST
588
+ field is ITS parents list, read the same way — arity-agnostic, exactly
589
+ like :func:`_parse_source`); any other compound entry (``theory(equality)``,
590
+ ...) contributes no name of its own but does not stop the REST of the
591
+ list from being read — the difference from :func:`_parse_source`, which
592
+ drops the whole list once any entry is compound.
593
+
594
+ Returns an empty set for a non-``inference`` source (``file(...)``, no
595
+ source field at all, ``introduced(...)``, ...) — that statement is a
596
+ leaf itself, with no ancestors to report.
597
+ """
598
+ if not source_text or not source_text.startswith("inference("):
599
+ return frozenset()
600
+ inner = source_text[len("inference("):-1]
601
+ fields = _split_top_level(inner)
602
+ if not fields:
603
+ return frozenset()
604
+ parents_field = fields[-1].strip()
605
+ if not (parents_field.startswith("[") and parents_field.endswith("]")):
606
+ return frozenset()
607
+ names: set = set()
608
+ for item in _split_top_level(parents_field[1:-1]):
609
+ item = item.strip()
610
+ if not item:
611
+ continue
612
+ if item.startswith("inference("):
613
+ names |= _deep_ancestor_names(item)
614
+ elif "(" not in item:
615
+ names.add(item)
616
+ # else: an uninterpreted compound term (theory(equality), a source
617
+ # form this module was not asked to descend into, ...) -- no name
618
+ # to report, matching _parse_source's treatment of the same shape.
619
+ return frozenset(names)
620
+
621
+
622
+ def _is_false_formula(formula_text: str) -> bool:
623
+ """Whether a statement's (raw, unparsed) formula text is ``$false``,
624
+ modulo the surrounding whitespace/parentheses a prover's pretty-printer
625
+ adds (``"($false)"``, ``"(\\n $false)"``, ...) — the textual marker of
626
+ a resolution refutation's SINK step, independent of whether the text
627
+ happens to parse (:func:`parse_tptp_formula` does understand ``$false``,
628
+ but this check is purely textual so it never depends on that grammar
629
+ staying in sync)."""
630
+ t = formula_text.strip()
631
+ while t.startswith("(") and t.endswith(")"):
632
+ inner = t[1:-1].strip()
633
+ if inner == t: # no progress -- stop, avoid looping forever
634
+ break
635
+ t = inner
636
+ return t == "$false"
637
+
638
+
639
+ def _walk_axiom_leaves(start_names: List[str],
640
+ statements: Dict[str, Tuple[str, str, Optional[str]]]
641
+ ) -> Optional[FrozenSet[str]]:
642
+ """Walk backward from every name in ``start_names`` through
643
+ ``statements``'s (possibly nested) ``inference(...)`` ancestor chains
644
+ down to every reachable ``axiom``-role leaf's NAME.
645
+
646
+ Returns ``None`` when the walk reaches a cited name that ``statements``
647
+ does not itself define (an incomplete derivation excerpt) — refuse
648
+ rather than guess, same contract as the caller.
649
+ """
650
+ axioms: set = set()
651
+ stack = list(start_names)
652
+ visited: set = set()
653
+ while stack:
654
+ name = stack.pop()
655
+ if name in visited:
656
+ continue
657
+ visited.add(name)
658
+ entry = statements.get(name)
659
+ if entry is None:
660
+ return None # a cited name this module cannot resolve
661
+ role, _formula_text, source_text = entry
662
+ ancestors = _deep_ancestor_names(source_text)
663
+ if not ancestors:
664
+ if role == "axiom":
665
+ axioms.add(name)
666
+ continue
667
+ stack.extend(a for a in ancestors if a not in visited)
668
+ return frozenset(axioms)
669
+
670
+
671
+ def _relevant_axiom_names(output: str) -> Optional[FrozenSet[str]]:
672
+ """Walk ``output``'s derivation backward from its sink step(s) through
673
+ the (possibly nested) ``inference(...)`` chain, down to every reachable
674
+ ``axiom``-role leaf's NAME.
675
+
676
+ The sinks are the steps whose formula text is ``$false`` — the final
677
+ empty clause of a refutation (both the E-prover and Vampire fixtures in
678
+ tests/test_tstp.py/eprover_3_5_1_*.txt end this way). A text with no
679
+ ``$false`` step is not a refutation at all (E's Saturation report for a
680
+ non-theorem, a truncated excerpt, ...), so there is nothing a premise
681
+ could be relevant *to*: ``None``.
682
+
683
+ What a non-``None`` result means: the leaves reachable from the sinks.
684
+ Every ``$false`` step together with its ancestors is a derivation of the
685
+ contradiction from exactly those leaves (plus the negated conjecture), so
686
+ the reachable axioms are SUFFICIENT for the entailment — provided the
687
+ prover's own inferences are sound, which is the trust this route already
688
+ places in the prover's PROVED verdict. When a text holds several
689
+ ``$false`` steps the union of their leaves is reported: each part is
690
+ sufficient on its own, so the union is too. The set is therefore never
691
+ an under-approximation of a sufficient set, but it is not claimed to be
692
+ minimal.
693
+
694
+ Returns ``None`` — refuse rather than guess, see this section's module
695
+ comment — when: there is no ``fof``/``cnf`` statement in ``output`` at
696
+ all; no step is ``$false``; or the walk reaches a cited name that is not
697
+ itself defined anywhere in ``output`` (an incomplete derivation excerpt).
698
+ A non-``None`` result is a genuine axiom-name set, even if empty (the
699
+ negated conjecture alone was contradictory).
700
+ """
701
+ found = _refutation_leaves(output)
702
+ return None if found is None else found[0]
703
+
704
+
705
+ def _refutation_leaves(output: str
706
+ ) -> Optional[Tuple[FrozenSet[str], Dict[str, Tuple[str, str, Optional[str]]]]]:
707
+ """:func:`_relevant_axiom_names`' walk, with the statements it walked: ``(names,
708
+ statements)`` where ``statements`` maps a statement's name to its ``(role,
709
+ formula text, raw source)``, or ``None`` for the cases that function refuses."""
710
+ statements: Dict[str, Tuple[str, str, Optional[str]]] = {}
711
+ for name, role, formula_text, source_text in _iter_statement_fields(output):
712
+ statements[name] = (role, formula_text, source_text)
713
+ if not statements:
714
+ return None
715
+
716
+ sinks = [name for name, (_role, ftext, _src) in statements.items()
717
+ if _is_false_formula(ftext)]
718
+ if not sinks:
719
+ return None
720
+ names = _walk_axiom_leaves(sinks, statements)
721
+ return None if names is None else (names, statements)
722
+
723
+
724
+ #: The names the fof writer gives the background axioms it adds on its own (the
725
+ #: non-emptiness line of a sort and the membership line of a sorted constant), for a
726
+ #: text whose problem has no name map to say so.
727
+ _BACKGROUND_NAME_RE = re.compile(r"^(?:nonempty_sort|sort_member)_\d+$")
728
+
729
+
730
+ def _premise_use_from_tstp(output: str, n_premises: int, name_map: Optional[TptpNameMap] = None,
731
+ *, eprover: bool = False
732
+ ) -> Optional[Tuple[Tuple[int, ...], Tuple[Tuple[str, str], ...]]]:
733
+ """``(premises, background)`` of a refutation: the 0-based indices of the caller's
734
+ premises its axiom leaves are, and the ``(name, meaning)`` of the background axioms
735
+ the writer added on its own that it used — or ``None`` where
736
+ :func:`relevant_premises_from_tstp` says ``None``.
737
+
738
+ The names of the problem come from ``name_map.premises`` when the writer recorded
739
+ them (``premise_names=``, or the default ``premise_<i>``), and are ``premise_1`` ...
740
+ ``premise_<n_premises>`` otherwise. A leaf is one of the caller's premises when its
741
+ printed name (:func:`~unicode_logic_kit.atp._writer_support.axiom_leaf_label`) is one of
742
+ those names, and a background axiom when it is one the map records (without a record:
743
+ one written ``nonempty_sort_<i>`` or ``sort_member_<i>``). Any other leaf, and any leaf
744
+ that ``eprover=True`` cannot tell between two premises, makes the whole answer
745
+ ``None``: the proof is not the one this problem's writer produced."""
746
+ from ._writer_support import axiom_leaf_label, default_premise_names, match_premise_label
747
+
748
+ found = _refutation_leaves(output)
749
+ if found is None:
750
+ return None
751
+ leaf_names, statements = found
752
+ # A problem with no premises records none, but its background facts are recorded all the same.
753
+ recorded_map = name_map if name_map is not None and (
754
+ bool(name_map.premises) or (n_premises == 0 and bool(name_map.background))) else None
755
+ recorded = recorded_map is not None
756
+ if recorded_map is not None:
757
+ names = tuple(recorded_map.premises)
758
+ if len(names) != n_premises:
759
+ return None
760
+ background = dict(recorded_map.background)
761
+ else:
762
+ names = default_premise_names(n_premises)
763
+ background = {}
764
+ indices: set = set()
765
+ used_background: Dict[str, str] = {}
766
+ for leaf in sorted(leaf_names):
767
+ label = axiom_leaf_label(leaf, statements[leaf][2])
768
+ index = match_premise_label(label, names, eprover=eprover)
769
+ if index is not None:
770
+ indices.add(index)
771
+ elif label in background:
772
+ used_background[label] = background[label]
773
+ elif not recorded and _BACKGROUND_NAME_RE.match(label):
774
+ used_background[label] = ""
775
+ else:
776
+ return None
777
+ return tuple(sorted(indices)), tuple(sorted(used_background.items()))
778
+
779
+
780
+ def relevant_premises_from_tstp(output: str, n_premises: int,
781
+ name_map: Optional[TptpNameMap] = None,
782
+ *, eprover: bool = False) -> Optional[Tuple[int, ...]]:
783
+ """Which of the caller's premises does a TSTP derivation's refutation actually
784
+ rest on?
785
+
786
+ Walks backward from the derivation's sink step(s) through the (possibly
787
+ nested) ``inference(...)`` chain via :func:`_relevant_axiom_names`, down
788
+ to every reachable ``axiom``-role leaf. This is genuinely NOT
789
+ the same walk as :func:`parse_tstp_derivation`'s ``TstpStep.parents``
790
+ alone would give (see this section's module comment): a leaf hidden
791
+ behind a prover's own nested, unnamed administrative inference would be
792
+ silently lost by ``.parents`` alone, under-reporting the true premise set.
793
+
794
+ Each leaf is read by the name the PROBLEM gave the axiom, which a prover prints back:
795
+ E as the statement's name, Vampire (run with ``--output_axiom_names on``) in the
796
+ second argument of the ``file(path, name)`` source of its numbered ``f<N>``
797
+ statements. The problem's names are the premise names its writer recorded in
798
+ ``name_map`` (``premise_names=``; see
799
+ :func:`~unicode_logic_kit.atp._tptp_problem.generate_tptp_problem_with_mapping`), and
800
+ ``premise_1`` ... ``premise_<n_premises>`` when ``name_map`` is ``None`` or holds none
801
+ (a problem written with the defaults, or read from text alone). A leaf that is a
802
+ background axiom the writer added on its own (the non-emptiness line of a sort, the
803
+ membership line of a sorted constant: ``nonempty_sort_<i>``, ``sort_member_<i>``) is
804
+ not a premise and is left out of the result, not a reason to answer ``None``;
805
+ :func:`_premise_use_from_tstp` also says which of them were used.
806
+
807
+ Args:
808
+ output: raw prover stdout containing the TSTP derivation. It is read as the
809
+ prover printed it: pass the text BEFORE any renaming of symbols back to the
810
+ caller's names (a premise name is not a symbol and must not be rewritten).
811
+ n_premises: how many premises the original call had.
812
+ name_map: the :class:`~unicode_logic_kit.atp._tptp_problem.TptpNameMap` of the
813
+ problem the prover was given, whose ``premises`` and ``background`` say what
814
+ its lines were called.
815
+ eprover: the output is E's, which reads a backslash and the character after it
816
+ as one backslash and so prints an apostrophe of a premise name as a backslash
817
+ (measured on E 3.5.1). A premise name is then matched as E prints it, and a
818
+ leaf that two premise names would be printed as is not matched.
819
+
820
+ Returns:
821
+ A sorted tuple of 0-based indices into the caller's premise list, or
822
+ ``None`` when the walk cannot be trusted — see
823
+ :func:`_relevant_axiom_names`'s ``Returns`` for when THAT happens,
824
+ plus: an axiom-role leaf whose name is neither a premise name nor a background
825
+ name of the problem (this module's own generator was evidently not what produced
826
+ ``output``), and a leaf named by more premises than one (``eprover=True``).
827
+ ``None`` is always the honest "don't know", never a silently under-approximated
828
+ subset — the kit's refuse-loudly rule; a wrong "premises used" answer is
829
+ worse for an eval pipeline than an honest absence of one.
830
+ """
831
+ use = _premise_use_from_tstp(output, n_premises, name_map, eprover=eprover)
832
+ return None if use is None else use[0]
833
+
834
+
835
+ def background_use_note(used: Tuple[Tuple[str, str], ...]) -> str:
836
+ """The sentence for a PROVED verdict's ``detail`` that names the background facts a
837
+ proof used, or ``""`` when it used none.
838
+
839
+ ``used`` is the ``(name, meaning)`` pairs :func:`_premise_use_from_tstp` returns as
840
+ its second item: the facts the problem writer added on its own for a sorted reading
841
+ (the non-emptiness of a sort, the membership of a sorted constant). They are not
842
+ premises of the caller's, which is why the sentence says so.
843
+ """
844
+ if not used:
845
+ return ""
846
+ facts = ", ".join(f"{name} ({meaning})" if meaning else name for name, meaning in used)
847
+ return (f"; the proof used the background facts of the sorted reading, which are "
848
+ f"not premises: {facts}")
849
+
850
+
851
+ # ---------------------------------------------------------------------------
852
+ # Hinweg: serialise a CERTIFIED ResolutionDerivation as annotated TSTP text —
853
+ # the write-side companion to parse_tstp_derivation above.
854
+ #
855
+ # Scope, precisely: this does NOT let the kit hand off proofs "it discovered"
856
+ # — atp.resolution.py's refute()/prove() return a bare bool and build no
857
+ # ResolutionDerivation trace, so there is no derivation object of the kit's
858
+ # OWN search to export. What this writer does serialise is whatever
859
+ # ResolutionDerivation the kit has independently CERTIFIED via
860
+ # atp.resolution_check.verify_resolution_proof, regardless of who
861
+ # constructed it (today: hand-authored fixtures, or a derivation transcribed
862
+ # from elsewhere and checked by the kit) — see :func:`to_tstp`'s Raises.
863
+ # ---------------------------------------------------------------------------
864
+
865
+ # Kit rule name -> TSTP inference-rule token. TSTP does not standardise this
866
+ # vocabulary (any stable string is spec-conformant, since it is purely
867
+ # informational — see the TPTP QuickGuide's own derivation grammar), so the
868
+ # tokens below are chosen to match what atp.tstp_check's VAMPIRE_CHECKED_RULES
869
+ # / EPROVER_CHECKED_RULES tables actually dispatch on, NOT the arbitrary
870
+ # strings a first pass at this mapping might reach for, because a token
871
+ # tstp_check does not recognise makes the emitted step come back "unchecked"
872
+ # by the independent second route rather than genuinely re-derived:
873
+ #
874
+ # - "resolve" -> "resolution": _check_tstp_resolve is a direct, unmodified
875
+ # port of resolution_check._check_resolve_step, so the semantics match
876
+ # exactly, not just generalize it.
877
+ # - "factor" -> "factoring": likewise a direct port of
878
+ # resolution_check._check_factor_step.
879
+ # - "paramodulate" -> "superposition", NOT the more obvious "paramodulation"
880
+ # (which tstp_check does not register in either checked-rule table at
881
+ # all — it would come back unchecked): _check_tstp_superposition
882
+ # generalizes resolution_check._check_paramodulate_step (same equation/
883
+ # target/direction/position recipe, searched instead of trusted) and tries
884
+ # both parent-role assignments, so it accepts a "paramodulate" step's
885
+ # (equation, target) parent order either way.
886
+ # - "demodulate" -> "rw" (E's abbreviation for its rewrite/demodulation
887
+ # rule; "forward_demodulation"/"backward_demodulation" dispatch to the
888
+ # identical checker and would work equally well): _check_tstp_demodulation
889
+ # generalizes resolution_check._check_demodulate_step (one-sided matching,
890
+ # term-order-checked orientation) and, like the superposition checker
891
+ # above, tries both parent orders.
892
+ # - "reflexivity" -> "equality_resolution", NOT "eq_resolution" (which does
893
+ # not exist in tstp_check's vocabulary either — TSTP/Vampire's own name for
894
+ # this rule is "equality_resolution", E's abbreviation "er"):
895
+ # _check_tstp_equality_resolution generalizes
896
+ # resolution_check._check_reflexivity_step (search over every negative
897
+ # equality literal instead of trusting a stated eq_literal).
898
+ # - "truth_constants" -> "true_and_false_elimination": Vampire's own name for
899
+ # dropping the literals that are false in every interpretation ($false and
900
+ # ~$true) from a clause (captured live, Vampire 5.0.1: ``p | $false`` gives
901
+ # ``p``, ``~p | q | ~$true`` gives ``~p | q``; see
902
+ # tests/fixtures/tstp_check/vampire_true_and_false_elimination.txt).
903
+ # _check_tstp_truth_constants re-derives it from ONE parent: the stated
904
+ # clause is the parent without some literals, each of which is $false or
905
+ # ~$true, and without any other literal.
906
+ #
907
+ # "input" has no entry here: an input step carries no inference(...) source
908
+ # at all (see to_tstp), matching how parse_tstp_derivation reads a leaf
909
+ # statement (TstpStep.rule is None) and how atp.tstp_check's "leaf" tier
910
+ # checks it (alpha-variant of a supplied premise, not a rule re-derivation).
911
+ _KIT_RULE_TO_TSTP: Dict[str, str] = {
912
+ "resolve": "resolution",
913
+ "factor": "factoring",
914
+ "paramodulate": "superposition",
915
+ "demodulate": "rw",
916
+ "reflexivity": "equality_resolution",
917
+ "truth_constants": "true_and_false_elimination",
918
+ }
919
+
920
+
921
+ def _collect_name_case_safe(renamer: _Renamer, name: str) -> None:
922
+ """Like :meth:`atp._tptp_problem._Renamer.collect`, but ALSO requires
923
+ ``name`` to already match this namespace's own round-trip-safe case
924
+ convention (``renamer.case_fix(name) == name`` — uppercase-initial for
925
+ predicates, lowercase-initial for terms) before treating it as an
926
+ untouched identity.
927
+
928
+ Why :meth:`_Renamer.collect`'s plain :func:`atp._tptp_problem
929
+ ._is_tptp_safe` check is not enough here (a real, reviewer-found
930
+ blocker): that check is case-INSENSITIVE (it only asks "is this ASCII
931
+ and letter-initial", not "is this the case my namespace round-trips"),
932
+ so it happily reserves e.g. a lower-case predicate ``"bar"`` or an
933
+ upper-case-initial constant ``"Foo"`` AS ITSELF. But
934
+ :meth:`~fol.nodes.Node.to_tptp` folds only the FIRST character on
935
+ export, and :func:`fol.tptp_input._cap` UNCONDITIONALLY upper-cases a
936
+ parsed predicate's first character on import (no compensating step
937
+ exists for constants/functions at all) — so an identity-mapped name
938
+ whose real first-letter case does not already match what the reader
939
+ will reconstruct is never recoverable: ``apply_reverse_tptp`` comes back
940
+ with a DIFFERENTLY-cased name than the one actually serialised, with no
941
+ error anywhere (confirmed: a derivation-level ``Atom.predicate``/
942
+ ``Constant.name`` is a plain, case-unchecked ``str`` field, and
943
+ :func:`~atp.resolution_check.verify_resolution_proof` never inspects
944
+ case, so nothing upstream of this function rules the input out).
945
+
946
+ A name that fails this stricter test is routed through the exact same
947
+ queued-for-synthesis path :meth:`_Renamer.collect` uses for a genuinely
948
+ illegal name (``self._pending``), so it still gets a properly-cased,
949
+ de-collided token — :meth:`_Renamer.finalize` already runs ``case_fix``
950
+ on every synthesised base, so this only changes WHICH names go through
951
+ that path, not the synthesis logic itself.
952
+ """
953
+ if name in renamer.mapping or name in renamer._pending:
954
+ return
955
+ if _is_tptp_safe(name) and renamer.case_fix(name) == name:
956
+ renamer.used.add(renamer.render(name))
957
+ renamer.mapping[name] = name
958
+ else:
959
+ renamer._pending.append(name)
960
+
961
+
962
+ def _collect_names_for_derivation(node: Node, predicates: _Renamer, terms: _Renamer) -> None:
963
+ """The exact walk :func:`atp._tptp_problem._collect_names_for_tptp` does
964
+ for the problem writers (equality and ``$true`` / ``$false`` are never
965
+ renamed; the arithmetic operators and comparisons are ordinary symbols), routed
966
+ through :func:`_collect_name_case_safe` instead of :meth:`_Renamer.collect`
967
+ directly — see that function's docstring for why."""
968
+ for n in node.walk():
969
+ if isinstance(n, Atom):
970
+ if not _is_fixed_atom(n, True):
971
+ _collect_name_case_safe(predicates, n.predicate)
972
+ elif isinstance(n, (Function, Constant)):
973
+ _collect_name_case_safe(terms, n.name)
974
+
975
+
976
+ def _extend_name_map_for_derivation(
977
+ derivation: ResolutionDerivation, name_map: Optional[TptpNameMap]
978
+ ) -> TptpNameMap:
979
+ """Return a :class:`TptpNameMap` that covers every predicate/function/
980
+ constant name ``derivation`` actually uses, seeded from ``name_map``
981
+ where the caller supplied one.
982
+
983
+ Mirrors :func:`atp._tptp_problem._sanitize_for_tptp`'s own two-pass
984
+ collect/finalize :class:`atp._tptp_problem._Renamer` split — the two
985
+ namespaces (predicate; function+constant), the never-renamed
986
+ infix/prefix/arithmetic exclusions, everything — but *seeds* each
987
+ namespace's ``mapping``/``used`` from ``name_map`` first (a shallow copy;
988
+ the caller's ``TptpNameMap`` is never mutated) instead of starting empty,
989
+ and collects each name via :func:`_collect_name_case_safe` (through
990
+ :func:`_collect_names_for_derivation`) rather than
991
+ :meth:`~atp._tptp_problem._Renamer.collect` directly — see that
992
+ function's docstring for the case-safety blocker this closes.
993
+ ``name_map is None`` seeds nothing, so this reduces to exactly what
994
+ :func:`_sanitize_for_tptp` would build from scratch, MODULO that same
995
+ case-safety fix — the ``name_map`` argument's documented "``None``
996
+ builds a fresh mapping" behaviour is this function's degenerate case,
997
+ not a separate code path.
998
+
999
+ This is what makes reusing a caller-supplied ``name_map`` actually SAFE:
1000
+ a symbol ``name_map`` already covers keeps exactly that spelling (the
1001
+ documented "identical spellings across a problem export and this proof"
1002
+ contract), while a symbol ``derivation`` uses that ``name_map`` does
1003
+ *not* cover — e.g. a Skolem constant introduced only in the proof, never
1004
+ in the problem the map was built from — is collected/finalised exactly
1005
+ as a fresh :func:`_sanitize_for_tptp` call would (modulo the case-safety
1006
+ fix above): an already-TPTP-legal, already-correctly-cased name passes
1007
+ through unchanged; anything else (non-ASCII, digit-leading, wrong-case,
1008
+ …) is synthesised a legal, correctly-cased token, de-collided against
1009
+ every token ``name_map`` already chose *and* every other symbol
1010
+ ``derivation`` uses. Previously :func:`_apply_forward_tptp` passed an
1011
+ uncovered name through completely unsanitised, which silently emitted
1012
+ illegal TSTP for exactly this scenario (a digit-leading or non-ASCII
1013
+ symbol absent from a caller's ``name_map``); this closes that gap before
1014
+ any text is ever rendered, so illegal TSTP can no longer reach
1015
+ :func:`to_tstp`'s output. A second, more subtle instance of the same
1016
+ root cause — a symbol that is TPTP-LEGAL but wrong-case, e.g. a
1017
+ lower-case predicate or an upper-case-initial constant — silently broke
1018
+ the round trip rather than the raw legality (see
1019
+ :func:`_collect_name_case_safe`); both are closed the same way, by
1020
+ routing the name through synthesis instead of identity.
1021
+ """
1022
+ predicates = _Renamer(prefix="p", render=tptp_fold_first_letter,
1023
+ case_fix=_predicate_base_case)
1024
+ terms = _Renamer(prefix="n", case_fix=_term_base_case,
1025
+ render=lambda n: tptp_fold_first_letter(constant_name_to_ascii(n)))
1026
+ if name_map is not None:
1027
+ predicates.mapping = dict(name_map.predicate)
1028
+ terms.mapping = dict(name_map.term)
1029
+ predicates.used = {predicates.render(token) for token in predicates.mapping.values()}
1030
+ terms.used = {terms.render(token) for token in terms.mapping.values()}
1031
+ literals = [literal for step in derivation.steps for literal in step.clause]
1032
+ for literal in literals:
1033
+ _collect_names_for_derivation(literal, predicates, terms)
1034
+ # A numeral is a constant of its own (see :mod:`atp._tptp_problem`): it is collected under
1035
+ # the name of its value, like the problem writers do, and recorded as a numeral.
1036
+ _, numerals = numerals_as_constants(literals, where="to_tstp")
1037
+ for name in sorted(numerals):
1038
+ _collect_name_case_safe(terms, name)
1039
+ predicates.finalize()
1040
+ terms.finalize()
1041
+ return TptpNameMap(predicate=predicates.mapping, term=terms.mapping,
1042
+ numerals=numerals | (name_map.numerals if name_map is not None
1043
+ else frozenset()))
1044
+
1045
+
1046
+ def _name_map_sentinel_nodes(name_map: TptpNameMap) -> List[Node]:
1047
+ """One bare, zero-arity sentinel node per token ``name_map`` assigns —
1048
+ an ``Atom`` for each ``predicate`` value, a ``Constant`` for each
1049
+ ``term`` value — so :func:`atp._tptp_problem._check_no_symbol_collisions`
1050
+ can see every symbol a (possibly caller-supplied) ``name_map`` already
1051
+ commits to, not just the ones ``derivation`` happens to repeat.
1052
+
1053
+ Why this is needed (a second reviewer-found gap, distinct from the
1054
+ identity/case one above): the collision guard only ever walks actual AST
1055
+ nodes, so a token ``name_map`` records for a name that is NOT used
1056
+ anywhere in ``derivation`` — the writer's own advertised primary use
1057
+ case, reusing a mapping built from an unrelated problem — is otherwise
1058
+ invisible to it, even though it still ends up in the rendered text's
1059
+ namespace. :func:`_extend_name_map_for_derivation` does correctly seed
1060
+ ``used`` from every token a caller-supplied ``name_map`` already
1061
+ reserves, so a genuinely NEW symbol ``derivation`` introduces always
1062
+ gets de-collided against it via :meth:`~atp._tptp_problem._Renamer
1063
+ .finalize`'s ``reserve_rendered`` call — but an already-legal,
1064
+ already-correctly-cased identity (the fast path both
1065
+ :meth:`atp._tptp_problem._Renamer.collect` and
1066
+ :func:`_collect_name_case_safe` above take) is reserved unconditionally,
1067
+ without first checking ``used`` — by design (R1 in
1068
+ :class:`atp._tptp_problem._Renamer`'s own docstring: "an already-legal
1069
+ name is never renamed", so it must never be skipped just because it
1070
+ might collide). If ``name_map`` ITSELF already carries a case-UNSAFE
1071
+ identity entry for one name (which can legitimately happen today, since
1072
+ :mod:`atp._tptp_problem`'s own ``_Renamer.collect`` still has the exact
1073
+ ``_is_tptp_safe``-only gap :func:`_collect_name_case_safe` closes for
1074
+ THIS module — e.g. ``generate_tptp_problem_with_mapping`` on a
1075
+ ``Constant("Foo")`` returns ``mapping.term == {"Foo": "Foo"}``, and
1076
+ fixing that is outside this item's file ownership), a derivation that
1077
+ separately introduces that entry's correctly-cased counterpart (e.g.
1078
+ ``"foo"``) takes the fast identity path on ITS OWN side too — both are
1079
+ "already legal, never renamed" in isolation, so neither is ever compared
1080
+ to the other unless something also walks ``name_map``'s own tokens.
1081
+ This is that something: it is passed to :func:`atp._tptp_problem
1082
+ ._check_no_symbol_collisions` alongside the derivation's own sanitised
1083
+ literals, so the two are checked together, not merged silently.
1084
+ """
1085
+ nodes: List[Node] = []
1086
+ for token in name_map.predicate.values():
1087
+ nodes.append(Atom(token, ()))
1088
+ for token in name_map.term.values():
1089
+ nodes.append(Constant(token))
1090
+ return nodes
1091
+
1092
+
1093
+ def _apply_forward_tptp(node: Node, mapping: TptpNameMap) -> Node:
1094
+ """Translate ``node``'s predicate/function/constant names from kit-level
1095
+ to the sanitised TPTP-ASCII tokens ``mapping`` records — the forward
1096
+ companion to :func:`atp._tptp_problem.apply_reverse_tptp`, walking a
1097
+ :class:`Node` the exact same way
1098
+ :func:`atp._tptp_problem._sanitize_node_for_tptp` does for the problem writers
1099
+ (equality is never renamed; the arithmetic operators and comparisons are
1100
+ ordinary symbols, and a numeral is the constant of the word ``mapping`` records
1101
+ for its value).
1102
+
1103
+ A name absent from ``mapping`` is left exactly as it is — safe ONLY
1104
+ because :func:`to_tstp` always calls this with a ``mapping`` already
1105
+ extended by :func:`_extend_name_map_for_derivation`, which guarantees
1106
+ every predicate/function/constant name ``node`` can possibly carry (it
1107
+ walks the very same ``derivation.steps`` this ``node`` came from) is
1108
+ already present, sanitised (and correctly CASED — see
1109
+ :func:`_collect_name_case_safe`) if it needed to be. This passthrough
1110
+ branch is exercised only by a name that is both already-TPTP-legal AND
1111
+ already in this namespace's own round-trip-safe case (mapped to itself
1112
+ by :func:`_collect_name_case_safe`), never by a name genuinely missing
1113
+ from ``mapping`` or one whose case would not survive the reader's own
1114
+ first-letter fold/cap — do not call this directly with a hand-built or
1115
+ otherwise unextended ``mapping``. :func:`to_tstp` also runs
1116
+ :func:`atp._tptp_problem._check_no_symbol_collisions` over every
1117
+ sanitised literal AND, via :func:`_name_map_sentinel_nodes`, over every
1118
+ token the (possibly caller-supplied) ``mapping`` itself already commits
1119
+ to — so two distinct symbols that happen to render to the same token are
1120
+ still caught, not silently merged, even when one of the two lives only
1121
+ in a reused ``name_map`` and never appears in ``derivation`` itself
1122
+ (mirroring :func:`atp._tptp_problem.generate_tptp_problem_with_mapping`'s
1123
+ own sanitise-then-check-collisions layering, widened to cover a reused
1124
+ mapping's own entries too).
1125
+ """
1126
+ if isinstance(node, Atom):
1127
+ if _is_fixed_atom(node, True):
1128
+ pred = node.predicate
1129
+ else:
1130
+ pred = mapping.predicate.get(node.predicate, node.predicate)
1131
+ return Atom(pred, tuple(_apply_forward_tptp(a, mapping) for a in node.args))
1132
+ if isinstance(node, Function):
1133
+ return Function(mapping.term.get(node.name, node.name),
1134
+ tuple(_apply_forward_tptp(a, mapping) for a in node.args))
1135
+ if isinstance(node, Constant):
1136
+ return Constant(mapping.term.get(node.name, node.name))
1137
+ if isinstance(node, Number):
1138
+ # A numeral is a constant (the problem writers' reading): the word the map has for
1139
+ # its value. (A clause has no counting bound, so every Number here is a term.)
1140
+ name = numeral_name(node.value)
1141
+ if name in mapping.numerals:
1142
+ return Constant(mapping.term[name])
1143
+ return node
1144
+ return node.map_children(lambda c: _apply_forward_tptp(c, mapping))
1145
+
1146
+
1147
+ def _render_clause_tptp(clause: FrozenSet[Node], mapping: TptpNameMap) -> List[Node]:
1148
+ """Sanitise every literal of ``clause`` via ``mapping``
1149
+ (:func:`_apply_forward_tptp`), in :func:`atp.resolution_check._lit_key`
1150
+ order — the same deterministic literal ordering
1151
+ :func:`atp.resolution_check.render_resolution_proof` already uses, so
1152
+ :func:`to_tstp`'s output is reproducible run to run regardless of
1153
+ ``frozenset`` iteration order. Returns the sanitised literal Nodes, not
1154
+ yet rendered to text — :func:`to_tstp` both joins them with ``Node
1155
+ .to_tptp()`` for the emitted line AND collects them (still as Nodes)
1156
+ for the whole-derivation collision check, so keeping this a Node list
1157
+ avoids parsing the emitted text back just to check it.
1158
+ """
1159
+ return [_apply_forward_tptp(lit, mapping) for lit in sorted(clause, key=_lit_key)]
1160
+
1161
+
1162
+ def to_tstp(derivation: ResolutionDerivation, *, name_map: Optional[TptpNameMap] = None) -> str:
1163
+ """Serialise a CERTIFIED :class:`~atp.resolution_check.ResolutionDerivation`
1164
+ as annotated TSTP ``cnf(...).`` text — the write-side companion to
1165
+ :func:`parse_tstp_derivation` above (see this section's module comment
1166
+ for the precise, narrower-than-it-sounds scope this covers).
1167
+
1168
+ One line per step, in :attr:`~atp.resolution_check.ResolutionDerivation
1169
+ .steps` order (already 1-indexed, matching each step's position):
1170
+
1171
+ - ``rule == "input"``: ``cnf(c<index>, plain, <clause>).`` — no source
1172
+ annotation at all, matching how :func:`parse_tstp_derivation` treats a
1173
+ leaf statement (``TstpStep.rule is None`` for one with no ``source``
1174
+ field).
1175
+ - otherwise: ``cnf(c<index>, plain, <clause>, inference(<rule>,
1176
+ [status(thm)], [c<p>, ...])).``, ``<rule>`` from :data:`_KIT_RULE_TO_TSTP`
1177
+ and ``<p>`` ranging over ``step.parents`` in citation order.
1178
+
1179
+ ``<clause>`` is ``$false`` for the empty clause, else its literals in
1180
+ :func:`atp.resolution_check._lit_key` order (matching
1181
+ :func:`~atp.resolution_check.render_resolution_proof`'s own literal
1182
+ ordering, for reproducible output) joined by ``" | "``, each rendered via
1183
+ :meth:`~fol.nodes.Node.to_tptp` after its symbols have been made
1184
+ TPTP-ASCII-legal exactly as :func:`atp._tptp_problem
1185
+ .generate_tptp_problem_with_mapping` does its own premises/conclusion
1186
+ (:func:`atp._tptp_problem._sanitize_for_tptp`, then
1187
+ :func:`atp._tptp_problem._check_no_symbol_collisions` over the sanitised
1188
+ result — see :func:`_apply_forward_tptp`'s docstring). A literal that is the
1189
+ nullary atom ``$true`` / ``$false`` (TPTP's own propositions, which this kit's
1190
+ reader produces) is written verbatim and is no symbol of the derivation's.
1191
+
1192
+ **A predicate and a function/constant that render as the same word** (the
1193
+ class ``Agent`` and the role function ``agent``: ``agent(agent(a))``) are
1194
+ separated exactly as the ``fof`` writer separates them
1195
+ (:func:`atp._tptp_problem._separate_term_names`): the term side becomes
1196
+ ``agent_term``, over the literals of the WHOLE derivation, with or without
1197
+ a ``name_map``. A ``name_map`` that already carries the separation (one
1198
+ from :func:`atp._tptp_problem.generate_tptp_problem_with_mapping`) keeps
1199
+ its spelling; without one the same recipe is applied here, so the text is
1200
+ the same either way. The replacement depends only on WHICH symbols occur,
1201
+ never on their order, and :func:`reverse_map_derivation` with the final map
1202
+ (:func:`_to_tstp_with_mapping` returns it) restores the original names.
1203
+
1204
+ **A numeral and the arithmetic symbols.** As in the problem writers, a numeral is a
1205
+ constant (written under the word the map has for its value, ``n1`` for ``1`` and
1206
+ ``1.0``) and ``+ - * /`` and ``< > ≤ ≥`` are ordinary symbols, never TPTP's number
1207
+ literals and dollar words, which a prover reads as arithmetic. The final map records
1208
+ them, and :func:`reverse_map_derivation` hands ``n1`` back as ``Number(1)``.
1209
+
1210
+ Args:
1211
+ derivation: the derivation to serialise. MUST already satisfy
1212
+ :func:`atp.resolution_check.verify_resolution_proof` (see
1213
+ Raises) — this writer never serialises an uncertified
1214
+ derivation, loudly.
1215
+ name_map: an optional, already-built :class:`atp._tptp_problem
1216
+ .TptpNameMap` to reuse instead of sanitising ``derivation``'s own
1217
+ symbols from scratch — for a derivation over a problem some
1218
+ caller already exported via :func:`atp._tptp_problem
1219
+ .generate_tptp_problem_with_mapping`, passing that call's
1220
+ returned mapping here keeps every shared symbol's TPTP spelling
1221
+ identical between the problem file and this proof (e.g. so a
1222
+ caller's own :func:`atp.tstp_check.check_tstp_derivation` call
1223
+ can compare against premises in the SAME sanitised namespace).
1224
+ A symbol ``derivation`` uses that this mapping does NOT cover
1225
+ (e.g. a Skolem constant introduced only in the proof, never in
1226
+ the problem the mapping was built from) is still sanitised, not
1227
+ passed through unchanged — :func:`_extend_name_map_for_derivation`
1228
+ extends a copy of ``name_map`` with exactly those symbols before
1229
+ any text is rendered, so an uncovered non-ASCII or digit-leading
1230
+ name can never reach the output unsanitised. The same extension
1231
+ ALSO re-sanitises a symbol that is TPTP-legal but wrong-case for
1232
+ its namespace (a lower-case predicate, an upper-case-initial
1233
+ constant/function) instead of passing it through as an identity
1234
+ — an already-legal name only ever keeps its exact spelling when
1235
+ that spelling would also survive :func:`parse_tstp_derivation`'s
1236
+ own read-back unchanged (see :func:`_collect_name_case_safe`),
1237
+ so the round trip this module's own docstring advertises holds
1238
+ for every symbol, not only already-correctly-cased ones. When
1239
+ omitted, a fresh mapping is built from exactly the symbols
1240
+ ``derivation.steps`` uses (the same extension function, seeded
1241
+ with nothing — equivalent to :func:`atp._tptp_problem
1242
+ ._sanitize_for_tptp`, modulo that same case fix) — an
1243
+ already-TPTP-legal, already-correctly-cased name (the common
1244
+ case for the kit's own ASCII test fixtures, whose predicates are
1245
+ conventionally upper-case-initial and whose constants are
1246
+ conventionally lower-case-initial) maps to itself, so the
1247
+ emitted text is byte-identical to what
1248
+ :meth:`~fol.nodes.Node.to_tptp` alone would have produced.
1249
+
1250
+ Returns:
1251
+ The derivation's TSTP text, one ``cnf(...).`` statement per line,
1252
+ each newline-terminated (``""`` for an empty ``derivation.steps``).
1253
+
1254
+ Raises:
1255
+ ValueError: ``verify_resolution_proof(derivation)`` does not accept
1256
+ ``derivation`` — this writer serialises only what the kit has
1257
+ independently CERTIFIED (see this section's module comment); a
1258
+ derivation research code, a human, or another prover produced
1259
+ must be checked FIRST, exactly as any other consumer of
1260
+ :mod:`atp.resolution_check` is required to.
1261
+ NotImplementedError: two distinct kit-level symbol names would
1262
+ render as the same TPTP identifier (:func:`atp._tptp_problem
1263
+ ._check_no_symbol_collisions`, run over ``derivation``'s own
1264
+ sanitised literals AND, via :func:`_name_map_sentinel_nodes`,
1265
+ every token ``name_map`` itself already commits to — so a
1266
+ collision between a symbol reused from ``name_map`` and one
1267
+ ``derivation`` introduces is caught too, not only a collision
1268
+ entirely within ``derivation``), naming both — the same guard
1269
+ :func:`atp._tptp_problem.generate_tptp_problem_with_mapping`
1270
+ raises for a TPTP problem file, reused here verbatim rather than
1271
+ silently merging two distinct symbols into one.
1272
+ """
1273
+ return _to_tstp_with_mapping(derivation, name_map)[0]
1274
+
1275
+
1276
+ def _to_tstp_with_mapping(derivation: ResolutionDerivation,
1277
+ name_map: Optional[TptpNameMap] = None
1278
+ ) -> Tuple[str, TptpNameMap]:
1279
+ """:func:`to_tstp`, also returning the FINAL :class:`TptpNameMap`: the
1280
+ caller's ``name_map`` (or a fresh one) extended with every symbol the
1281
+ derivation introduces AND with the function/constant renames that separate
1282
+ them from a predicate rendered as the same word
1283
+ (:func:`atp._tptp_problem._separate_term_names`). It is what
1284
+ :func:`reverse_map_derivation` needs to restore the original names from the
1285
+ text this returns when no ``name_map`` was given."""
1286
+ check = verify_resolution_proof(derivation)
1287
+ if not check.ok:
1288
+ raise ValueError(
1289
+ f"to_tstp: derivation is not certified by verify_resolution_proof "
1290
+ f"({check.error}); this writer only serialises a derivation the "
1291
+ f"kit has independently checked, never an unverified one"
1292
+ )
1293
+
1294
+ name_map = _extend_name_map_for_derivation(derivation, name_map)
1295
+
1296
+ sanitised_by_index: Dict[int, List[Node]] = {
1297
+ step.index: _render_clause_tptp(step.clause, name_map)
1298
+ for step in derivation.steps
1299
+ }
1300
+ # One node per CLAUSE, so that the variables of a clause are one scope: the
1301
+ # writers check variables per formula (``x`` and ``X`` are one TPTP variable),
1302
+ # and the literals of a clause are one formula, not several.
1303
+ _check_no_symbol_collisions(
1304
+ _name_map_sentinel_nodes(name_map) +
1305
+ [functools.reduce(Or, literals) for literals in sanitised_by_index.values() if literals],
1306
+ where="to_tstp", subject="derivation",
1307
+ )
1308
+
1309
+ # A function/constant that renders as a predicate's word is renamed on the
1310
+ # term side, over every literal of the derivation at once (the fof writer's
1311
+ # own pass), then the literals are handed back to their clauses in order.
1312
+ flat = [lit for step in derivation.steps for lit in sanitised_by_index[step.index]]
1313
+ flat, name_map = _separate_term_names(flat, name_map)
1314
+ position = 0
1315
+ for step in derivation.steps:
1316
+ count = len(sanitised_by_index[step.index])
1317
+ sanitised_by_index[step.index] = flat[position:position + count]
1318
+ position += count
1319
+
1320
+ lines: List[str] = []
1321
+ for step in derivation.steps:
1322
+ literals = sanitised_by_index[step.index]
1323
+ clause_text = "$false" if not literals else " | ".join(lit.to_tptp() for lit in literals)
1324
+ if step.rule == "input":
1325
+ lines.append(f"cnf(c{step.index}, plain, {clause_text}).")
1326
+ else:
1327
+ rule_name = _KIT_RULE_TO_TSTP[step.rule]
1328
+ parents = ", ".join(f"c{p}" for p in step.parents)
1329
+ lines.append(
1330
+ f"cnf(c{step.index}, plain, {clause_text}, "
1331
+ f"inference({rule_name}, [status(thm)], [{parents}]))."
1332
+ )
1333
+ return "".join(line + "\n" for line in lines), name_map