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,941 @@
|
|
|
1
|
+
"""Repair layer over :mod:`unicode_logic_kit.fol.tptp_input` for LLM-generated TPTP.
|
|
2
|
+
|
|
3
|
+
LLM-generated TPTP fails on pure SYNTAX often enough to be worth repairing
|
|
4
|
+
rather than retrying — a syntax rejection costs a whole generation attempt.
|
|
5
|
+
Three shapes account for nearly all of it:
|
|
6
|
+
|
|
7
|
+
1. **Biimplication structure error** — the LLM writes
|
|
8
|
+
``predicate(x) <=> condition1 & condition2`` and a strict downstream
|
|
9
|
+
parser (gavel) rejects it for lacking explicit brackets around the
|
|
10
|
+
right side.
|
|
11
|
+
2. **Invalid predicate name** — a chemical name used as a
|
|
12
|
+
predicate/function symbol starts with a digit or contains characters TPTP
|
|
13
|
+
forbids in a bare ``lower_word`` (hyphens, commas, parentheses — e.g. a
|
|
14
|
+
stereodescriptor prefix like ``(2S)`` or a positional prefix like
|
|
15
|
+
``1,2-``). Renaming such a name to a plain camelCase identifier
|
|
16
|
+
*loses* exactly the chemical information those characters encode and
|
|
17
|
+
severs the link back to the ChEBI class; TPTP single-quoting
|
|
18
|
+
(``'1,2-diacyl-sn-glycero-3-phosphocholine'``) keeps both.
|
|
19
|
+
3. **Unbound variable error** — e.g.
|
|
20
|
+
``threeOxoSteroid(X) <=> (steroid & ?[A1,A2]: (c(A1) & o(A2))))``: ``X``
|
|
21
|
+
on the left never occurs on the right. The conservative fix drops the
|
|
22
|
+
argument (``threeOxoSteroid <=> …``) precisely because the definition is
|
|
23
|
+
a 0-ary, molecule-level property, not a property of some individual
|
|
24
|
+
``X`` — and this shape is NOT reliably fixable by a parser heuristic in
|
|
25
|
+
general.
|
|
26
|
+
|
|
27
|
+
This module's premise (verified against :func:`parse_tptp_formula` before
|
|
28
|
+
writing a line here): the kit's own Lark TPTP grammar already ACCEPTS all
|
|
29
|
+
three shapes — case 1 by having the same precedence ladder gavel wants made
|
|
30
|
+
explicit, case 2 via its single-quoted-atom (``SQ``) token, case 3 by simply
|
|
31
|
+
leaving the variable free in the AST rather than rejecting it. So repairing is
|
|
32
|
+
not "write a smarter TPTP parser"; it is: parse with the grammar the kit
|
|
33
|
+
already has, look at what came out, and turn that into gavel-compatible text
|
|
34
|
+
— or, for case 2 alone (the one shape the grammar cannot even tokenise
|
|
35
|
+
unquoted), a narrow, auditable text rewrite BEFORE parsing.
|
|
36
|
+
|
|
37
|
+
Public API
|
|
38
|
+
----------
|
|
39
|
+
:func:`repair_tptp_formula` — repair one bare TPTP formula, returning a
|
|
40
|
+
:class:`RepairResult` (``ok``, ``formula``, ``repaired_text``, ``issues``,
|
|
41
|
+
``changed``). :func:`repair_tptp_problem` — the same over a whole
|
|
42
|
+
``fof(name, role, formula).`` file, formula by formula, preserving every
|
|
43
|
+
statement's name/role/comments and touching only the formula span that needed
|
|
44
|
+
it.
|
|
45
|
+
|
|
46
|
+
Hard limits (refuse rather than silently reinterpret)
|
|
47
|
+
-------------------------------------------------------
|
|
48
|
+
* Case 2's raw-text detector is a narrow, two-shape heuristic (see
|
|
49
|
+
:data:`_CANDIDATE_RE`), not a general TPTP-syntax-error corrector: genuinely
|
|
50
|
+
broken input (e.g. unbalanced parentheses) that isn't explained by an
|
|
51
|
+
unquoted invalid name is reported ``ok=False`` with the parser's own
|
|
52
|
+
diagnosis, never guessed at.
|
|
53
|
+
* Case 3's fix is applied ONLY with ``close_free_variables=True`` (default
|
|
54
|
+
``False``: report, don't touch), and even then only the narrow
|
|
55
|
+
``predicate(X) <=> …`` / ``predicate(X) => …`` shape (exactly: the free
|
|
56
|
+
variable is the sole argument of the definiendum on the LEFT, and does not
|
|
57
|
+
recur on the right) gets the meaning-preserving argument-drop; anything
|
|
58
|
+
else gets the standard, well-understood universal closure (∀-wrap every
|
|
59
|
+
free variable) — never a guess at what the author meant that could go
|
|
60
|
+
either way.
|
|
61
|
+
* A predicate name that needed quoting because :mod:`tptp_input` failed to
|
|
62
|
+
parse it unquoted is quoted using the EXACT original substring, tracked
|
|
63
|
+
through re-parsing by exact ``_cap()``-image lookup (see the
|
|
64
|
+
``known_names`` machinery below ``_uncap``) — no case folding, no
|
|
65
|
+
character loss, EVER, for a name this module itself detected and fixed.
|
|
66
|
+
If two DISTINCT such candidate names would reduce to the SAME ``_cap()``
|
|
67
|
+
image (e.g. ``"N-acetyl-x"`` and ``"n-acetyl-x"``, differing only in the
|
|
68
|
+
case of their first letter — both invalid unquoted, both quoted by this
|
|
69
|
+
module, both reparsing to the identical ``Atom`` predicate), quoting
|
|
70
|
+
BOTH would silently merge two distinct predicates into one; this is
|
|
71
|
+
detected while ``known_names`` is built and reported ``ok=False`` (a
|
|
72
|
+
``"name_collision"`` issue) rather than silently keeping one of the two.
|
|
73
|
+
* For a name the CALLER already had quoted before this module saw the
|
|
74
|
+
text, the raw text is scanned independently (:func:`_find_prequoted_names`
|
|
75
|
+
— a best-effort, position-agnostic sibling of the case-2 detector above)
|
|
76
|
+
for every single-quoted literal and its exact original spelling recovered
|
|
77
|
+
by the same ``_cap()``-image lookup; this covers every already-quoted name
|
|
78
|
+
that appears unambiguously (its ``_cap()`` image doesn't collide with a
|
|
79
|
+
DIFFERENT quoted original found elsewhere in the same text). It cannot
|
|
80
|
+
distinguish a quoted name's PREDICATE vs. term (function/constant) role
|
|
81
|
+
from raw text alone, but that distinction is immaterial here: only
|
|
82
|
+
``Atom.predicate`` rendering ever consults it (``Function``/``Constant``
|
|
83
|
+
rendering never folds case in the first place, so it never needs it).
|
|
84
|
+
The one case this cannot recover — two DIFFERENT quoted originals sharing
|
|
85
|
+
one ``_cap()`` image (e.g. both ``'Foo'`` and ``'foo'`` literally present
|
|
86
|
+
in the text) — falls back to :func:`_uncap`'s guess for either, exactly as
|
|
87
|
+
before this scan existed, never worse. :func:`_uncap` assumes the true
|
|
88
|
+
original was lower-case, which is always correct for a name that arrived
|
|
89
|
+
via a bare (unquoted) functor — the TPTP grammar itself requires that
|
|
90
|
+
first character to already be lower-case, so there is no other
|
|
91
|
+
possibility for such a name — and is otherwise a last-resort, irreversible
|
|
92
|
+
GUESS reached only when neither of the two recovery mechanisms above
|
|
93
|
+
applies. This residual gap is real but narrow and fully documented at
|
|
94
|
+
:func:`_uncap`; it never fires on either literal example above (both start
|
|
95
|
+
with a digit / a parenthesis, so :func:`_uncap` is a no-op regardless of
|
|
96
|
+
which branch computes it).
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
import re
|
|
100
|
+
from dataclasses import dataclass
|
|
101
|
+
from typing import Optional, Tuple
|
|
102
|
+
|
|
103
|
+
from ._fol_nodes import _number_text, constant_name_to_ascii
|
|
104
|
+
from ._tptp_symbols import truth_constant_word
|
|
105
|
+
from .naming import ParsingError
|
|
106
|
+
from .nodes import (
|
|
107
|
+
Node, Variable, Constant, Number, Function,
|
|
108
|
+
Atom, Not, And, Or, Xor, Implies, Iff, Quantifier,
|
|
109
|
+
free_variables,
|
|
110
|
+
)
|
|
111
|
+
from .tptp_input import parse_tptp_formula, _cap, _functor_name
|
|
112
|
+
|
|
113
|
+
__all__ = [
|
|
114
|
+
"Issue", "RepairResult", "repair_tptp_formula",
|
|
115
|
+
"ProblemRepairEntry", "ProblemRepairResult", "repair_tptp_problem",
|
|
116
|
+
"TptpRepairError",
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class TptpRepairError(Exception):
|
|
121
|
+
"""Raised when :func:`repair_tptp_problem` cannot even locate statement
|
|
122
|
+
boundaries in a ``fof(...)``/``cnf(...)`` file (malformed wrapper) — a
|
|
123
|
+
structural problem this module refuses to guess through, distinct from a
|
|
124
|
+
per-formula syntax issue (which :func:`repair_tptp_formula` reports
|
|
125
|
+
instead of raising)."""
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@dataclass(frozen=True)
|
|
129
|
+
class Issue:
|
|
130
|
+
"""One diagnostic from a repair pass.
|
|
131
|
+
|
|
132
|
+
``kind`` is one of ``"bracket_normalisation"``, ``"invalid_predicate_name"``,
|
|
133
|
+
``"free_variable"``, ``"syntax_error"`` (input :func:`repair_tptp_formula`
|
|
134
|
+
could not turn into a valid formula at all), or ``"name_collision"`` (two
|
|
135
|
+
of THIS module's own case-2 candidate names would be indistinguishable
|
|
136
|
+
once rendered — see :func:`repair_tptp_formula`'s known_names collision
|
|
137
|
+
check). ``suggestion`` is optional and, when present, is the concrete
|
|
138
|
+
text that either WAS applied (``changed=True``) or COULD be applied by
|
|
139
|
+
opting in to the relevant flag.
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
kind: str
|
|
143
|
+
message: str
|
|
144
|
+
suggestion: Optional[str] = None
|
|
145
|
+
|
|
146
|
+
def to_dict(self) -> dict:
|
|
147
|
+
return {"kind": self.kind, "message": self.message, "suggestion": self.suggestion}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@dataclass(frozen=True)
|
|
151
|
+
class RepairResult:
|
|
152
|
+
"""Outcome of :func:`repair_tptp_formula`.
|
|
153
|
+
|
|
154
|
+
``ok``: a valid :class:`Node` was produced (either the input already
|
|
155
|
+
parsed, or a case-2 text rewrite made it parse). ``False`` only for
|
|
156
|
+
genuinely unrepairable syntax — see the module docstring's hard limits.
|
|
157
|
+
Note ``ok=True`` does NOT mean issue-free: an unclosed free variable
|
|
158
|
+
(case 3, default ``close_free_variables=False``) still yields
|
|
159
|
+
``ok=True`` with a ``"free_variable"`` issue, because the AST is
|
|
160
|
+
perfectly valid — the kit just refuses to guess how to close it.
|
|
161
|
+
|
|
162
|
+
``formula``: the repaired AST (``None`` iff ``ok`` is ``False``).
|
|
163
|
+
|
|
164
|
+
``repaired_text``: canonical, gavel-compatible TPTP — fully bracketed,
|
|
165
|
+
every name that needs quoting quoted — that re-parses to ``formula``
|
|
166
|
+
exactly (see :func:`repair_tptp_formula`'s docstring for the precedence
|
|
167
|
+
argument backing this for case 1). Equal to the ORIGINAL input text when
|
|
168
|
+
``ok`` is ``False`` (nothing is ever silently rewritten into something
|
|
169
|
+
unverified).
|
|
170
|
+
|
|
171
|
+
``issues``: every :class:`Issue` found, in detection order (name issues,
|
|
172
|
+
then free-variable issues; bracket normalisation — if any — is implicit
|
|
173
|
+
in whether ``repaired_text`` differs and is reported explicitly too).
|
|
174
|
+
|
|
175
|
+
``changed``: whether ``repaired_text`` differs from ``text`` (whitespace
|
|
176
|
+
collapsed away — pure reformatting is not "changed").
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
ok: bool
|
|
180
|
+
formula: Optional[Node]
|
|
181
|
+
repaired_text: str
|
|
182
|
+
issues: Tuple[Issue, ...]
|
|
183
|
+
changed: bool
|
|
184
|
+
|
|
185
|
+
def to_dict(self) -> dict:
|
|
186
|
+
return {
|
|
187
|
+
"ok": self.ok,
|
|
188
|
+
"formula": self.formula.to_dict() if self.formula is not None else None,
|
|
189
|
+
"repaired_text": self.repaired_text,
|
|
190
|
+
"issues": [i.to_dict() for i in self.issues],
|
|
191
|
+
"changed": self.changed,
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
# ---------------------------------------------------------------------------
|
|
196
|
+
# Case 2 — invalid predicate/function/constant names: raw-text detection
|
|
197
|
+
# ---------------------------------------------------------------------------
|
|
198
|
+
#
|
|
199
|
+
# tptp_input.py's grammar requires an unquoted functor name to be a TPTP
|
|
200
|
+
# `lower_word` (LOWER token: `[a-z][A-Za-z0-9_]*`) — this is not a gavel
|
|
201
|
+
# quirk, it is the TPTP standard, so a name violating it is rejected by ANY
|
|
202
|
+
# conformant parser, this kit's own included, unless quoted. That means the
|
|
203
|
+
# raw text genuinely fails to PARSE (not merely fails some downstream
|
|
204
|
+
# validator) before we ever get an AST to inspect — the fix has to happen at
|
|
205
|
+
# the text level, on the ORIGINAL characters, before parsing.
|
|
206
|
+
#
|
|
207
|
+
# _CANDIDATE_RE looks for a name-shaped span immediately followed by an
|
|
208
|
+
# argument list's "(" (i.e. a functor application), built from the two
|
|
209
|
+
# concrete shapes case 2 takes in practice:
|
|
210
|
+
#
|
|
211
|
+
# (a) a stray parenthesised prefix glued to more identifier characters,
|
|
212
|
+
# e.g. "(2S)Flavan4One" — `(?:\([^()\n]{1,40}\))?` optionally eats a
|
|
213
|
+
# single, non-nested parenthesised fragment first;
|
|
214
|
+
# (b) a run of letters/digits/commas/hyphens, e.g.
|
|
215
|
+
# "1,2-diacyl-sn-glycero-3-phosphocholine".
|
|
216
|
+
#
|
|
217
|
+
# A left-boundary negative lookbehind stops the match from starting mid-run
|
|
218
|
+
# (so scanning is deterministic and non-overlapping), and the whole candidate
|
|
219
|
+
# is discarded (never rewritten) if it ALREADY satisfies TPTP's lower_word
|
|
220
|
+
# rule — i.e. this only ever fires on text that would otherwise fail to
|
|
221
|
+
# parse, never on an ordinary `steroid(x)`-style call.
|
|
222
|
+
|
|
223
|
+
_VALID_UNQUOTED = re.compile(r"^[a-z][A-Za-z0-9_]*$")
|
|
224
|
+
|
|
225
|
+
_CANDIDATE_RE = re.compile(
|
|
226
|
+
r"(?<![A-Za-z0-9,\-)])"
|
|
227
|
+
r"((?:\([^()\n]{1,40}\))?[A-Za-z0-9][A-Za-z0-9,\-]*)"
|
|
228
|
+
r"(?=\s*\()"
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _tptp_quote(name: str) -> str:
|
|
233
|
+
"""Wrap ``name`` in TPTP single quotes, escaping exactly ``\\`` and ``'``.
|
|
234
|
+
|
|
235
|
+
The inverse of :func:`unicode_logic_kit.fol.tptp_input._functor_name`'s
|
|
236
|
+
unescaping (``re.sub(r"\\\\(.)", r"\\1", s[1:-1])``): escaping only these
|
|
237
|
+
two characters — backslash first, so a literal backslash in the name
|
|
238
|
+
never gets swallowed by the quote-escaping step that follows — produces
|
|
239
|
+
exactly the ``'(\\\\.|[^'\\\\])*'`` shape the grammar's ``SQ`` token
|
|
240
|
+
requires, so the result always re-parses to ``name`` unchanged.
|
|
241
|
+
"""
|
|
242
|
+
escaped = name.replace("\\", "\\\\").replace("'", "\\'")
|
|
243
|
+
return f"'{escaped}'"
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _find_name_candidates(text: str):
|
|
247
|
+
"""Distinct, order-preserving ``(span, name)`` pairs :data:`_CANDIDATE_RE`
|
|
248
|
+
finds in ``text`` that are NOT already valid unquoted TPTP names."""
|
|
249
|
+
seen = set()
|
|
250
|
+
out = []
|
|
251
|
+
for m in _CANDIDATE_RE.finditer(text):
|
|
252
|
+
name = m.group(1)
|
|
253
|
+
if _VALID_UNQUOTED.match(name):
|
|
254
|
+
continue
|
|
255
|
+
if name not in seen:
|
|
256
|
+
seen.add(name)
|
|
257
|
+
out.append((m.span(1), name))
|
|
258
|
+
return out
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _apply_name_quoting(text: str, candidates) -> str:
|
|
262
|
+
"""Quote every distinct candidate NAME wherever it occurs immediately
|
|
263
|
+
before an argument list, by name rather than by the single span
|
|
264
|
+
:func:`_find_name_candidates` returned for it — the same invalid name can
|
|
265
|
+
legitimately appear more than once (e.g. the same mis-written predicate
|
|
266
|
+
used on both sides of a formula), and substituting by name catches every
|
|
267
|
+
occurrence in one pass without having to track shifting offsets."""
|
|
268
|
+
out = text
|
|
269
|
+
for _span, name in candidates:
|
|
270
|
+
pattern = re.compile(re.escape(name) + r"(?=\s*\()")
|
|
271
|
+
out = pattern.sub(lambda m, n=name: _tptp_quote(n), out)
|
|
272
|
+
return out
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
# ---------------------------------------------------------------------------
|
|
276
|
+
# Canonical renderer — full bracketing (case 1) + correct quoting (case 2)
|
|
277
|
+
# ---------------------------------------------------------------------------
|
|
278
|
+
#
|
|
279
|
+
# Node.to_tptp() (see _fol_nodes.py) already fully brackets every connective
|
|
280
|
+
# and quantifier UNCONDITIONALLY — that is exactly case 1's fix, for free.
|
|
281
|
+
# What it does NOT do is quote a name that needs quoting (it just
|
|
282
|
+
# lower-cases and emits it bare), which would silently re-emit an invalid
|
|
283
|
+
# case-2 name. _render duplicates to_tptp()'s exact structural/operator
|
|
284
|
+
# choices (so behaviour is identical for everything that was already valid)
|
|
285
|
+
# and adds the one thing to_tptp() is missing: a quoting decision for every
|
|
286
|
+
# Atom/Function/Constant name.
|
|
287
|
+
|
|
288
|
+
def _needs_quote(name: str) -> bool:
|
|
289
|
+
return not bool(_VALID_UNQUOTED.match(name))
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _uncap(name: str) -> str:
|
|
293
|
+
"""Undo :mod:`tptp_input`'s ``Atom``-predicate ``_cap()``: that parser
|
|
294
|
+
force-uppercases the first character of EVERY parsed predicate name — its
|
|
295
|
+
"predicate names are capitalised" convention — whether the name came
|
|
296
|
+
from a bare ``LOWER`` token (whose first character the TPTP grammar
|
|
297
|
+
itself REQUIRES to already be lower-case) or an already single-quoted
|
|
298
|
+
``SQ`` token (arbitrary content); everything from the second character
|
|
299
|
+
on is untouched by ``_cap()`` either way. Recovering the true original
|
|
300
|
+
first character is needed here to decide (and correctly render) whether
|
|
301
|
+
a predicate name needs quoting; force-lowercasing it back is the exact
|
|
302
|
+
inverse whenever the true original was lower-case. That covers the
|
|
303
|
+
``LOWER``-token case UNCONDITIONALLY (the grammar leaves no other
|
|
304
|
+
possibility). The ``SQ``-quoted case is instead recovered exactly by
|
|
305
|
+
:func:`_find_prequoted_names` (a raw-text scan of the original quoted
|
|
306
|
+
spelling) BEFORE this function is ever consulted for such a name — see
|
|
307
|
+
:func:`_render_predicate_name`; this function is reached for an
|
|
308
|
+
``SQ``-quoted name only as the last-resort fallback when that scan
|
|
309
|
+
could not recover it unambiguously (see the module docstring for that
|
|
310
|
+
one residual case).
|
|
311
|
+
"""
|
|
312
|
+
return (name[:1].lower() + name[1:]) if name and name[0].isalpha() else name
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
_NO_KNOWN_NAMES: dict = {}
|
|
316
|
+
|
|
317
|
+
_SQ_TOKEN_RE = re.compile(r"'(?:\\.|[^'\\])*'")
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _find_prequoted_names(text: str) -> dict:
|
|
321
|
+
"""Best-effort ``_cap(original) -> original`` map recovered from every
|
|
322
|
+
single-quoted TPTP atom (``'...'``) literally present in ``text`` — i.e.
|
|
323
|
+
every name the CALLER already had quoted before this module ever touched
|
|
324
|
+
the text, as opposed to the case-2 ``known_names`` entries built in
|
|
325
|
+
:func:`repair_tptp_formula`, which cover names THIS module quoted itself.
|
|
326
|
+
|
|
327
|
+
This is a raw-text scan (:data:`_SQ_TOKEN_RE`, mirroring the grammar's
|
|
328
|
+
own ``SQ`` token), not a re-parse: it cannot tell a predicate-position
|
|
329
|
+
quoted name (``'Foo'(a)`` as an atom, or ``'Foo'`` as a 0-ary atom) from
|
|
330
|
+
a term-position one (``'Foo'`` used as a constant, or nested as a
|
|
331
|
+
function argument) — but that distinction never matters for correctness
|
|
332
|
+
here, because :func:`_render_functor_name` (``Function``/``Constant``)
|
|
333
|
+
never consults this map at all; only :func:`_render_predicate_name`
|
|
334
|
+
(``Atom.predicate``) does, and only for names ``_cap()`` has already
|
|
335
|
+
folded. Two DIFFERENT quoted originals sharing one ``_cap()`` image
|
|
336
|
+
(e.g. both ``'Foo'`` and ``'foo'`` literally present) are unrecoverably
|
|
337
|
+
ambiguous for that shared image from text alone — silently dropped from
|
|
338
|
+
the returned map (falls back to :func:`_uncap`'s pre-existing guess for
|
|
339
|
+
either predicate that ends up with that image, exactly today's
|
|
340
|
+
behaviour — never worse than before this scan existed) rather than
|
|
341
|
+
picking one arbitrarily.
|
|
342
|
+
"""
|
|
343
|
+
found: dict = {}
|
|
344
|
+
ambiguous: set = set()
|
|
345
|
+
for m in _SQ_TOKEN_RE.finditer(text):
|
|
346
|
+
original = _functor_name(m.group(0))
|
|
347
|
+
capped = _cap(original)
|
|
348
|
+
prior = found.get(capped)
|
|
349
|
+
if prior is not None and prior != original:
|
|
350
|
+
ambiguous.add(capped)
|
|
351
|
+
continue
|
|
352
|
+
found[capped] = original
|
|
353
|
+
for key in ambiguous:
|
|
354
|
+
found.pop(key, None)
|
|
355
|
+
return found
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _render_predicate_name(name: str, known_names: dict) -> str:
|
|
359
|
+
"""An ``Atom.predicate`` name, quoted iff it needs it.
|
|
360
|
+
|
|
361
|
+
``known_names`` maps ``_cap(original) -> original`` for every name this
|
|
362
|
+
module recovered the true original spelling of before ``_cap()`` folded
|
|
363
|
+
it: names THIS module's own case-2 raw-text repair just quoted (an
|
|
364
|
+
exact, ambiguity-free record — see :func:`repair_tptp_formula`), merged
|
|
365
|
+
with every name :func:`_find_prequoted_names` recovered from the raw
|
|
366
|
+
input text (a best-effort record for names the CALLER already had
|
|
367
|
+
quoted). When ``name`` (the stored, ``_cap()``'d predicate) is a hit in
|
|
368
|
+
either source, that recovered original is used; otherwise the
|
|
369
|
+
best-effort :func:`_uncap` guess is used (correct whenever the name
|
|
370
|
+
truly arrived as a bare unquoted functor — the realistic residual gap is
|
|
371
|
+
now limited to a quoted name whose distinct original spelling could not
|
|
372
|
+
be recovered from raw text at all, e.g. it collided ambiguously with
|
|
373
|
+
another quoted name sharing the same ``_cap()`` image — see the module
|
|
374
|
+
docstring).
|
|
375
|
+
"""
|
|
376
|
+
original = known_names[name] if name in known_names else _uncap(name)
|
|
377
|
+
return _tptp_quote(original) if _needs_quote(original) else original
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _render_functor_name(name: str) -> str:
|
|
381
|
+
"""A ``Function``/``Constant`` name — never touched by ``_cap()`` (only
|
|
382
|
+
``Atom.predicate`` is), so the stored name IS the original: quoted iff it
|
|
383
|
+
is not already a valid bare TPTP ``lower_word``, unchanged otherwise."""
|
|
384
|
+
return _tptp_quote(name) if _needs_quote(name) else name
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _render(node: Node, known_names: dict = _NO_KNOWN_NAMES) -> str:
|
|
388
|
+
"""Render ``node`` as canonical, gavel-compatible TPTP.
|
|
389
|
+
|
|
390
|
+
Structurally identical to :meth:`Node.to_tptp` for every connective,
|
|
391
|
+
quantifier, and already-valid name (same brackets, same operators, same
|
|
392
|
+
variable upper-casing) — the behavioural differences are: (1) a name
|
|
393
|
+
failing TPTP's unquoted ``lower_word`` rule is single-quoted instead of
|
|
394
|
+
emitted bare (case 2), via :func:`_render_predicate_name` /
|
|
395
|
+
:func:`_render_functor_name`; (2) an already-valid ``Atom`` predicate
|
|
396
|
+
name is emitted via ``known_names``/:func:`_uncap` (see
|
|
397
|
+
:func:`_render_predicate_name`) rather than :meth:`Node.to_tptp`'s
|
|
398
|
+
whole-string ``.lower()`` — the whole-string fold is lossy for a
|
|
399
|
+
mixed-case name like ``threeOxoSteroid`` (it would come back as
|
|
400
|
+
``threeoxosteroid``, a DIFFERENT ``LOWER`` token, breaking this module's
|
|
401
|
+
own case-1 meaning-preservation guarantee for exactly the camelCase
|
|
402
|
+
names ChEBI definitions use); touching only the character ``_cap()``
|
|
403
|
+
actually touched reparses to the identical predicate string. (3) The
|
|
404
|
+
truth constants ``$true`` and ``$false`` are TPTP's own propositions and are
|
|
405
|
+
written as they are, never quoted: a quoted ``'$false'`` is an ordinary atom to
|
|
406
|
+
Vampire, which then answers another question.
|
|
407
|
+
"""
|
|
408
|
+
if isinstance(node, Variable):
|
|
409
|
+
return node.name.upper()
|
|
410
|
+
if isinstance(node, Number):
|
|
411
|
+
return _number_text(node.value)
|
|
412
|
+
if isinstance(node, Constant):
|
|
413
|
+
return _render_functor_name(constant_name_to_ascii(node.name))
|
|
414
|
+
if isinstance(node, Function):
|
|
415
|
+
if node.name in Function.TPTP_ARITH_OPS:
|
|
416
|
+
args = ",".join(_render(a, known_names) for a in node.args)
|
|
417
|
+
return f"{Function.TPTP_ARITH_OPS[node.name]}({args})"
|
|
418
|
+
name = _render_functor_name(node.name)
|
|
419
|
+
if not node.args:
|
|
420
|
+
return name
|
|
421
|
+
args = ",".join(_render(a, known_names) for a in node.args)
|
|
422
|
+
return f"{name}({args})"
|
|
423
|
+
if isinstance(node, Atom):
|
|
424
|
+
if node.predicate in Atom.INFIX_PREDS_TPTP and len(node.args) == 2:
|
|
425
|
+
op = Atom.INFIX_PREDS_TPTP[node.predicate]
|
|
426
|
+
return f"({_render(node.args[0], known_names)} {op} {_render(node.args[1], known_names)})"
|
|
427
|
+
if node.predicate in Atom.PREFIX_PREDS_TPTP and len(node.args) == 2:
|
|
428
|
+
op = Atom.PREFIX_PREDS_TPTP[node.predicate]
|
|
429
|
+
return f"{op}({_render(node.args[0], known_names)},{_render(node.args[1], known_names)})"
|
|
430
|
+
truth = truth_constant_word(node)
|
|
431
|
+
if truth is not None:
|
|
432
|
+
return truth # TPTP's own propositions: a quoted '$true' is an ordinary atom
|
|
433
|
+
name = _render_predicate_name(node.predicate, known_names)
|
|
434
|
+
if not node.args:
|
|
435
|
+
return name
|
|
436
|
+
args = ",".join(_render(a, known_names) for a in node.args)
|
|
437
|
+
return f"{name}({args})"
|
|
438
|
+
if isinstance(node, Not):
|
|
439
|
+
return f"~({_render(node.formula, known_names)})"
|
|
440
|
+
if isinstance(node, And):
|
|
441
|
+
return f"({_render(node.left, known_names)} & {_render(node.right, known_names)})"
|
|
442
|
+
if isinstance(node, Or):
|
|
443
|
+
return f"({_render(node.left, known_names)} | {_render(node.right, known_names)})"
|
|
444
|
+
if isinstance(node, Xor):
|
|
445
|
+
return f"({_render(node.left, known_names)} <~> {_render(node.right, known_names)})"
|
|
446
|
+
if isinstance(node, Implies):
|
|
447
|
+
return f"({_render(node.left, known_names)} => {_render(node.right, known_names)})"
|
|
448
|
+
if isinstance(node, Iff):
|
|
449
|
+
return f"({_render(node.left, known_names)} <=> {_render(node.right, known_names)})"
|
|
450
|
+
if isinstance(node, Quantifier):
|
|
451
|
+
var = _render(node.variable, known_names)
|
|
452
|
+
body = _render(node.formula, known_names)
|
|
453
|
+
if node.type in ("forall", "∀"):
|
|
454
|
+
return f"(![{var}]: {body})"
|
|
455
|
+
if node.type in ("exists", "∃"):
|
|
456
|
+
return f"(?[{var}]: {body})"
|
|
457
|
+
raise ValueError(f"tptp_repair: unknown quantifier type {node.type!r}")
|
|
458
|
+
raise NotImplementedError(
|
|
459
|
+
f"tptp_repair: {type(node).__name__} is outside the fof/cnf fragment "
|
|
460
|
+
"parse_tptp_formula can produce (this module's scope, per "
|
|
461
|
+
"tptp_input.py's own docstring) — nothing built from TPTP text can "
|
|
462
|
+
"reach this branch; it would only fire on a hand-built AST using a "
|
|
463
|
+
"node type outside classical FOL.")
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
_WS_RE = re.compile(r"\s+")
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def _squash(text: str) -> str:
|
|
470
|
+
"""Whitespace-insensitive comparison key: TPTP tokens never depend on
|
|
471
|
+
inter-token whitespace to disambiguate (unlike e.g. Python), so this is a
|
|
472
|
+
safe basis for "is this really the same formula, modulo formatting"."""
|
|
473
|
+
return _WS_RE.sub("", text)
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
# ---------------------------------------------------------------------------
|
|
477
|
+
# Case 3 — free variables: report, or (opt-in) close
|
|
478
|
+
# ---------------------------------------------------------------------------
|
|
479
|
+
|
|
480
|
+
def _drop_argument_fix(formula: Node, var: Variable) -> Optional[Node]:
|
|
481
|
+
"""Case 3's argument-drop fix: ``P(X) <=> …`` / ``P(X) => …`` becomes
|
|
482
|
+
``P <=> …`` when ``X`` is the SOLE free variable, is the entire (single)
|
|
483
|
+
argument list of the left-hand definiendum, and does not recur on the
|
|
484
|
+
right — i.e. exactly when the formula is, in substance, already a 0-ary
|
|
485
|
+
molecule-level definition that only accidentally carries an argument.
|
|
486
|
+
Returns ``None`` (defer to universal closure) whenever this precise shape
|
|
487
|
+
does not hold — this function never guesses.
|
|
488
|
+
"""
|
|
489
|
+
if not isinstance(formula, (Iff, Implies)):
|
|
490
|
+
return None
|
|
491
|
+
left = formula.left
|
|
492
|
+
if not (isinstance(left, Atom) and len(left.args) == 1 and left.args[0] == var):
|
|
493
|
+
return None
|
|
494
|
+
if var in free_variables(formula.right):
|
|
495
|
+
return None
|
|
496
|
+
cls = type(formula)
|
|
497
|
+
return cls(Atom(left.predicate, []), formula.right)
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def _close_universally(formula: Node, variables) -> Node:
|
|
501
|
+
"""Standard universal closure: wrap ``formula`` in ``∀v`` for every free
|
|
502
|
+
``v``, outermost quantifier first in alphabetical order (order among
|
|
503
|
+
universal quantifiers never changes truth, so this is purely for
|
|
504
|
+
deterministic, readable output)."""
|
|
505
|
+
result = formula
|
|
506
|
+
for var in sorted(variables, key=lambda v: v.name, reverse=True):
|
|
507
|
+
result = Quantifier("∀", var, result)
|
|
508
|
+
return result
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
# ---------------------------------------------------------------------------
|
|
512
|
+
# Public API
|
|
513
|
+
# ---------------------------------------------------------------------------
|
|
514
|
+
|
|
515
|
+
def repair_tptp_formula(text: str, *, quote_invalid_names: bool = True,
|
|
516
|
+
close_free_variables: bool = False) -> RepairResult:
|
|
517
|
+
"""Repair one bare TPTP FOF/CNF formula (no ``fof(...)`` wrapper).
|
|
518
|
+
|
|
519
|
+
Pipeline:
|
|
520
|
+
|
|
521
|
+
1. Try :func:`parse_tptp_formula` directly. If ``text`` fails to parse
|
|
522
|
+
AND ``quote_invalid_names`` is true, look for case-2 candidates (see
|
|
523
|
+
:data:`_CANDIDATE_RE`) and retry on the quoted text. Neither
|
|
524
|
+
candidate found nor the retry succeeding is honest failure:
|
|
525
|
+
``ok=False``, ``repaired_text`` left as the untouched input, a single
|
|
526
|
+
``"syntax_error"`` issue carrying the parser's own message. If two
|
|
527
|
+
DISTINCT candidates would reduce to the same predicate once quoted
|
|
528
|
+
and re-parsed (see the module docstring's ``"name_collision"``
|
|
529
|
+
paragraph), that is honest failure too — ``ok=False`` rather than
|
|
530
|
+
silently merging them.
|
|
531
|
+
2. Free variables (case 3) are read off the resulting AST via
|
|
532
|
+
:func:`~unicode_logic_kit.fol.nodes.free_variables`. Reported via a
|
|
533
|
+
``"free_variable"`` issue always; additionally CLOSED (via
|
|
534
|
+
:func:`_drop_argument_fix` if that exact shape applies, else
|
|
535
|
+
:func:`_close_universally`) only when ``close_free_variables=True``.
|
|
536
|
+
3. ``repaired_text`` is :func:`_render` of the (possibly closed) AST —
|
|
537
|
+
always fully bracketed (case 1, for free: ``Node.to_tptp`` already
|
|
538
|
+
brackets unconditionally and :func:`_render` matches it exactly) and
|
|
539
|
+
with every name that needs it quoted (case 2, render-time half: a
|
|
540
|
+
name that arrived ALREADY quoted in ``text`` — recovered by
|
|
541
|
+
:func:`_find_prequoted_names`, independently of whether it still
|
|
542
|
+
needs quoting on output for the same reason it arrived quoted, e.g.
|
|
543
|
+
an internal hyphen, or for a different reason entirely, e.g. it
|
|
544
|
+
merely starts upper-case — is preserved quoted, with its exact
|
|
545
|
+
original spelling, rather than accidentally emitted bare or
|
|
546
|
+
mis-cased).
|
|
547
|
+
|
|
548
|
+
**Case-1 meaning-preservation argument** (why ``repaired_text``
|
|
549
|
+
reparses to a formula structurally ``==`` the one parsed from ``text``,
|
|
550
|
+
not just logically equivalent to it): the grammar's operator ladder is
|
|
551
|
+
``equiv > imp > disj > conj > unary`` (``<=>``/``<~>`` loosest, ``&``
|
|
552
|
+
tightest among binary connectives), applied consistently whether or not
|
|
553
|
+
the input carries redundant parentheses — parentheses only ever
|
|
554
|
+
OVERRIDE precedence, they never change how already-unambiguous text
|
|
555
|
+
parses. So ``p <=> a & b`` and ``p <=> (a & b)`` walk the identical
|
|
556
|
+
production sequence (``equiv`` → two ``imp``s, the right one reducing
|
|
557
|
+
through ``conj: conj "&" unary``) and build the identical tree,
|
|
558
|
+
``Iff(P, And(A, B))``, in either surface form. ``repaired_text`` is
|
|
559
|
+
:func:`_render` of the AST parsed from ``text`` — by the identity above,
|
|
560
|
+
reparsing it is guaranteed to retrace the same production sequence and
|
|
561
|
+
rebuild the same tree, not merely an equivalent one. See
|
|
562
|
+
``test_bracket_normalisation_preserves_ast`` for the executable form of
|
|
563
|
+
this argument (AST identity, plus an independent
|
|
564
|
+
:func:`unicode_logic_kit.equivalent` check as a second witness).
|
|
565
|
+
|
|
566
|
+
Args:
|
|
567
|
+
text: a bare TPTP formula (as accepted by ``parse_tptp_formula``, or
|
|
568
|
+
one violating unquoted-name syntax in the specific ways
|
|
569
|
+
:data:`_CANDIDATE_RE` targets).
|
|
570
|
+
quote_invalid_names: attempt the case-2 raw-text quoting fallback
|
|
571
|
+
when direct parsing fails. ``False`` disables ONLY this
|
|
572
|
+
fallback attempt (unparseable input then reports ``ok=False``
|
|
573
|
+
immediately) — it never suppresses the render-time quoting an
|
|
574
|
+
already-parseable formula's own names may still need, since
|
|
575
|
+
that is required for ``repaired_text`` to be valid TPTP at all.
|
|
576
|
+
close_free_variables: apply case 3's fix (see step 2) instead of
|
|
577
|
+
only reporting it. Default ``False``: report, never touch — an
|
|
578
|
+
unclosed free variable never blocks ``ok=True``.
|
|
579
|
+
|
|
580
|
+
Returns:
|
|
581
|
+
A :class:`RepairResult`.
|
|
582
|
+
"""
|
|
583
|
+
issues = []
|
|
584
|
+
working_text = text
|
|
585
|
+
formula = None
|
|
586
|
+
known_names = {} # _cap(original) -> original, for names THIS call quoted itself
|
|
587
|
+
|
|
588
|
+
try:
|
|
589
|
+
formula = parse_tptp_formula(working_text)
|
|
590
|
+
except ParsingError as exc:
|
|
591
|
+
if quote_invalid_names:
|
|
592
|
+
candidates = _find_name_candidates(text)
|
|
593
|
+
if candidates:
|
|
594
|
+
retried_text = _apply_name_quoting(text, candidates)
|
|
595
|
+
try:
|
|
596
|
+
formula = parse_tptp_formula(retried_text)
|
|
597
|
+
except ParsingError as exc2:
|
|
598
|
+
return RepairResult(
|
|
599
|
+
ok=False, formula=None, repaired_text=text,
|
|
600
|
+
issues=(Issue(
|
|
601
|
+
"syntax_error",
|
|
602
|
+
f"quoting {sorted(n for _s, n in candidates)!r} did not "
|
|
603
|
+
f"make the formula parseable: {exc2}",
|
|
604
|
+
),),
|
|
605
|
+
changed=False)
|
|
606
|
+
working_text = retried_text
|
|
607
|
+
for _span, name in candidates:
|
|
608
|
+
capped = _cap(name)
|
|
609
|
+
prior = known_names.get(capped)
|
|
610
|
+
if prior is not None and prior != name:
|
|
611
|
+
# Two DISTINCT case-2 candidates (e.g. "N-acetyl-x"
|
|
612
|
+
# and "n-acetyl-x") that differ only in the case of
|
|
613
|
+
# their first letter both reduce to the same
|
|
614
|
+
# tptp_input _cap() image and would already reparse
|
|
615
|
+
# to the IDENTICAL Atom predicate — silently keeping
|
|
616
|
+
# only one of them in known_names would merge two
|
|
617
|
+
# distinct predicates into one without any warning.
|
|
618
|
+
# Refuse rather than choose (see the module
|
|
619
|
+
# docstring's "refuse rather than silently
|
|
620
|
+
# reinterpret" line).
|
|
621
|
+
return RepairResult(
|
|
622
|
+
ok=False, formula=None, repaired_text=text,
|
|
623
|
+
issues=(Issue(
|
|
624
|
+
"name_collision",
|
|
625
|
+
f"case-2 quoting {name!r} and {prior!r} both "
|
|
626
|
+
f"reduce to the same predicate name {capped!r} "
|
|
627
|
+
"once tptp_input capitalises the first "
|
|
628
|
+
"character of every parsed Atom predicate "
|
|
629
|
+
"(_cap) — quoting both would silently merge "
|
|
630
|
+
"two distinct predicates into one; refusing "
|
|
631
|
+
"rather than picking one arbitrarily.",
|
|
632
|
+
),),
|
|
633
|
+
changed=False)
|
|
634
|
+
known_names[capped] = name
|
|
635
|
+
issues.append(Issue(
|
|
636
|
+
"invalid_predicate_name",
|
|
637
|
+
f"{name!r} is not a valid unquoted TPTP name "
|
|
638
|
+
"(fails lower_word: [a-z][A-Za-z0-9_]*); single-quoted "
|
|
639
|
+
"so the name is preserved exactly.",
|
|
640
|
+
suggestion=_tptp_quote(name)))
|
|
641
|
+
if formula is None:
|
|
642
|
+
return RepairResult(
|
|
643
|
+
ok=False, formula=None, repaired_text=text,
|
|
644
|
+
issues=(Issue("syntax_error", str(exc)),), changed=False)
|
|
645
|
+
else:
|
|
646
|
+
return RepairResult(
|
|
647
|
+
ok=False, formula=None, repaired_text=text,
|
|
648
|
+
issues=(Issue("syntax_error", str(exc)),), changed=False)
|
|
649
|
+
|
|
650
|
+
# Independent of the case-2 path above: recover the exact spelling of
|
|
651
|
+
# every name the CALLER already had quoted in the raw input (Finding-2
|
|
652
|
+
# fix — see _find_prequoted_names), so _render never has to fall back to
|
|
653
|
+
# _uncap's lossy guess for a name this module never itself quoted.
|
|
654
|
+
# setdefault: the case-2 known_names entries above are exact/authoritative
|
|
655
|
+
# (built from text this module itself controlled the quoting of) and
|
|
656
|
+
# always take precedence over this best-effort text scan.
|
|
657
|
+
for capped, original in _find_prequoted_names(text).items():
|
|
658
|
+
known_names.setdefault(capped, original)
|
|
659
|
+
|
|
660
|
+
canonical_before_close = _render(formula, known_names)
|
|
661
|
+
if _squash(working_text) != _squash(canonical_before_close):
|
|
662
|
+
issues.append(Issue(
|
|
663
|
+
"bracket_normalisation",
|
|
664
|
+
"the input parsed only via operator precedence; brackets made "
|
|
665
|
+
"explicit (meaning-preserving — see repair_tptp_formula's "
|
|
666
|
+
"docstring for the argument).",
|
|
667
|
+
suggestion=canonical_before_close))
|
|
668
|
+
|
|
669
|
+
free = free_variables(formula)
|
|
670
|
+
final_formula = formula
|
|
671
|
+
if free:
|
|
672
|
+
names = ", ".join(sorted(v.name for v in free))
|
|
673
|
+
if close_free_variables:
|
|
674
|
+
dropped = _drop_argument_fix(formula, next(iter(free))) if len(free) == 1 else None
|
|
675
|
+
if dropped is not None:
|
|
676
|
+
final_formula = dropped
|
|
677
|
+
issues.append(Issue(
|
|
678
|
+
"free_variable",
|
|
679
|
+
f"unbound variable {names!r} is the sole argument of the "
|
|
680
|
+
"left-hand definiendum and does not recur on the right — "
|
|
681
|
+
"dropped it: the definition is, in substance, a 0-ary "
|
|
682
|
+
"property that only accidentally carries an argument.",
|
|
683
|
+
suggestion=_render(dropped, known_names)))
|
|
684
|
+
else:
|
|
685
|
+
final_formula = _close_universally(formula, free)
|
|
686
|
+
issues.append(Issue(
|
|
687
|
+
"free_variable",
|
|
688
|
+
f"unbound variable(s) {names} closed via universal "
|
|
689
|
+
"quantification (standard universal-closure convention; "
|
|
690
|
+
"the argument-drop fix does not apply — the "
|
|
691
|
+
"formula is not a `predicate(X) <=> …`/`predicate(X) => …` "
|
|
692
|
+
"definition with X absent from the right-hand side).",
|
|
693
|
+
suggestion=_render(final_formula, known_names)))
|
|
694
|
+
else:
|
|
695
|
+
issues.append(Issue(
|
|
696
|
+
"free_variable",
|
|
697
|
+
f"unbound variable(s) {names} — not closed (default "
|
|
698
|
+
"close_free_variables=False). The kit will not guess whether "
|
|
699
|
+
"you meant universal closure or an argument that should be "
|
|
700
|
+
"dropped; pass close_free_variables=True to apply the "
|
|
701
|
+
"documented conservative fix, or rewrite by hand."))
|
|
702
|
+
|
|
703
|
+
repaired_text = _render(final_formula, known_names)
|
|
704
|
+
changed = _squash(text) != _squash(repaired_text)
|
|
705
|
+
return RepairResult(ok=True, formula=final_formula, repaired_text=repaired_text,
|
|
706
|
+
issues=tuple(issues), changed=changed)
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
# ---------------------------------------------------------------------------
|
|
710
|
+
# Whole-problem repair
|
|
711
|
+
# ---------------------------------------------------------------------------
|
|
712
|
+
|
|
713
|
+
@dataclass(frozen=True)
|
|
714
|
+
class _Statement:
|
|
715
|
+
name: str
|
|
716
|
+
role: str
|
|
717
|
+
formula_text: str
|
|
718
|
+
formula_span: Tuple[int, int]
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
def _skip_ws_and_comments(text: str, i: int) -> int:
|
|
722
|
+
n = len(text)
|
|
723
|
+
while i < n:
|
|
724
|
+
c = text[i]
|
|
725
|
+
if c.isspace():
|
|
726
|
+
i += 1
|
|
727
|
+
elif c == "%":
|
|
728
|
+
while i < n and text[i] not in "\r\n":
|
|
729
|
+
i += 1
|
|
730
|
+
elif text[i:i + 2] == "/*":
|
|
731
|
+
end = text.find("*/", i + 2)
|
|
732
|
+
i = (end + 2) if end != -1 else n
|
|
733
|
+
else:
|
|
734
|
+
break
|
|
735
|
+
return i
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
def _read_ident(text: str, i: int, what: str) -> Tuple[str, int]:
|
|
739
|
+
n = len(text)
|
|
740
|
+
start = i
|
|
741
|
+
while i < n and (text[i].isalnum() or text[i] == "_"):
|
|
742
|
+
i += 1
|
|
743
|
+
if i == start:
|
|
744
|
+
raise TptpRepairError(
|
|
745
|
+
f"tptp_repair: expected {what} at position {start} in TPTP problem text")
|
|
746
|
+
return text[start:i], i
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
def _expect(text: str, i: int, ch: str, what: str) -> int:
|
|
750
|
+
if i >= len(text) or text[i] != ch:
|
|
751
|
+
raise TptpRepairError(
|
|
752
|
+
f"tptp_repair: expected {ch!r} ({what}) at position {i} in TPTP problem text")
|
|
753
|
+
return i + 1
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
def _depth_zero_stop(text: str, i: int, n: int, stop_at_comma: bool = False) -> int:
|
|
757
|
+
"""Index of the first ``)`` (or, with ``stop_at_comma``, ``,``) at bracket
|
|
758
|
+
depth 0 from ``i``, skipping ``'...'`` atoms, ``"..."`` strings and
|
|
759
|
+
comments; ``n`` if there is none. ``(`` and ``[`` both open a level."""
|
|
760
|
+
depth = 0
|
|
761
|
+
m = i
|
|
762
|
+
while m < n:
|
|
763
|
+
c = text[m]
|
|
764
|
+
if c in ("'", '"'):
|
|
765
|
+
m += 1
|
|
766
|
+
while m < n and text[m] != c:
|
|
767
|
+
m += 2 if text[m] == "\\" else 1
|
|
768
|
+
m += 1
|
|
769
|
+
continue
|
|
770
|
+
if c == "%":
|
|
771
|
+
while m < n and text[m] not in "\r\n":
|
|
772
|
+
m += 1
|
|
773
|
+
continue
|
|
774
|
+
if text[m:m + 2] == "/*":
|
|
775
|
+
end = text.find("*/", m + 2)
|
|
776
|
+
m = (end + 2) if end != -1 else n
|
|
777
|
+
continue
|
|
778
|
+
if c in "([":
|
|
779
|
+
depth += 1
|
|
780
|
+
elif c in ")]":
|
|
781
|
+
if depth == 0:
|
|
782
|
+
return m if c == ")" else n
|
|
783
|
+
depth -= 1
|
|
784
|
+
elif c == "," and depth == 0 and stop_at_comma:
|
|
785
|
+
return m
|
|
786
|
+
m += 1
|
|
787
|
+
return n
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
def _split_tptp_statements(text: str) -> Tuple[_Statement, ...]:
|
|
791
|
+
"""Locate every ``fof(name, role, FORMULA).`` / ``cnf(...)`` statement's
|
|
792
|
+
exact ``FORMULA`` text span, without requiring the formula itself to be
|
|
793
|
+
parseable — the whole point is to hand each formula to
|
|
794
|
+
:func:`repair_tptp_formula` individually, so one broken statement must
|
|
795
|
+
never prevent locating the others.
|
|
796
|
+
|
|
797
|
+
A hand-rolled character scanner (not the Lark grammar) because the
|
|
798
|
+
grammar's own ``file`` rule parses formulas inline: a syntax error in
|
|
799
|
+
ANY one statement fails the whole file, which is exactly what per-formula
|
|
800
|
+
repair needs to route around. Tracks ``'...'`` quoted atoms (backslash
|
|
801
|
+
escapes) and ``%``/``/* */`` comments so parens/quotes inside them never
|
|
802
|
+
perturb paren-depth tracking; a statement's formula ends at the first
|
|
803
|
+
``,`` or ``)`` seen at depth 0 (both ``()`` and ``[]`` count, so the comma
|
|
804
|
+
in ``![X,Y]`` stays inside), and the statement at the ``)`` closing it,
|
|
805
|
+
which must be followed (after whitespace/comments) by ``.``. Whatever sits
|
|
806
|
+
between a formula's ``,`` and that ``)`` — the optional source and
|
|
807
|
+
useful-info annotations — is left untouched. An ``include(...)``
|
|
808
|
+
directive holds no formula and is skipped the same way.
|
|
809
|
+
"""
|
|
810
|
+
n = len(text)
|
|
811
|
+
i = 0
|
|
812
|
+
out = []
|
|
813
|
+
while True:
|
|
814
|
+
i = _skip_ws_and_comments(text, i)
|
|
815
|
+
if i >= n:
|
|
816
|
+
break
|
|
817
|
+
keyword, i = _read_ident(text, i, "'fof', 'cnf' or 'include'")
|
|
818
|
+
if keyword == "include":
|
|
819
|
+
i = _skip_ws_and_comments(text, i)
|
|
820
|
+
i = _expect(text, i, "(", "include opening paren")
|
|
821
|
+
end = _depth_zero_stop(text, i, n)
|
|
822
|
+
if end >= n or text[end] != ")":
|
|
823
|
+
raise TptpRepairError(
|
|
824
|
+
"tptp_repair: an include directive has unbalanced parentheses")
|
|
825
|
+
after = _skip_ws_and_comments(text, end + 1)
|
|
826
|
+
i = _expect(text, after, ".", "include terminator")
|
|
827
|
+
continue
|
|
828
|
+
if keyword not in ("fof", "cnf"):
|
|
829
|
+
raise TptpRepairError(
|
|
830
|
+
f"tptp_repair: unsupported TPTP statement keyword {keyword!r} "
|
|
831
|
+
"(only fof, cnf and include are supported)")
|
|
832
|
+
i = _skip_ws_and_comments(text, i)
|
|
833
|
+
i = _expect(text, i, "(", "statement opening paren")
|
|
834
|
+
i = _skip_ws_and_comments(text, i)
|
|
835
|
+
name, i = _read_ident(text, i, "statement name")
|
|
836
|
+
i = _skip_ws_and_comments(text, i)
|
|
837
|
+
i = _expect(text, i, ",", "name/role separator")
|
|
838
|
+
i = _skip_ws_and_comments(text, i)
|
|
839
|
+
role, i = _read_ident(text, i, "statement role")
|
|
840
|
+
i = _skip_ws_and_comments(text, i)
|
|
841
|
+
i = _expect(text, i, ",", "role/formula separator")
|
|
842
|
+
i = _skip_ws_and_comments(text, i)
|
|
843
|
+
formula_start = i
|
|
844
|
+
|
|
845
|
+
m = _depth_zero_stop(text, i, n, stop_at_comma=True)
|
|
846
|
+
if m >= n:
|
|
847
|
+
raise TptpRepairError(
|
|
848
|
+
f"tptp_repair: statement {name!r} has unbalanced parentheses "
|
|
849
|
+
"(no closing ')' found for its formula)")
|
|
850
|
+
formula_end = m
|
|
851
|
+
if text[m] == ",":
|
|
852
|
+
m = _depth_zero_stop(text, m + 1, n)
|
|
853
|
+
if m >= n or text[m] != ")":
|
|
854
|
+
raise TptpRepairError(
|
|
855
|
+
f"tptp_repair: statement {name!r} has unbalanced parentheses "
|
|
856
|
+
"(no closing ')' found after its annotations)")
|
|
857
|
+
after = _skip_ws_and_comments(text, m + 1)
|
|
858
|
+
after = _expect(text, after, ".", "statement terminator")
|
|
859
|
+
|
|
860
|
+
out.append(_Statement(name=name, role=role,
|
|
861
|
+
formula_text=text[formula_start:formula_end],
|
|
862
|
+
formula_span=(formula_start, formula_end)))
|
|
863
|
+
i = after
|
|
864
|
+
return tuple(out)
|
|
865
|
+
|
|
866
|
+
|
|
867
|
+
@dataclass(frozen=True)
|
|
868
|
+
class ProblemRepairEntry:
|
|
869
|
+
"""One statement's repair, with its ``fof(...)`` name/role preserved."""
|
|
870
|
+
|
|
871
|
+
name: str
|
|
872
|
+
role: str
|
|
873
|
+
result: RepairResult
|
|
874
|
+
|
|
875
|
+
def to_dict(self) -> dict:
|
|
876
|
+
return {"name": self.name, "role": self.role, "result": self.result.to_dict()}
|
|
877
|
+
|
|
878
|
+
|
|
879
|
+
@dataclass(frozen=True)
|
|
880
|
+
class ProblemRepairResult:
|
|
881
|
+
"""Whole-file repair: every statement's :class:`ProblemRepairEntry`, plus
|
|
882
|
+
the reassembled file text (each statement's ``fof(``/``, name, role,``/
|
|
883
|
+
``).`` wrapper and every byte between statements — comments included —
|
|
884
|
+
kept byte-for-byte; only each formula's own span is replaced by its
|
|
885
|
+
``RepairResult.repaired_text``, and only when that repair succeeded).
|
|
886
|
+
"""
|
|
887
|
+
|
|
888
|
+
entries: Tuple[ProblemRepairEntry, ...]
|
|
889
|
+
ok: bool
|
|
890
|
+
repaired_text: str
|
|
891
|
+
changed: bool
|
|
892
|
+
|
|
893
|
+
def to_dict(self) -> dict:
|
|
894
|
+
return {
|
|
895
|
+
"entries": [e.to_dict() for e in self.entries],
|
|
896
|
+
"ok": self.ok,
|
|
897
|
+
"repaired_text": self.repaired_text,
|
|
898
|
+
"changed": self.changed,
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
def repair_tptp_problem(text: str, *, quote_invalid_names: bool = True,
|
|
903
|
+
close_free_variables: bool = False) -> ProblemRepairResult:
|
|
904
|
+
""":func:`repair_tptp_formula`, applied statement-by-statement to a whole
|
|
905
|
+
TPTP problem file (one or more ``fof(name, role, formula).`` /
|
|
906
|
+
``cnf(...)`` statements).
|
|
907
|
+
|
|
908
|
+
Each statement is repaired independently (see :func:`_split_tptp_statements`
|
|
909
|
+
for why a whole-file Lark parse cannot be used here: one broken formula
|
|
910
|
+
must not block the rest), and only its formula span is rewritten in the
|
|
911
|
+
reassembled ``repaired_text`` — the wrapper, name, role, and every
|
|
912
|
+
comment/blank line survive untouched. ``ok`` is the conjunction of every
|
|
913
|
+
per-statement ``RepairResult.ok``; ``changed`` the disjunction.
|
|
914
|
+
|
|
915
|
+
Raises:
|
|
916
|
+
TptpRepairError: the file's statement STRUCTURE (not a formula's
|
|
917
|
+
content) is malformed — an unrecognised keyword, a missing
|
|
918
|
+
``,``/``.``, or unbalanced parentheses that prevent even locating
|
|
919
|
+
where a formula ends. Distinct from a per-formula syntax issue,
|
|
920
|
+
which is reported per-entry instead.
|
|
921
|
+
"""
|
|
922
|
+
statements = _split_tptp_statements(text)
|
|
923
|
+
entries = []
|
|
924
|
+
pieces = []
|
|
925
|
+
cursor = 0
|
|
926
|
+
all_ok = True
|
|
927
|
+
any_changed = False
|
|
928
|
+
for stmt in statements:
|
|
929
|
+
result = repair_tptp_formula(
|
|
930
|
+
stmt.formula_text, quote_invalid_names=quote_invalid_names,
|
|
931
|
+
close_free_variables=close_free_variables)
|
|
932
|
+
entries.append(ProblemRepairEntry(stmt.name, stmt.role, result))
|
|
933
|
+
all_ok = all_ok and result.ok
|
|
934
|
+
any_changed = any_changed or result.changed
|
|
935
|
+
start, end = stmt.formula_span
|
|
936
|
+
pieces.append(text[cursor:start])
|
|
937
|
+
pieces.append(result.repaired_text)
|
|
938
|
+
cursor = end
|
|
939
|
+
pieces.append(text[cursor:])
|
|
940
|
+
return ProblemRepairResult(entries=tuple(entries), ok=all_ok,
|
|
941
|
+
repaired_text="".join(pieces), changed=any_changed)
|