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.
- unicode_logic_kit/__init__.py +385 -0
- unicode_logic_kit/__main__.py +520 -0
- unicode_logic_kit/_deadline.py +219 -0
- unicode_logic_kit/ace/__init__.py +126 -0
- unicode_logic_kit/ace/_align.py +135 -0
- unicode_logic_kit/ace/chem_lexicon.py +128 -0
- unicode_logic_kit/ace/drs_reader.py +570 -0
- unicode_logic_kit/ace/mapping.py +666 -0
- unicode_logic_kit/ace/reverse_modal.py +138 -0
- unicode_logic_kit/ace/runner.py +551 -0
- unicode_logic_kit/ace/translate.py +452 -0
- unicode_logic_kit/ace/verbalize.py +1070 -0
- unicode_logic_kit/api.py +1284 -0
- unicode_logic_kit/atp/__init__.py +177 -0
- unicode_logic_kit/atp/_ascii_names.py +113 -0
- unicode_logic_kit/atp/_html.py +72 -0
- unicode_logic_kit/atp/_substructural_input.py +228 -0
- unicode_logic_kit/atp/_tff_problem.py +715 -0
- unicode_logic_kit/atp/_tptp_problem.py +1111 -0
- unicode_logic_kit/atp/_writer_support.py +289 -0
- unicode_logic_kit/atp/clingo_backend.py +1180 -0
- unicode_logic_kit/atp/cvc5_backend.py +1385 -0
- unicode_logic_kit/atp/eprover_backend.py +732 -0
- unicode_logic_kit/atp/finite_domain.py +1055 -0
- unicode_logic_kit/atp/fitch.py +1547 -0
- unicode_logic_kit/atp/fitch_search.py +551 -0
- unicode_logic_kit/atp/hets_backend.py +339 -0
- unicode_logic_kit/atp/hybrid_down.py +120 -0
- unicode_logic_kit/atp/incremental.py +250 -0
- unicode_logic_kit/atp/kripke_enum.py +741 -0
- unicode_logic_kit/atp/lambek.py +436 -0
- unicode_logic_kit/atp/leo3_backend.py +332 -0
- unicode_logic_kit/atp/linear.py +738 -0
- unicode_logic_kit/atp/lj.py +705 -0
- unicode_logic_kit/atp/logic_backends.py +566 -0
- unicode_logic_kit/atp/ltl_tableau.py +1084 -0
- unicode_logic_kit/atp/minizinc_backend.py +1402 -0
- unicode_logic_kit/atp/modal_tableau.py +1382 -0
- unicode_logic_kit/atp/nanocop_backend.py +410 -0
- unicode_logic_kit/atp/portfolio.py +489 -0
- unicode_logic_kit/atp/protocol.py +1803 -0
- unicode_logic_kit/atp/prover9_entailment.py +1153 -0
- unicode_logic_kit/atp/resolution.py +1376 -0
- unicode_logic_kit/atp/resolution_check.py +1114 -0
- unicode_logic_kit/atp/sequent.py +1050 -0
- unicode_logic_kit/atp/tableau.py +921 -0
- unicode_logic_kit/atp/tableau_check.py +543 -0
- unicode_logic_kit/atp/tptp_ncl.py +811 -0
- unicode_logic_kit/atp/tptp_tff.py +1546 -0
- unicode_logic_kit/atp/tstp.py +1333 -0
- unicode_logic_kit/atp/tstp_check.py +1096 -0
- unicode_logic_kit/atp/twee_backend.py +236 -0
- unicode_logic_kit/atp/twee_check.py +711 -0
- unicode_logic_kit/atp/twee_entailment.py +953 -0
- unicode_logic_kit/atp/vampire_entailment.py +540 -0
- unicode_logic_kit/atp/z3_arith.py +470 -0
- unicode_logic_kit/atp/z3_equivalence.py +36 -0
- unicode_logic_kit/atp/z3_fuzzy.py +362 -0
- unicode_logic_kit/atp/z3_input.py +500 -0
- unicode_logic_kit/atp/z3_models.py +208 -0
- unicode_logic_kit/chem/__init__.py +88 -0
- unicode_logic_kit/chem/_naming.py +284 -0
- unicode_logic_kit/chem/cache.py +185 -0
- unicode_logic_kit/chem/interop.py +244 -0
- unicode_logic_kit/chem/mol.py +525 -0
- unicode_logic_kit/chem/signature.py +112 -0
- unicode_logic_kit/comorphism.py +497 -0
- unicode_logic_kit/dl/__init__.py +384 -0
- unicode_logic_kit/dl/classification.py +227 -0
- unicode_logic_kit/dl/concepts.py +632 -0
- unicode_logic_kit/dl/datatypes.py +818 -0
- unicode_logic_kit/dl/owl_functional.py +2433 -0
- unicode_logic_kit/dl/owl_manchester.py +1637 -0
- unicode_logic_kit/dl/owl_reasoner.py +790 -0
- unicode_logic_kit/dl/parser.py +391 -0
- unicode_logic_kit/dl/tableau.py +4048 -0
- unicode_logic_kit/dl/translate.py +2704 -0
- unicode_logic_kit/drt/__init__.py +94 -0
- unicode_logic_kit/drt/export.py +179 -0
- unicode_logic_kit/drt/nodes.py +506 -0
- unicode_logic_kit/drt/parser.py +965 -0
- unicode_logic_kit/drt/resolve.py +195 -0
- unicode_logic_kit/drt/reverse.py +175 -0
- unicode_logic_kit/eval/__init__.py +106 -0
- unicode_logic_kit/eval/batch.py +382 -0
- unicode_logic_kit/eval/canonical.py +663 -0
- unicode_logic_kit/eval/chem_batch.py +606 -0
- unicode_logic_kit/eval/converses.py +200 -0
- unicode_logic_kit/eval/datasets/__init__.py +136 -0
- unicode_logic_kit/eval/datasets/_base.py +263 -0
- unicode_logic_kit/eval/datasets/_proofwriter_proof.py +422 -0
- unicode_logic_kit/eval/datasets/c3po.py +678 -0
- unicode_logic_kit/eval/datasets/folio.py +158 -0
- unicode_logic_kit/eval/datasets/fracas.py +418 -0
- unicode_logic_kit/eval/datasets/groves.py +191 -0
- unicode_logic_kit/eval/datasets/logicbench.py +467 -0
- unicode_logic_kit/eval/datasets/logicnli.py +303 -0
- unicode_logic_kit/eval/datasets/malls.py +133 -0
- unicode_logic_kit/eval/datasets/pfolio.py +594 -0
- unicode_logic_kit/eval/datasets/pmb.py +242 -0
- unicode_logic_kit/eval/datasets/prontoqa.py +611 -0
- unicode_logic_kit/eval/datasets/proofwriter.py +1431 -0
- unicode_logic_kit/eval/datasets/proverqa.py +674 -0
- unicode_logic_kit/eval/datasets/willow.py +478 -0
- unicode_logic_kit/eval/equivalence.py +466 -0
- unicode_logic_kit/eval/exercise_gen.py +533 -0
- unicode_logic_kit/eval/explain.py +791 -0
- unicode_logic_kit/eval/generality.py +750 -0
- unicode_logic_kit/eval/metric_hf.py +458 -0
- unicode_logic_kit/eval/predicate_match.py +343 -0
- unicode_logic_kit/eval/theory_check.py +1170 -0
- unicode_logic_kit/eval/validate.py +306 -0
- unicode_logic_kit/fol/__init__.py +177 -0
- unicode_logic_kit/fol/_atom_keys.py +510 -0
- unicode_logic_kit/fol/_fol_nodes.py +3586 -0
- unicode_logic_kit/fol/_free_parameters.py +105 -0
- unicode_logic_kit/fol/_ho_nodes.py +448 -0
- unicode_logic_kit/fol/_hybrid_nodes.py +308 -0
- unicode_logic_kit/fol/_identifiers.py +1091 -0
- unicode_logic_kit/fol/_lambek_nodes.py +112 -0
- unicode_logic_kit/fol/_linear_nodes.py +352 -0
- unicode_logic_kit/fol/_modal_nodes.py +1467 -0
- unicode_logic_kit/fol/_msfl_nodes.py +2196 -0
- unicode_logic_kit/fol/_numeral_symbols.py +231 -0
- unicode_logic_kit/fol/_so_nodes.py +200 -0
- unicode_logic_kit/fol/_symbol_names.py +81 -0
- unicode_logic_kit/fol/_team_nodes.py +181 -0
- unicode_logic_kit/fol/_tptp_symbols.py +551 -0
- unicode_logic_kit/fol/_truth_constants.py +117 -0
- unicode_logic_kit/fol/casl_export.py +1135 -0
- unicode_logic_kit/fol/casl_import.py +929 -0
- unicode_logic_kit/fol/derivation.py +367 -0
- unicode_logic_kit/fol/dialect_detect.py +70 -0
- unicode_logic_kit/fol/dialect_repair.py +537 -0
- unicode_logic_kit/fol/frames.py +637 -0
- unicode_logic_kit/fol/grammars/terminals.lark +31 -0
- unicode_logic_kit/fol/lambda_tools.py +297 -0
- unicode_logic_kit/fol/latex_input.py +429 -0
- unicode_logic_kit/fol/modal_translation.py +944 -0
- unicode_logic_kit/fol/msflparser.py +1033 -0
- unicode_logic_kit/fol/naming.py +422 -0
- unicode_logic_kit/fol/nodes.py +241 -0
- unicode_logic_kit/fol/normalforms.py +492 -0
- unicode_logic_kit/fol/pal.py +287 -0
- unicode_logic_kit/fol/prolog_export.py +566 -0
- unicode_logic_kit/fol/prolog_input.py +505 -0
- unicode_logic_kit/fol/prover9_input.py +1325 -0
- unicode_logic_kit/fol/qml.py +1760 -0
- unicode_logic_kit/fol/qmltp_input.py +525 -0
- unicode_logic_kit/fol/sanitize.py +221 -0
- unicode_logic_kit/fol/serialize.py +79 -0
- unicode_logic_kit/fol/signature.py +1290 -0
- unicode_logic_kit/fol/simplify_check.py +544 -0
- unicode_logic_kit/fol/spans.py +594 -0
- unicode_logic_kit/fol/tptp_input.py +1503 -0
- unicode_logic_kit/fol/tptp_repair.py +941 -0
- unicode_logic_kit/fol/unification.py +157 -0
- unicode_logic_kit/fol/verbalize.py +263 -0
- unicode_logic_kit/hets/__init__.py +163 -0
- unicode_logic_kit/hets/bridge.py +142 -0
- unicode_logic_kit/hets/client.py +748 -0
- unicode_logic_kit/hets/docker.py +420 -0
- unicode_logic_kit/hets/dol.py +712 -0
- unicode_logic_kit/hets/haskell_json.py +355 -0
- unicode_logic_kit/hets/owl_backend.py +794 -0
- unicode_logic_kit/hets/owl_cli.py +598 -0
- unicode_logic_kit/hets/symbols.py +512 -0
- unicode_logic_kit/hol/__init__.py +140 -0
- unicode_logic_kit/hol/_ho_common.py +323 -0
- unicode_logic_kit/hol/_isabelle_binders.py +125 -0
- unicode_logic_kit/hol/classical.py +812 -0
- unicode_logic_kit/hol/deepshallow/__init__.py +45 -0
- unicode_logic_kit/hol/deepshallow/_common.py +177 -0
- unicode_logic_kit/hol/deepshallow/conditional.py +225 -0
- unicode_logic_kit/hol/deepshallow/intuitionistic.py +181 -0
- unicode_logic_kit/hol/deepshallow/modal.py +217 -0
- unicode_logic_kit/hol/deepshallow/qml.py +406 -0
- unicode_logic_kit/hol/deepshallow/relevant.py +206 -0
- unicode_logic_kit/hol/free.py +753 -0
- unicode_logic_kit/hol/goedel.py +336 -0
- unicode_logic_kit/hol/ho_modal.py +1743 -0
- unicode_logic_kit/hol/intuitionistic.py +403 -0
- unicode_logic_kit/hol/isabelle_conditional.py +593 -0
- unicode_logic_kit/hol/isabelle_modal.py +1908 -0
- unicode_logic_kit/hol/isabelle_relevant.py +412 -0
- unicode_logic_kit/hol/isabelle_runner.py +1147 -0
- unicode_logic_kit/hol/isabelle_substructural.py +884 -0
- unicode_logic_kit/hol/lean.py +1018 -0
- unicode_logic_kit/hol/manyvalued.py +921 -0
- unicode_logic_kit/hol/secondorder.py +687 -0
- unicode_logic_kit/hol/thf_modal.py +941 -0
- unicode_logic_kit/hol/thirdorder.py +397 -0
- unicode_logic_kit/ilp/__init__.py +89 -0
- unicode_logic_kit/ilp/readback.py +389 -0
- unicode_logic_kit/ilp/separation.py +153 -0
- unicode_logic_kit/ilp/task.py +730 -0
- unicode_logic_kit/logic.py +163 -0
- unicode_logic_kit/mcp/__init__.py +28 -0
- unicode_logic_kit/mcp/__main__.py +5 -0
- unicode_logic_kit/mcp/chem_tools.py +1031 -0
- unicode_logic_kit/mcp/server.py +2453 -0
- unicode_logic_kit/mcp/syntax_spec.py +681 -0
- unicode_logic_kit/prob/__init__.py +53 -0
- unicode_logic_kit/prob/_bdd.py +225 -0
- unicode_logic_kit/prob/_column_gen.py +668 -0
- unicode_logic_kit/prob/distribution.py +686 -0
- unicode_logic_kit/prob/nilsson.py +470 -0
- unicode_logic_kit/py.typed +0 -0
- unicode_logic_kit/semantics/__init__.py +137 -0
- unicode_logic_kit/semantics/_modal_reject.py +156 -0
- unicode_logic_kit/semantics/action_models.py +466 -0
- unicode_logic_kit/semantics/asp_models.py +1200 -0
- unicode_logic_kit/semantics/conditional.py +580 -0
- unicode_logic_kit/semantics/dynamic_epistemic.py +95 -0
- unicode_logic_kit/semantics/free_logic.py +913 -0
- unicode_logic_kit/semantics/fuzzy.py +384 -0
- unicode_logic_kit/semantics/fuzzy_kripke.py +442 -0
- unicode_logic_kit/semantics/intuitionistic.py +581 -0
- unicode_logic_kit/semantics/kripke.py +1139 -0
- unicode_logic_kit/semantics/manyvalued.py +580 -0
- unicode_logic_kit/semantics/matrix.py +342 -0
- unicode_logic_kit/semantics/model_eval.py +1135 -0
- unicode_logic_kit/semantics/modelfinder.py +1036 -0
- unicode_logic_kit/semantics/nonmonotonic.py +372 -0
- unicode_logic_kit/semantics/relevant.py +331 -0
- unicode_logic_kit/semantics/secondorder.py +657 -0
- unicode_logic_kit/semantics/structures.py +352 -0
- unicode_logic_kit/semantics/tarski.py +975 -0
- unicode_logic_kit/semantics/team.py +315 -0
- unicode_logic_kit/semantics/team_translation.py +416 -0
- unicode_logic_kit/semantics/thirdorder.py +358 -0
- unicode_logic_kit/semantics/tnorm.py +85 -0
- unicode_logic_kit/semantics/truthtable.py +201 -0
- unicode_logic_kit-0.31.0.dist-info/METADATA +333 -0
- unicode_logic_kit-0.31.0.dist-info/RECORD +237 -0
- unicode_logic_kit-0.31.0.dist-info/WHEEL +4 -0
- 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
|