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,1503 @@
|
|
|
1
|
+
"""TPTP input: read TPTP ``fof`` / ``cnf`` formulas and whole problem files into the AST.
|
|
2
|
+
|
|
3
|
+
This is the inverse of :meth:`Node.to_tptp`. TPTP has a genuinely different surface
|
|
4
|
+
syntax from the toolkit's Unicode notation (the ``fof(name, role, formula).`` wrapper,
|
|
5
|
+
``! [X] : φ`` / ``? [X] : φ`` quantifiers, uppercase variables, lowercase
|
|
6
|
+
constants/functions/predicates, ``& | ~ => <=> <~>`` connectives, ``$less``/``$sum``
|
|
7
|
+
dollar-words), so it gets its own Lark grammar rather than the glyph-translation used
|
|
8
|
+
for LaTeX import.
|
|
9
|
+
|
|
10
|
+
Public API:
|
|
11
|
+
|
|
12
|
+
- :func:`parse_tptp_formula` — parse a single bare FOF/CNF formula into a :class:`Node`.
|
|
13
|
+
- :func:`parse_tptp` — parse a whole TPTP problem (one or more ``fof``/``cnf``
|
|
14
|
+
statements) into a list of :class:`TptpFormula` ``(name, role, formula)`` records.
|
|
15
|
+
- :func:`load_tptp` — read a ``.p`` / ``.tptp`` file and :func:`parse_tptp` it.
|
|
16
|
+
|
|
17
|
+
Naming conventions (the exact inverse of :meth:`Node.to_tptp`): a TPTP **variable**
|
|
18
|
+
(uppercase) becomes a lowercase :class:`Variable`; a **predicate** (lowercase in TPTP)
|
|
19
|
+
is capitalised to match the toolkit's uppercase-predicate convention, so ``loves`` ↦
|
|
20
|
+
``Loves`` and ``p`` ↦ ``P``; **constants** and **functions** stay lowercase; ``=`` /
|
|
21
|
+
``!=`` map to the ``=`` / ``≠`` atoms; the comparison dollar-words ``$less`` /
|
|
22
|
+
``$greater`` / ``$lesseq`` / ``$greatereq`` map to the ``<`` / ``>`` / ``≤`` / ``≥``
|
|
23
|
+
atoms; and the arithmetic dollar-words ``$sum`` / ``$difference`` / ``$product`` /
|
|
24
|
+
``$quotient`` map to the ``+`` / ``-`` / ``*`` / ``/`` functions. ``$true`` / ``$false``
|
|
25
|
+
are imported as the nullary atoms ``$true`` / ``$false`` (the toolkit has no
|
|
26
|
+
boolean-constant node), which every route that decides or evaluates a formula reads
|
|
27
|
+
as TPTP's defined propositions — true and false — and the TPTP writers write back
|
|
28
|
+
verbatim.
|
|
29
|
+
|
|
30
|
+
A TPTP variable is case-sensitive and the kit variable is its lower-cased form, so
|
|
31
|
+
two variables that differ only by letter case (``Xa``, ``XA``) would become one
|
|
32
|
+
kit variable. Where that changes the formula — one is captured by the other's
|
|
33
|
+
quantifier, or both occur free in one formula — the reader REFUSES it by name,
|
|
34
|
+
naming both variables (``![Xa, XA]: p(Xa, XA)`` is not read as ``∀xa ∀xa
|
|
35
|
+
P(xa, xa)``); where each is bound by a quantifier of its own and they never meet
|
|
36
|
+
(``(![Xa]: p(Xa)) & (![XA]: q(XA))``) the reading is the same formula and is
|
|
37
|
+
unchanged. No renaming is invented: rename one of the two.
|
|
38
|
+
|
|
39
|
+
Single-quoted atoms (``'http___example_org_Thing'``) — the form OWL→FOL translators
|
|
40
|
+
emit for IRIs — are accepted as functor / predicate / constant names: the quotes are
|
|
41
|
+
stripped and the ``\\'`` / ``\\\\`` escapes unescaped. The resulting names may contain
|
|
42
|
+
characters that are not legal MSFLParser tokens, so call
|
|
43
|
+
:func:`unicode_logic_kit.fol.sanitize_names` before re-parsing the rendered Unicode.
|
|
44
|
+
|
|
45
|
+
Scope: the first-order ``fof`` fragment, ``cnf`` clauses, and TF0 (monomorphic
|
|
46
|
+
typed first-order) ``tff`` — see :func:`parse_tff_problem` below for the typed
|
|
47
|
+
half. ``include('path')`` / ``include('path', [name, ...])`` directives are
|
|
48
|
+
resolved by every file/load entry point (see "Includes" below); the optional
|
|
49
|
+
4th (``source``)/5th (``useful_info``) annotation fields of a statement parse
|
|
50
|
+
and are discarded. THF (higher-order TPTP), TF1 polymorphism (type
|
|
51
|
+
variables, ``!>``), and TPTP's built-in arithmetic sorts (``$int``/``$rat``/
|
|
52
|
+
``$real``) remain out of scope — each is refused LOUDLY, naming the
|
|
53
|
+
construct, rather than silently narrowed. THF and TF1 are refused BEFORE the
|
|
54
|
+
first-order grammar is tried, by the statement's kind and by the binder, so the
|
|
55
|
+
refusal does not depend on the rest of the text parsing: a problem with a ``thf(...)``
|
|
56
|
+
statement (a type declaration, an application ``p @ A``, whatever its body) is a
|
|
57
|
+
:class:`TptpParsingError` naming THF, and a text with the type binder ``!>`` or a
|
|
58
|
+
quantifier variable of type ``$tType`` (``![A: $tType]``) is an error naming TF1
|
|
59
|
+
polymorphism that is a :class:`TptpParsingError` and also a
|
|
60
|
+
:class:`NotImplementedError`. Comments and single-quoted atoms are taken out first,
|
|
61
|
+
so a ``thf(`` or a ``!>`` inside one is no statement and no binder. The arithmetic sorts
|
|
62
|
+
are refused by :class:`NotImplementedError` where a declaration or a quantifier names
|
|
63
|
+
one (see :func:`parse_tff_problem`'s docstring for exactly which constructs raise
|
|
64
|
+
where).
|
|
65
|
+
|
|
66
|
+
Includes
|
|
67
|
+
--------
|
|
68
|
+
:func:`parse_tptp`, :func:`parse_tptp_problem` and :func:`parse_tff_problem`
|
|
69
|
+
take an optional ``base_dir`` (the directory an ``include(...)`` inside
|
|
70
|
+
``text`` resolves relative to first) and ``search_paths`` (further roots
|
|
71
|
+
tried next, in order — the TPTP-library convention: ``include(
|
|
72
|
+
'Axioms/SET001+0.ax')`` is root-relative, not relative to whatever file
|
|
73
|
+
referred to it). ``os.environ["TPTP"]``, if set, is ALSO tried as a root,
|
|
74
|
+
after ``search_paths`` — the same convention external TPTP tooling uses to
|
|
75
|
+
locate the library. ``base_dir=None`` (the default) means "no including
|
|
76
|
+
file's directory is known": an ``include`` then raises
|
|
77
|
+
:class:`TptpParsingError` naming the directive rather than guessing, which
|
|
78
|
+
keeps every existing bare-text caller — :func:`parse_tptp_formula` included,
|
|
79
|
+
which never resolves includes at all, since a single bare formula has no
|
|
80
|
+
"including file" — byte-identical by default.
|
|
81
|
+
|
|
82
|
+
:func:`load_tptp`, :func:`load_tptp_problem` and :func:`load_tff_problem`
|
|
83
|
+
set ``base_dir`` to the loaded file's own directory automatically (so a
|
|
84
|
+
sibling include resolves with no extra argument) and seed cycle detection
|
|
85
|
+
with the loaded file's own path, so a chain that loops back to the very
|
|
86
|
+
file you started from is caught, not only a cycle confined to files reached
|
|
87
|
+
purely via nested includes. A selection list (``include('path', [name1,
|
|
88
|
+
name2])``) imports only those formulas, matched by their own TPTP statement
|
|
89
|
+
name, and refuses a name absent from the included file; a missing include
|
|
90
|
+
file names the path and every root tried; a circular include chain names
|
|
91
|
+
the whole chain. A selection list on a file that also declares TFF
|
|
92
|
+
vocabulary (a ``tff(name, type, ...).`` statement) is refused — see
|
|
93
|
+
:func:`parse_tff_problem`'s docstring.
|
|
94
|
+
|
|
95
|
+
TF0 (``tff``) reading, in brief
|
|
96
|
+
--------------------------------
|
|
97
|
+
:func:`parse_tptp` / :func:`parse_tptp_formula` / :func:`load_tptp` now also
|
|
98
|
+
accept ``tff(name, axiom|conjecture|..., <formula>).`` statements (typed
|
|
99
|
+
quantifiers ``! [X: sort] : φ`` / ``? [X: sort] : φ`` build
|
|
100
|
+
:class:`~unicode_logic_kit.fol.nodes.SortedQuantifier`; an untyped ``! [X] : φ``
|
|
101
|
+
still builds a plain :class:`~unicode_logic_kit.fol.nodes.Quantifier`, exactly
|
|
102
|
+
as before — this is purely additive). A ``tff(name, type, ...).`` TYPE
|
|
103
|
+
DECLARATION, however, has no ``TptpFormula`` shape to return (it declares
|
|
104
|
+
vocabulary, not a formula), so :func:`parse_tptp` refuses a problem
|
|
105
|
+
containing one, naming :func:`parse_tff_problem` as the function that reads
|
|
106
|
+
it: that function returns the declared
|
|
107
|
+
:class:`~unicode_logic_kit.fol.signature.Signature` ALONGSIDE the formulas,
|
|
108
|
+
and additionally promotes a plain :class:`~unicode_logic_kit.fol.nodes.Constant`
|
|
109
|
+
occurrence to a :class:`~unicode_logic_kit.fol.nodes.SortedConstant` wherever
|
|
110
|
+
that name was declared with a concrete (non-``$i``) sort — recovering the
|
|
111
|
+
same AST shape :func:`~unicode_logic_kit.atp.tptp_tff.generate_tff_problem`
|
|
112
|
+
started from, since a TFF formula BODY carries no inline sort annotation for
|
|
113
|
+
a constant occurrence (only a bound variable does; a constant's sort lives
|
|
114
|
+
solely in its separate ``type`` declaration).
|
|
115
|
+
|
|
116
|
+
Two parsers, LALR first and Earley as a fallback
|
|
117
|
+
------------------------------------------------
|
|
118
|
+
The grammar below is parsed by FOUR lark parsers: an LALR(1) pair (``file``
|
|
119
|
+
and ``formula`` start symbols) that is tried first, and the original Earley
|
|
120
|
+
pair that takes over whenever LALR raises ``UnexpectedInput``. The reason is
|
|
121
|
+
measured, not stylistic: on a real 1,367,212-byte, 4291-formula TPTP
|
|
122
|
+
translation of an ontology, ``load_tptp_problem`` took 68.3 s through Earley
|
|
123
|
+
and 1.7 s through LALR — and the two produce BYTE-IDENTICAL item lists for
|
|
124
|
+
all 4291 records. The cost is per formula (4.15 ms against 0.2 ms), not
|
|
125
|
+
superlinear in file size, and a ``cProfile`` run attributes 98 % of it to
|
|
126
|
+
lark's dynamic-lexer Earley chart, so this is the only lever that matters.
|
|
127
|
+
|
|
128
|
+
The fallback is what makes it risk-free, and only the PARSE step falls back:
|
|
129
|
+
if LALR refuses a text, Earley re-parses it and Earley raises, so no input
|
|
130
|
+
that parsed before stops parsing and every syntax-error message stays
|
|
131
|
+
byte-identical. The TRANSFORM step is never retried — the TF0-scope refusals
|
|
132
|
+
(``NotImplementedError`` for TF1/``$int``/``$rat``/``$real``, the
|
|
133
|
+
``ConflictingArityError`` path) come out of the shared transformer and are
|
|
134
|
+
identical either way, so a retry would only double the work.
|
|
135
|
+
|
|
136
|
+
The asymmetry that remains: LALR uses lark's contextual lexer and Earley the
|
|
137
|
+
dynamic one, so in principle LALR could ACCEPT a text Earley rejects, which
|
|
138
|
+
the fallback does not protect against. Every terminal here is disjoint by
|
|
139
|
+
first character (``VAR`` ``[A-Z]``, ``LOWER`` ``[a-z]``, ``SQ`` ``'``,
|
|
140
|
+
``DOLLARWORD`` ``$``, ``NUMBER`` a digit or ``-``) and every operator is a
|
|
141
|
+
literal string resolved by longest match, so a divergence would need a
|
|
142
|
+
GRAMMAR change to introduce. ``tests/test_tptp_input.py`` therefore keeps a
|
|
143
|
+
permanent parametrised agreement battery over both parsers, plus an explicit
|
|
144
|
+
assertion that the grammar still builds as LALR(1) — so a future grammar edit
|
|
145
|
+
that breaks the equivalence goes red instead of silently falling back to
|
|
146
|
+
Earley for every parse.
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
import os
|
|
150
|
+
import re
|
|
151
|
+
from dataclasses import dataclass, field
|
|
152
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
153
|
+
|
|
154
|
+
from lark import Lark, Transformer, Tree
|
|
155
|
+
from lark.exceptions import UnexpectedInput, VisitError
|
|
156
|
+
|
|
157
|
+
from ._fol_nodes import _numeral_from_text
|
|
158
|
+
from .nodes import (
|
|
159
|
+
Node, Variable, Constant, Number, Function,
|
|
160
|
+
Atom, Not, And, Or, Xor, Implies, Iff, Quantifier,
|
|
161
|
+
SortedQuantifier, SortedConstant,
|
|
162
|
+
)
|
|
163
|
+
from .naming import ParsingError
|
|
164
|
+
from .signature import Signature, PredicateDecl, FunctionDecl, ConstantDecl
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class TptpParsingError(ParsingError):
|
|
168
|
+
"""A TPTP import failure, carrying a plain message.
|
|
169
|
+
|
|
170
|
+
Subclasses :class:`ParsingError` (so ``except ParsingError`` still catches it)
|
|
171
|
+
but takes a string instead of a Lark exception — the same pattern
|
|
172
|
+
:class:`ConflictingArityError` uses.
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
def __init__(self, message: str):
|
|
176
|
+
self.args = (message,)
|
|
177
|
+
|
|
178
|
+
def __str__(self):
|
|
179
|
+
return self.args[0]
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class _Tf1PolymorphismError(TptpParsingError, NotImplementedError):
|
|
183
|
+
"""TF1 polymorphism (a type variable), refused by name.
|
|
184
|
+
|
|
185
|
+
A :class:`TptpParsingError`, like every other refusal of a text this reader does not
|
|
186
|
+
read, and also a :class:`NotImplementedError`, which is what the refusal of a TF1
|
|
187
|
+
type declaration always was: a caller that catches either keeps catching it.
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
_THF_REFUSAL = (
|
|
192
|
+
"SYNTAX_ERROR: THF (higher-order TPTP) is out of scope for "
|
|
193
|
+
"this reader; only fof, cnf, and tff (TF0, monomorphic) are "
|
|
194
|
+
"supported."
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
_TF1_REFUSAL = (
|
|
198
|
+
"TF0 reader: TF1 polymorphic types and type variables ('!> [...] : ...', "
|
|
199
|
+
"'![A: $tType] : ...') are out of scope for this monomorphic TF0-only reader."
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# Dollar-word predicate / function dictionaries — the inverse of the
|
|
204
|
+
# PREFIX_PREDS_TPTP / TPTP_ARITH_OPS tables in _fol_nodes.py.
|
|
205
|
+
_DOLLAR_PRED = {"$less": "<", "$greater": ">", "$lesseq": "≤", "$greatereq": "≥"}
|
|
206
|
+
_DOLLAR_FUNC = {"$sum": "+", "$difference": "-", "$product": "*", "$quotient": "/"}
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@dataclass(frozen=True)
|
|
210
|
+
class TptpFormula:
|
|
211
|
+
"""One annotated TPTP statement: its ``name``, ``role``, and parsed ``formula``."""
|
|
212
|
+
|
|
213
|
+
name: str
|
|
214
|
+
role: str
|
|
215
|
+
formula: Node
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
@dataclass(frozen=True)
|
|
219
|
+
class _IncludeDirective:
|
|
220
|
+
"""One parsed ``include('path').`` / ``include('path', [n1, n2]).`` directive.
|
|
221
|
+
|
|
222
|
+
Internal only: ``file()`` (see :class:`_TptpTransformer`) returns these
|
|
223
|
+
interleaved with :class:`TptpFormula` / TF0 declaration records in
|
|
224
|
+
source order, and :func:`_resolve_includes` splices each one, in place,
|
|
225
|
+
with the (recursively resolved) statements of the file it names, before
|
|
226
|
+
any public function ever sees one. ``selection`` is ``None`` when no
|
|
227
|
+
``[...]`` name list was given (import everything from the included
|
|
228
|
+
file), else the tuple of names to import selectively.
|
|
229
|
+
"""
|
|
230
|
+
|
|
231
|
+
file_name: str
|
|
232
|
+
selection: Optional[Tuple[str, ...]]
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
_GRAMMAR = r"""
|
|
236
|
+
file: (stmt | include_stmt)+
|
|
237
|
+
stmt: LOWER "(" fof_name "," LOWER "," formula ["," annotation_term ["," annotation_term]] ")" "."
|
|
238
|
+
| LOWER "(" fof_name "," LOWER "," tff_type_decl ["," annotation_term ["," annotation_term]] ")" "." -> stmt_type
|
|
239
|
+
fof_name: LOWER | NUMBER
|
|
240
|
+
|
|
241
|
+
// --- include directives: "include('path')." / "include('path', [n1,n2])." ---
|
|
242
|
+
// Resolved by _resolve_includes AFTER transforming (splicing in the named
|
|
243
|
+
// file's own statements), never during parsing -- see that function and the
|
|
244
|
+
// module docstring's "Includes" section.
|
|
245
|
+
include_stmt: "include" "(" (LOWER | SQ) ["," "[" name_list "]"] ")" "."
|
|
246
|
+
name_list: fof_name ("," fof_name)*
|
|
247
|
+
|
|
248
|
+
// --- the optional 4th (source) / 5th (useful_info) annotation fields ---
|
|
249
|
+
// Accepted syntactically and discarded -- see _TptpTransformer.stmt /
|
|
250
|
+
// .stmt_type. Not TPTP's full "general_term" grammar (no THF-shaped terms,
|
|
251
|
+
// no arithmetic expressions), just enough to admit the shapes real TPTP
|
|
252
|
+
// files use for these two fields: a functor application (`file(...)`,
|
|
253
|
+
// `inference(...)`), a bracketed list of either, or a bare atom/variable/
|
|
254
|
+
// number/quoted string.
|
|
255
|
+
annotation_term: LOWER "(" annotation_term_list ")"
|
|
256
|
+
| "[" [annotation_term_list] "]"
|
|
257
|
+
| LOWER
|
|
258
|
+
| SQ
|
|
259
|
+
| VAR
|
|
260
|
+
| NUMBER
|
|
261
|
+
annotation_term_list: annotation_term ("," annotation_term)*
|
|
262
|
+
|
|
263
|
+
?formula: equiv
|
|
264
|
+
?equiv: imp
|
|
265
|
+
| imp "<=>" imp -> iff
|
|
266
|
+
| imp "<~>" imp -> xor
|
|
267
|
+
?imp: disj
|
|
268
|
+
| disj "=>" imp -> implies
|
|
269
|
+
| disj "<=" imp -> rev_implies
|
|
270
|
+
?disj: conj
|
|
271
|
+
| disj "|" conj -> or_
|
|
272
|
+
| disj "~|" conj -> nor
|
|
273
|
+
?conj: unary
|
|
274
|
+
| conj "&" unary -> and_
|
|
275
|
+
| conj "~&" unary -> nand
|
|
276
|
+
?unary: "~" unary -> neg
|
|
277
|
+
| "!" "[" varlist "]" ":" unary -> forall
|
|
278
|
+
| "?" "[" varlist "]" ":" unary -> exists
|
|
279
|
+
| "(" formula ")"
|
|
280
|
+
| atom
|
|
281
|
+
|
|
282
|
+
// A TFF quantifier's variable may carry an explicit ":" sort. A plain,
|
|
283
|
+
// untyped "! [X] : ..." still round-trips (typed_var's optional sort is
|
|
284
|
+
// simply absent) -- shared unmodified by fof/cnf too, see module docstring.
|
|
285
|
+
varlist: typed_var ("," typed_var)*
|
|
286
|
+
typed_var: VAR (":" tff_atomic_type)?
|
|
287
|
+
|
|
288
|
+
?atom: term "=" term -> equality
|
|
289
|
+
| term "!=" term -> disequality
|
|
290
|
+
| DOLLARWORD "(" termlist ")" -> dollar_atom
|
|
291
|
+
| LOWER "(" termlist ")" -> pred_app
|
|
292
|
+
| SQ "(" termlist ")" -> pred_app
|
|
293
|
+
| DOLLARWORD -> bool_const
|
|
294
|
+
| LOWER -> prop_atom
|
|
295
|
+
| SQ -> prop_atom
|
|
296
|
+
|
|
297
|
+
?term: VAR -> var
|
|
298
|
+
| DOLLARWORD "(" termlist ")" -> dollar_func_app
|
|
299
|
+
| LOWER "(" termlist ")" -> func_app
|
|
300
|
+
| SQ "(" termlist ")" -> func_app
|
|
301
|
+
| LOWER -> constant
|
|
302
|
+
| SQ -> constant
|
|
303
|
+
| NUMBER -> number
|
|
304
|
+
|
|
305
|
+
termlist: term ("," term)*
|
|
306
|
+
|
|
307
|
+
// --- TF0 type declarations: "tff(name, type, <tff_type_decl>)." ---
|
|
308
|
+
// (TF1 polymorphism and THF are NOT extensions of this: tff_poly_decl below
|
|
309
|
+
// always raises, and "thf(" is refused at the statement-keyword check —
|
|
310
|
+
// see the module docstring and _TptpTransformer.stmt_type/tff_poly_decl.)
|
|
311
|
+
tff_type_decl: (LOWER | SQ) ":" tff_top_type
|
|
312
|
+
|
|
313
|
+
?tff_top_type: DOLLAR_TTYPE -> tff_sort_decl
|
|
314
|
+
| "!>" "[" tyvarlist "]" ":" tff_poly_monotype -> tff_poly_decl
|
|
315
|
+
| tff_mapping_type -> tff_symbol_decl
|
|
316
|
+
| tff_atomic_type -> tff_nullary_decl
|
|
317
|
+
|
|
318
|
+
tyvarlist: VAR ("," VAR)*
|
|
319
|
+
|
|
320
|
+
// The TF1 polymorphic body ("!> [A] : (A > A)" and similar) is parsed only
|
|
321
|
+
// so tff_poly_decl below can raise a NAMED refusal instead of a generic
|
|
322
|
+
// parse failure -- a type VARIABLE (VAR) is otherwise never a legal atomic
|
|
323
|
+
// type (see tff_atomic_type), so this sub-grammar is kept separate rather
|
|
324
|
+
// than widening the ordinary (monomorphic) one. Never given transformer
|
|
325
|
+
// methods: tff_poly_decl discards its children unconditionally.
|
|
326
|
+
?tff_poly_monotype: tff_poly_atomic_type
|
|
327
|
+
| "(" tff_poly_mapping_type ")"
|
|
328
|
+
tff_poly_mapping_type: tff_poly_domain ">" tff_poly_atomic_type
|
|
329
|
+
tff_poly_domain: tff_poly_atomic_type
|
|
330
|
+
| "(" tff_poly_xprod ")"
|
|
331
|
+
tff_poly_xprod: tff_poly_atomic_type ("*" tff_poly_atomic_type)+
|
|
332
|
+
tff_poly_atomic_type: LOWER | SQ | DOLLARWORD | VAR
|
|
333
|
+
|
|
334
|
+
tff_mapping_type: tff_domain ">" tff_atomic_type
|
|
335
|
+
?tff_domain: tff_atomic_type -> domain_single
|
|
336
|
+
| "(" tff_xprod ")" -> domain_prod
|
|
337
|
+
tff_xprod: tff_atomic_type ("*" tff_atomic_type)+
|
|
338
|
+
|
|
339
|
+
tff_atomic_type: LOWER -> plain_type
|
|
340
|
+
| SQ -> plain_type
|
|
341
|
+
| DOLLARWORD -> dollar_type
|
|
342
|
+
|
|
343
|
+
VAR: /[A-Z][A-Za-z0-9_]*/
|
|
344
|
+
LOWER: /[a-z][A-Za-z0-9_]*/
|
|
345
|
+
SQ: /'(\\.|[^'\\])*'/
|
|
346
|
+
DOLLARWORD: /\$[a-z_]+/
|
|
347
|
+
DOLLAR_TTYPE.2: "$tType"
|
|
348
|
+
NUMBER: /-?[0-9]+(\.[0-9]+)?/
|
|
349
|
+
|
|
350
|
+
%import common.WS
|
|
351
|
+
%ignore WS
|
|
352
|
+
%ignore /%[^\r\n]*/
|
|
353
|
+
%ignore /\/\*(.|\n)*?\*\//
|
|
354
|
+
"""
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _cap(name: str) -> str:
|
|
358
|
+
"""Capitalise the first letter (invert to_tptp's predicate lower-casing)."""
|
|
359
|
+
return name[:1].upper() + name[1:] if name else name
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _functor_name(token) -> str:
|
|
363
|
+
"""Return the bare functor/constant name from a LOWER or single-quoted token.
|
|
364
|
+
|
|
365
|
+
A TPTP **single-quoted atom** ``'…'`` is an arbitrary functor name (the form
|
|
366
|
+
OWL→FOL dumps use for IRIs, e.g. ``'http___example_org_Thing'``). The
|
|
367
|
+
surrounding quotes are stripped and the TPTP escapes ``\\'`` / ``\\\\`` are
|
|
368
|
+
unescaped; a plain LOWER token is returned unchanged. The resulting name may
|
|
369
|
+
contain characters (underscores, etc.) that are not legal MSFLParser tokens —
|
|
370
|
+
use :func:`unicode_logic_kit.fol.sanitize_names` before re-parsing the rendered
|
|
371
|
+
Unicode.
|
|
372
|
+
"""
|
|
373
|
+
s = str(token)
|
|
374
|
+
if len(s) >= 2 and s[0] == "'" and s[-1] == "'":
|
|
375
|
+
return re.sub(r"\\(.)", r"\1", s[1:-1])
|
|
376
|
+
return s
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
# =============================================================================
|
|
380
|
+
# TF0 type declarations: parsed shapes and atomic-type resolution
|
|
381
|
+
# =============================================================================
|
|
382
|
+
|
|
383
|
+
_ARITHMETIC_SORTS = frozenset({"$int", "$rat", "$real"})
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
@dataclass(frozen=True)
|
|
387
|
+
class _TffSortDecl:
|
|
388
|
+
"""A parsed ``tff(name, type, S: $tType).`` sort declaration."""
|
|
389
|
+
|
|
390
|
+
name: str
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
@dataclass(frozen=True)
|
|
394
|
+
class _TffPredDecl:
|
|
395
|
+
"""A parsed predicate type declaration: name, and each argument's
|
|
396
|
+
resolved sort (``None`` for ``$i``, else a capitalised kit-level sort
|
|
397
|
+
name -- see :func:`_resolve_type_str`)."""
|
|
398
|
+
|
|
399
|
+
name: str
|
|
400
|
+
arg_sorts: Tuple[Optional[str], ...]
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
@dataclass(frozen=True)
|
|
404
|
+
class _TffFuncDecl:
|
|
405
|
+
"""A parsed function/constant type declaration (``arg_sorts == ()``
|
|
406
|
+
means a 0-ary function, i.e. a constant, in :func:`parse_tff_problem`'s
|
|
407
|
+
Signature assembly)."""
|
|
408
|
+
|
|
409
|
+
name: str
|
|
410
|
+
arg_sorts: Tuple[Optional[str], ...]
|
|
411
|
+
result_sort: Optional[str]
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def _resolve_type_str(type_str: str, *, context: str) -> Optional[str]:
|
|
415
|
+
"""Return the kit-level sort name for one TFF atomic-type string, or
|
|
416
|
+
``None`` for ``"$i"`` (TPTP's default individual type -- "no sort
|
|
417
|
+
annotation needed", matching a plain unsorted :class:`Quantifier` /
|
|
418
|
+
:class:`Constant`). ``context`` (e.g. ``"a quantified variable's"``,
|
|
419
|
+
``"an argument of 'p''s"``) is spliced into the error message naming
|
|
420
|
+
WHERE the offending type was used.
|
|
421
|
+
|
|
422
|
+
Raises:
|
|
423
|
+
TptpParsingError: the type is ``"$o"`` (boolean may only appear as
|
|
424
|
+
a PREDICATE's own overall result type -- never a variable's,
|
|
425
|
+
argument's, or function-result type) or an unrecognised
|
|
426
|
+
``"$..."`` built-in.
|
|
427
|
+
NotImplementedError: the type is ``"$int"``/``"$rat"``/``"$real"``
|
|
428
|
+
-- TPTP's arithmetic sorts, out of scope for this TF0-only
|
|
429
|
+
reader (see module docstring).
|
|
430
|
+
"""
|
|
431
|
+
if type_str == "$i":
|
|
432
|
+
return None
|
|
433
|
+
if type_str == "$o":
|
|
434
|
+
raise TptpParsingError(
|
|
435
|
+
f"SYNTAX_ERROR: '$o' (boolean) cannot be {context} type -- only "
|
|
436
|
+
"a predicate's own overall result may be $o."
|
|
437
|
+
)
|
|
438
|
+
if type_str in _ARITHMETIC_SORTS:
|
|
439
|
+
raise NotImplementedError(
|
|
440
|
+
f"TF0 reader: {context} type {type_str!r} needs TPTP's "
|
|
441
|
+
"arithmetic sorts ($int/$rat/$real), which are out of scope for "
|
|
442
|
+
"this TF0-only reader (see module docstring 'Scope')."
|
|
443
|
+
)
|
|
444
|
+
if type_str.startswith("$"):
|
|
445
|
+
raise TptpParsingError(
|
|
446
|
+
f"SYNTAX_ERROR: unsupported TPTP built-in type {type_str!r} as "
|
|
447
|
+
f"{context} type."
|
|
448
|
+
)
|
|
449
|
+
return _cap(type_str)
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
class _TptpTransformer(Transformer):
|
|
453
|
+
"""Turn the Lark parse tree into the toolkit AST."""
|
|
454
|
+
|
|
455
|
+
# --- whole-file / statements ---
|
|
456
|
+
def file(self, items):
|
|
457
|
+
return list(items)
|
|
458
|
+
|
|
459
|
+
def stmt(self, items):
|
|
460
|
+
keyword = str(items[0])
|
|
461
|
+
if keyword == "thf":
|
|
462
|
+
raise TptpParsingError(_THF_REFUSAL)
|
|
463
|
+
if keyword not in ("fof", "cnf", "tff"):
|
|
464
|
+
raise TptpParsingError(
|
|
465
|
+
f"SYNTAX_ERROR: unsupported TPTP statement '{keyword}' "
|
|
466
|
+
"(only fof, cnf, and tff are supported)."
|
|
467
|
+
)
|
|
468
|
+
name, role, formula = str(items[1]), str(items[2]), items[3]
|
|
469
|
+
# items[4] / items[5]: the optional 4th (source) / 5th (useful_info)
|
|
470
|
+
# annotation fields (None when absent -- see the grammar's
|
|
471
|
+
# annotation_term rule). This reader models the statement's own
|
|
472
|
+
# FORMULA, never its provenance/derivation metadata -- accepted
|
|
473
|
+
# syntactically, discarded here.
|
|
474
|
+
return TptpFormula(name, role, formula)
|
|
475
|
+
|
|
476
|
+
def stmt_type(self, items):
|
|
477
|
+
"""A ``tff(name, type, <decl>).`` type-declaration statement.
|
|
478
|
+
|
|
479
|
+
Returns one of :class:`_TffSortDecl` / :class:`_TffPredDecl` /
|
|
480
|
+
:class:`_TffFuncDecl` — never a :class:`TptpFormula`, since a type
|
|
481
|
+
declaration declares vocabulary, not a formula. Only
|
|
482
|
+
:func:`parse_tff_problem` consumes these; :func:`parse_tptp` refuses
|
|
483
|
+
a problem containing one (see that function). ``items[4]``/
|
|
484
|
+
``items[5]`` (the optional annotation fields) are discarded exactly
|
|
485
|
+
like :meth:`stmt` discards them.
|
|
486
|
+
"""
|
|
487
|
+
keyword, name, role, decl = str(items[0]), str(items[1]), str(items[2]), items[3]
|
|
488
|
+
if keyword == "thf":
|
|
489
|
+
raise TptpParsingError(_THF_REFUSAL)
|
|
490
|
+
if keyword != "tff":
|
|
491
|
+
raise TptpParsingError(
|
|
492
|
+
f"SYNTAX_ERROR: a type declaration (role 'type') is only "
|
|
493
|
+
f"valid inside a 'tff(...)' statement, got '{keyword}(...)'."
|
|
494
|
+
)
|
|
495
|
+
if role != "type":
|
|
496
|
+
raise TptpParsingError(
|
|
497
|
+
f"SYNTAX_ERROR: expected role 'type' for a TFF type "
|
|
498
|
+
f"declaration, got {role!r}."
|
|
499
|
+
)
|
|
500
|
+
return decl
|
|
501
|
+
|
|
502
|
+
def fof_name(self, items):
|
|
503
|
+
return str(items[0])
|
|
504
|
+
|
|
505
|
+
# --- include directives (spliced in by _resolve_includes, post-parse) ---
|
|
506
|
+
def include_stmt(self, items):
|
|
507
|
+
file_tok, names = items
|
|
508
|
+
selection = tuple(names) if names is not None else None
|
|
509
|
+
return _IncludeDirective(_functor_name(file_tok), selection)
|
|
510
|
+
|
|
511
|
+
def name_list(self, items):
|
|
512
|
+
return list(items)
|
|
513
|
+
|
|
514
|
+
# --- connectives ---
|
|
515
|
+
def iff(self, items):
|
|
516
|
+
return Iff(items[0], items[1])
|
|
517
|
+
|
|
518
|
+
def xor(self, items):
|
|
519
|
+
return Xor(items[0], items[1])
|
|
520
|
+
|
|
521
|
+
def implies(self, items):
|
|
522
|
+
return Implies(items[0], items[1])
|
|
523
|
+
|
|
524
|
+
def rev_implies(self, items):
|
|
525
|
+
# TPTP a <= b means "a if b", i.e. b => a.
|
|
526
|
+
return Implies(items[1], items[0])
|
|
527
|
+
|
|
528
|
+
def or_(self, items):
|
|
529
|
+
return Or(items[0], items[1])
|
|
530
|
+
|
|
531
|
+
def nor(self, items):
|
|
532
|
+
return Not(Or(items[0], items[1]))
|
|
533
|
+
|
|
534
|
+
def and_(self, items):
|
|
535
|
+
return And(items[0], items[1])
|
|
536
|
+
|
|
537
|
+
def nand(self, items):
|
|
538
|
+
return Not(And(items[0], items[1]))
|
|
539
|
+
|
|
540
|
+
def neg(self, items):
|
|
541
|
+
return Not(items[0])
|
|
542
|
+
|
|
543
|
+
# --- quantifiers ---
|
|
544
|
+
def forall(self, items):
|
|
545
|
+
return self._quantify("∀", items[0], items[1])
|
|
546
|
+
|
|
547
|
+
def exists(self, items):
|
|
548
|
+
return self._quantify("∃", items[0], items[1])
|
|
549
|
+
|
|
550
|
+
def _quantify(self, qtype, variables, body):
|
|
551
|
+
# Each element of `variables` is either a plain Variable (untyped,
|
|
552
|
+
# from a bare typed_var with no ":sort") or a (Variable, sort) pair
|
|
553
|
+
# (typed_var's optional sort was present) -- see typed_var below.
|
|
554
|
+
for var in reversed(variables):
|
|
555
|
+
if isinstance(var, tuple):
|
|
556
|
+
bound, sort = var
|
|
557
|
+
body = SortedQuantifier(qtype, bound, sort, body)
|
|
558
|
+
else:
|
|
559
|
+
body = Quantifier(qtype, var, body)
|
|
560
|
+
return body
|
|
561
|
+
|
|
562
|
+
def varlist(self, items):
|
|
563
|
+
return list(items)
|
|
564
|
+
|
|
565
|
+
def typed_var(self, items):
|
|
566
|
+
"""One quantifier variable, optionally TFF-typed: ``X`` or ``X: sort``.
|
|
567
|
+
|
|
568
|
+
Returns a bare :class:`Variable` (untyped -- matches fof/cnf's
|
|
569
|
+
pre-existing behaviour exactly) when no ``: sort`` was given, or a
|
|
570
|
+
``(Variable, sort)`` pair when one was -- :meth:`_quantify` turns the
|
|
571
|
+
pair into a :class:`SortedQuantifier` binder. ``$i`` resolves to
|
|
572
|
+
"untyped" (a bare :class:`Variable`), matching TPTP's own "no type
|
|
573
|
+
given" default; ``$o``/arithmetic sorts/``$tType`` are refused (see
|
|
574
|
+
:func:`_resolve_type_str`).
|
|
575
|
+
"""
|
|
576
|
+
if len(items) == 1:
|
|
577
|
+
return Variable(str(items[0]).lower())
|
|
578
|
+
var_tok, type_str = items
|
|
579
|
+
sort = _resolve_type_str(type_str, context="a quantified variable's")
|
|
580
|
+
var = Variable(str(var_tok).lower())
|
|
581
|
+
return var if sort is None else (var, sort)
|
|
582
|
+
|
|
583
|
+
# --- atoms ---
|
|
584
|
+
def equality(self, items):
|
|
585
|
+
return Atom("=", [items[0], items[1]])
|
|
586
|
+
|
|
587
|
+
def disequality(self, items):
|
|
588
|
+
return Atom("≠", [items[0], items[1]])
|
|
589
|
+
|
|
590
|
+
def pred_app(self, items):
|
|
591
|
+
return Atom(_cap(_functor_name(items[0])), items[1])
|
|
592
|
+
|
|
593
|
+
def prop_atom(self, items):
|
|
594
|
+
return Atom(_cap(_functor_name(items[0])), [])
|
|
595
|
+
|
|
596
|
+
def dollar_atom(self, items):
|
|
597
|
+
word = str(items[0])
|
|
598
|
+
if word in _DOLLAR_PRED:
|
|
599
|
+
return Atom(_DOLLAR_PRED[word], items[1])
|
|
600
|
+
raise TptpParsingError(
|
|
601
|
+
f"SYNTAX_ERROR: unsupported TPTP dollar-word predicate '{word}'."
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
def bool_const(self, items):
|
|
605
|
+
word = str(items[0])
|
|
606
|
+
if word in ("$true", "$false"):
|
|
607
|
+
return Atom(word, [])
|
|
608
|
+
raise TptpParsingError(
|
|
609
|
+
f"SYNTAX_ERROR: unsupported TPTP dollar-word '{word}'."
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
# --- terms ---
|
|
613
|
+
def var(self, items):
|
|
614
|
+
return Variable(str(items[0]).lower())
|
|
615
|
+
|
|
616
|
+
def constant(self, items):
|
|
617
|
+
return Constant(_functor_name(items[0]))
|
|
618
|
+
|
|
619
|
+
def number(self, items):
|
|
620
|
+
return Number(_numeral_from_text(str(items[0])))
|
|
621
|
+
|
|
622
|
+
def func_app(self, items):
|
|
623
|
+
return Function(_functor_name(items[0]), items[1])
|
|
624
|
+
|
|
625
|
+
def dollar_func_app(self, items):
|
|
626
|
+
word = str(items[0])
|
|
627
|
+
if word in _DOLLAR_FUNC:
|
|
628
|
+
return Function(_DOLLAR_FUNC[word], items[1])
|
|
629
|
+
raise TptpParsingError(
|
|
630
|
+
f"SYNTAX_ERROR: unsupported TPTP dollar-word function '{word}'."
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
def termlist(self, items):
|
|
634
|
+
return list(items)
|
|
635
|
+
|
|
636
|
+
# --- TF0 type declarations ---
|
|
637
|
+
def tff_type_decl(self, items):
|
|
638
|
+
name_tok, shape_payload = items
|
|
639
|
+
name = _functor_name(name_tok)
|
|
640
|
+
shape, payload = shape_payload
|
|
641
|
+
if shape == "sort":
|
|
642
|
+
return _TffSortDecl(_cap(name))
|
|
643
|
+
if shape == "nullary":
|
|
644
|
+
if payload == "$o":
|
|
645
|
+
return _TffPredDecl(_cap(name), ())
|
|
646
|
+
return _TffFuncDecl(name, (), _resolve_type_str(payload, context="this symbol's"))
|
|
647
|
+
if shape == "mapping":
|
|
648
|
+
domain, result = payload
|
|
649
|
+
arg_sorts = tuple(
|
|
650
|
+
_resolve_type_str(t, context=f"an argument of {name!r}'s") for t in domain)
|
|
651
|
+
if result == "$o":
|
|
652
|
+
return _TffPredDecl(_cap(name), arg_sorts)
|
|
653
|
+
return _TffFuncDecl(
|
|
654
|
+
name, arg_sorts, _resolve_type_str(result, context=f"{name!r}'s result"))
|
|
655
|
+
raise AssertionError(f"tff_type_decl: unreachable shape {shape!r}") # pragma: no cover
|
|
656
|
+
|
|
657
|
+
def tff_sort_decl(self, items):
|
|
658
|
+
return ("sort", None)
|
|
659
|
+
|
|
660
|
+
def tff_poly_decl(self, items):
|
|
661
|
+
raise _Tf1PolymorphismError(_TF1_REFUSAL)
|
|
662
|
+
|
|
663
|
+
def tff_symbol_decl(self, items):
|
|
664
|
+
return ("mapping", items[0])
|
|
665
|
+
|
|
666
|
+
def tff_nullary_decl(self, items):
|
|
667
|
+
return ("nullary", items[0])
|
|
668
|
+
|
|
669
|
+
def tff_mapping_type(self, items):
|
|
670
|
+
domain, result = items
|
|
671
|
+
return (domain, result)
|
|
672
|
+
|
|
673
|
+
def domain_single(self, items):
|
|
674
|
+
return (items[0],)
|
|
675
|
+
|
|
676
|
+
def domain_prod(self, items):
|
|
677
|
+
return items[0]
|
|
678
|
+
|
|
679
|
+
def tff_xprod(self, items):
|
|
680
|
+
return tuple(items)
|
|
681
|
+
|
|
682
|
+
def plain_type(self, items):
|
|
683
|
+
return _functor_name(items[0])
|
|
684
|
+
|
|
685
|
+
def dollar_type(self, items):
|
|
686
|
+
return str(items[0])
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
_FORMULA_PARSER = Lark(_GRAMMAR, start="formula", parser="earley")
|
|
690
|
+
_FILE_PARSER = Lark(_GRAMMAR, start="file", parser="earley")
|
|
691
|
+
# The LALR(1) twins, tried first -- see "Two parsers" in the module
|
|
692
|
+
# docstring. Construction costs +0.06 s at import for the pair (measured),
|
|
693
|
+
# against 45x on every non-trivial parse. lark raises GrammarError on any
|
|
694
|
+
# shift/reduce or reduce/reduce collision, so these two lines are themselves
|
|
695
|
+
# the assertion that the grammar above is LALR(1).
|
|
696
|
+
_FORMULA_PARSER_FAST = Lark(_GRAMMAR, start="formula", parser="lalr")
|
|
697
|
+
_FILE_PARSER_FAST = Lark(_GRAMMAR, start="file", parser="lalr")
|
|
698
|
+
_TRANSFORMER = _TptpTransformer()
|
|
699
|
+
|
|
700
|
+
#: A DOL ``logic <Name>.<Sublogic>`` line is what HETS puts in front of every
|
|
701
|
+
#: ``GET /theory`` rendering (``logic TPTP.FOF``, ``logic CASL.SulFOL=``,
|
|
702
|
+
#: ``logic OWL.NP-sROIQx-D|Literal|...``). It is not TPTP, and neither is the
|
|
703
|
+
#: CASL ``%{ ... }%`` block that follows it -- TPTP's only comment forms are
|
|
704
|
+
#: ``%`` to end-of-line and ``/* ... */``, both already in the grammar's
|
|
705
|
+
#: %ignore lines above. Detected here only so the refusal can NAME it and
|
|
706
|
+
#: point at the stripper; the grammar is deliberately NOT widened, because a
|
|
707
|
+
#: reader that treated ``%{ ... }%`` as a comment would accept a CASL theory,
|
|
708
|
+
#: ignore its whole body and return an EMPTY formula list -- a silent empty
|
|
709
|
+
#: answer to a wrong-translation request.
|
|
710
|
+
#:
|
|
711
|
+
#: The WHOLE first line must be ``logic`` plus ONE DOL logic reference
|
|
712
|
+
#: (:data:`_HETS_LOGIC_REFERENCE`): a logic NAME, which is an identifier
|
|
713
|
+
#: (``TPTP``, ``CASL``, ``SoftFOL``, ``HasCASL``, ``OWL`` ...), optionally
|
|
714
|
+
#: followed by ``.`` and a free-form sublogic. The first character of the word
|
|
715
|
+
#: after ``logic`` is therefore a LETTER, never an operator, so the guard
|
|
716
|
+
#: cannot fire on a legitimate formula that happens to use ``logic`` as a
|
|
717
|
+
#: predicate or constant name: ``logic & p``, ``logic &p``, ``logic|q``,
|
|
718
|
+
#: ``logic =a``, ``logic != b``, ``logic <=> q``, ``logic(X)`` all keep parsing
|
|
719
|
+
#: exactly as before. (An earlier test, ``logic`` plus any whitespace-free
|
|
720
|
+
#: word, refused ``logic &p`` and ``logic =a`` -- valid formulas that parsed
|
|
721
|
+
#: before the pointer existed.)
|
|
722
|
+
#:
|
|
723
|
+
#: On top of the shape, :func:`_parse` consults it only AFTER both parsers have
|
|
724
|
+
#: failed, so by construction no text that parses is ever refused here; the
|
|
725
|
+
#: shape is what keeps the pointer from naming the wrong cause for a text that
|
|
726
|
+
#: does not.
|
|
727
|
+
_HETS_LOGIC_REFERENCE = r"[A-Za-z][A-Za-z0-9_]*(?:\.\S*)?"
|
|
728
|
+
_HETS_THEORY_HEADER = re.compile(r"logic[ \t]+" + _HETS_LOGIC_REFERENCE + r"\Z")
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
def _refuse_hets_theory_header(text: str) -> None:
|
|
732
|
+
"""Refuse a HETS ``/theory`` rendering by name. Called by :func:`_parse`
|
|
733
|
+
once the text has FAILED to parse, never before: a text that parses is not
|
|
734
|
+
a Hets rendering, whatever its first line looks like."""
|
|
735
|
+
stripped = text.lstrip()
|
|
736
|
+
first_line = stripped.split("\n", 1)[0].rstrip()
|
|
737
|
+
if not _HETS_THEORY_HEADER.fullmatch(first_line):
|
|
738
|
+
return
|
|
739
|
+
raise TptpParsingError(
|
|
740
|
+
"SYNTAX_ERROR: this is not a TPTP problem but a Hets theory "
|
|
741
|
+
f"rendering (it begins {first_line!r}; a '%{{ ... }}%' block is CASL "
|
|
742
|
+
"comment syntax, not TPTP). Strip the header first with "
|
|
743
|
+
"unicode_logic_kit.hets.strip_hets_theory_header(text), or fetch it "
|
|
744
|
+
"already stripped with HetsClient.theory_tptp().")
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
#: What carries no syntax of its own in TPTP text: a single-quoted atom (its escapes as in the
|
|
748
|
+
#: grammar's ``SQ``), a ``%`` comment to the end of the line, a ``/* ... */`` block comment.
|
|
749
|
+
_QUOTED_OR_COMMENT = re.compile(r"'(?:\\.|[^'\\])*'|%[^\r\n]*|/\*.*?\*/", re.DOTALL)
|
|
750
|
+
|
|
751
|
+
#: A ``thf`` statement: the keyword opens the text or follows the ``.`` that ends the
|
|
752
|
+
#: statement before it. (A ``.`` elsewhere is the point of a numeral, and a digit follows it.)
|
|
753
|
+
_THF_STATEMENT = re.compile(r"(?:\A|\.)\s*thf\s*\(")
|
|
754
|
+
|
|
755
|
+
#: TF1 syntax: the type binder ``!>``, and a quantifier variable whose type is ``$tType``
|
|
756
|
+
#: (``![A: $tType]``). TF0 uses ``$tType`` only in ``name: $tType``, a lower-case name.
|
|
757
|
+
_TF1_SYNTAX = re.compile(r"!>|[\[,]\s*[A-Z][A-Za-z0-9_]*\s*:\s*\$tType")
|
|
758
|
+
|
|
759
|
+
|
|
760
|
+
def _refuse_out_of_scope_syntax(text: str, *, problem: bool) -> None:
|
|
761
|
+
"""Refuse THF and TF1 by name before the first-order grammar is tried.
|
|
762
|
+
|
|
763
|
+
A THF statement has a body the first-order grammar cannot read (``p @ A``, a type
|
|
764
|
+
declaration ``p : $i > $o``), and TF1 writes a type variable with ``!>`` or with a
|
|
765
|
+
quantifier over ``$tType``; each ended in a syntax error that does not say what the
|
|
766
|
+
text is. The statement's KIND and the binder are found in the text with comments and
|
|
767
|
+
quoted atoms taken out, so neither a comment nor a quoted atom is ever refused, and
|
|
768
|
+
the check does not depend on the body parsing.
|
|
769
|
+
|
|
770
|
+
Args:
|
|
771
|
+
text: the text about to be parsed.
|
|
772
|
+
problem: whether ``text`` is a whole problem (statements). A bare formula has no
|
|
773
|
+
statement keyword: ``thf(a)`` there is the atom ``Thf(a)``.
|
|
774
|
+
|
|
775
|
+
Raises:
|
|
776
|
+
TptpParsingError: ``text`` has a ``thf`` statement.
|
|
777
|
+
_Tf1PolymorphismError: ``text`` uses ``!>`` or a ``$tType`` quantifier variable
|
|
778
|
+
(a :class:`TptpParsingError` and a :class:`NotImplementedError`).
|
|
779
|
+
"""
|
|
780
|
+
if "thf" not in text and "!>" not in text and "$tType" not in text:
|
|
781
|
+
return
|
|
782
|
+
code = _QUOTED_OR_COMMENT.sub(" ", text)
|
|
783
|
+
if problem and _THF_STATEMENT.search(code):
|
|
784
|
+
raise TptpParsingError(_THF_REFUSAL)
|
|
785
|
+
if _TF1_SYNTAX.search(code):
|
|
786
|
+
raise _Tf1PolymorphismError(_TF1_REFUSAL)
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
#: Parse-tree nodes whose ``VAR`` tokens are not variables of a formula: the
|
|
790
|
+
#: discarded 4th/5th annotation fields of a statement (an opaque term may carry
|
|
791
|
+
#: one), the statement's name, and the declarations that are not formulas.
|
|
792
|
+
_NOT_A_FORMULA = frozenset({
|
|
793
|
+
"annotation_term", "annotation_term_list", "fof_name", "include_stmt",
|
|
794
|
+
"name_list", "stmt_type",
|
|
795
|
+
})
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
def _refuse_merged_variables(tree) -> None:
|
|
799
|
+
"""Refuse, by name, a formula in which lower-casing turns two TPTP variables
|
|
800
|
+
into one.
|
|
801
|
+
|
|
802
|
+
:class:`_TptpTransformer` reads a TPTP variable as its lower-cased kit
|
|
803
|
+
:class:`Variable`, and a TPTP variable is case-sensitive: ``Xa`` and ``XA``
|
|
804
|
+
are two variables that both become ``xa``. Where that merges them the formula
|
|
805
|
+
is read as a DIFFERENT formula (``![Xa, XA]: p(Xa, XA)`` as ``∀xa ∀xa
|
|
806
|
+
P(xa, xa)``), and nothing said so. A kit variable here is a NAME, resolved to
|
|
807
|
+
the nearest enclosing binder of that name; TPTP resolves an occurrence to the
|
|
808
|
+
nearest enclosing binder of the EXACT spelling, or leaves it free. The two
|
|
809
|
+
readings of an occurrence therefore differ exactly when
|
|
810
|
+
|
|
811
|
+
* the nearest binder of the lower-cased name is not the nearest binder of the
|
|
812
|
+
exact spelling (one variable is captured by another's quantifier, or a free
|
|
813
|
+
variable by a binder), or
|
|
814
|
+
* two spellings of one lower-cased name occur free in the same formula.
|
|
815
|
+
|
|
816
|
+
Those are refused, naming both variables. Anything else reads as it always
|
|
817
|
+
did, byte for byte: ``(![Xa]: p(Xa)) & (![XA]: q(XA))`` has two binders that
|
|
818
|
+
never meet, so the kit's ``(∀xa P(xa)) ∧ (∀xa Q(xa))`` is the same formula.
|
|
819
|
+
Each statement of a problem is checked on its own. No renaming is invented
|
|
820
|
+
here: a formula that needs one is refused and the caller renames.
|
|
821
|
+
"""
|
|
822
|
+
if tree.data == "file":
|
|
823
|
+
roots = [child for child in tree.children
|
|
824
|
+
if isinstance(child, Tree) and child.data == "stmt"]
|
|
825
|
+
else:
|
|
826
|
+
roots = [tree]
|
|
827
|
+
for root in roots:
|
|
828
|
+
free: Dict[str, str] = {} # lower-cased name -> first free spelling
|
|
829
|
+
# (node, binders); binders is None or (spelling, enclosing binders)
|
|
830
|
+
stack: List[Tuple[Any, Any]] = [(root, None)]
|
|
831
|
+
while stack:
|
|
832
|
+
node, binders = stack.pop()
|
|
833
|
+
if not isinstance(node, Tree) or node.data in _NOT_A_FORMULA:
|
|
834
|
+
continue
|
|
835
|
+
if node.data == "var":
|
|
836
|
+
_check_variable_occurrence(str(node.children[0]), binders, free)
|
|
837
|
+
elif node.data in ("forall", "exists"):
|
|
838
|
+
varlist, body = node.children
|
|
839
|
+
for typed_var in varlist.children:
|
|
840
|
+
binders = (str(typed_var.children[0]), binders)
|
|
841
|
+
stack.append((body, binders))
|
|
842
|
+
else:
|
|
843
|
+
for child in reversed(node.children):
|
|
844
|
+
stack.append((child, binders))
|
|
845
|
+
|
|
846
|
+
|
|
847
|
+
def _check_variable_occurrence(spelling: str, binders: Any, free: Dict[str, str]) -> None:
|
|
848
|
+
"""One occurrence of the TPTP variable ``spelling`` under ``binders`` — see
|
|
849
|
+
:func:`_refuse_merged_variables`."""
|
|
850
|
+
lowered = spelling.lower()
|
|
851
|
+
exact = by_name = None
|
|
852
|
+
scope = binders
|
|
853
|
+
while scope is not None and (exact is None or by_name is None):
|
|
854
|
+
if exact is None and scope[0] == spelling:
|
|
855
|
+
exact = scope
|
|
856
|
+
if by_name is None and scope[0].lower() == lowered:
|
|
857
|
+
by_name = scope
|
|
858
|
+
scope = scope[1]
|
|
859
|
+
if by_name is not None and by_name is not exact:
|
|
860
|
+
_refuse_variable_pair(by_name[0], spelling, lowered)
|
|
861
|
+
if by_name is None:
|
|
862
|
+
first = free.setdefault(lowered, spelling)
|
|
863
|
+
if first != spelling:
|
|
864
|
+
_refuse_variable_pair(first, spelling, lowered)
|
|
865
|
+
|
|
866
|
+
|
|
867
|
+
def _refuse_variable_pair(one: str, other: str, lowered: str) -> None:
|
|
868
|
+
raise TptpParsingError(
|
|
869
|
+
f"SYNTAX_ERROR: the TPTP variables {one!r} and {other!r} are different "
|
|
870
|
+
f"variables that differ only by letter case, and this reader turns a "
|
|
871
|
+
f"TPTP variable into a kit variable by lower-casing it: both would "
|
|
872
|
+
f"become {lowered!r}, and the formula would be read with one variable "
|
|
873
|
+
f"where TPTP has two. Rename one of them so that they differ by more "
|
|
874
|
+
f"than letter case.")
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
def _parse(text: str, parser: Lark, what: str, *, fast: Optional[Lark] = None):
|
|
878
|
+
"""Parse + transform with unified error handling (unwrapping lark's VisitError).
|
|
879
|
+
|
|
880
|
+
``fast`` is the LALR(1) twin of ``parser``; it is tried first and Earley
|
|
881
|
+
takes over on ``UnexpectedInput``, so no text that parsed before stops
|
|
882
|
+
parsing and every syntax-error message is Earley's, unchanged. Only the
|
|
883
|
+
PARSE is retried — see "Two parsers" in the module docstring.
|
|
884
|
+
|
|
885
|
+
A HETS theory rendering is named only once BOTH parsers have refused the
|
|
886
|
+
text (see :func:`_refuse_hets_theory_header`): the pointer replaces a syntax
|
|
887
|
+
error with a better one, and can never turn a text that parses into a
|
|
888
|
+
refusal.
|
|
889
|
+
"""
|
|
890
|
+
_refuse_out_of_scope_syntax(text, problem=what == "problem")
|
|
891
|
+
tree = None
|
|
892
|
+
if fast is not None:
|
|
893
|
+
try:
|
|
894
|
+
tree = fast.parse(text)
|
|
895
|
+
except UnexpectedInput:
|
|
896
|
+
tree = None
|
|
897
|
+
if tree is None:
|
|
898
|
+
try:
|
|
899
|
+
tree = parser.parse(text)
|
|
900
|
+
except TptpParsingError:
|
|
901
|
+
raise
|
|
902
|
+
except Exception as exc:
|
|
903
|
+
_refuse_hets_theory_header(text)
|
|
904
|
+
raise TptpParsingError(
|
|
905
|
+
f"SYNTAX_ERROR: could not parse TPTP {what}: {exc}")
|
|
906
|
+
_refuse_merged_variables(tree)
|
|
907
|
+
try:
|
|
908
|
+
return _TRANSFORMER.transform(tree)
|
|
909
|
+
except VisitError as exc:
|
|
910
|
+
original = exc.orig_exc
|
|
911
|
+
# NotImplementedError alongside ParsingError: the TF0-scope refusals
|
|
912
|
+
# (TF1 polymorphism, $int/$rat/$real -- see _resolve_type_str /
|
|
913
|
+
# tff_poly_decl) are meant to surface as NotImplementedError, not get
|
|
914
|
+
# folded into a generic SYNTAX_ERROR string, so its type must survive
|
|
915
|
+
# this unwrap exactly like ParsingError's already does.
|
|
916
|
+
if isinstance(original, (ParsingError, NotImplementedError)):
|
|
917
|
+
raise original
|
|
918
|
+
raise TptpParsingError(f"SYNTAX_ERROR: in TPTP {what}: {original}")
|
|
919
|
+
|
|
920
|
+
|
|
921
|
+
# =============================================================================
|
|
922
|
+
# Include resolution — shared by parse_tptp / parse_tptp_problem /
|
|
923
|
+
# parse_tff_problem and their load_* siblings (see the module docstring's
|
|
924
|
+
# "Includes" section).
|
|
925
|
+
# =============================================================================
|
|
926
|
+
|
|
927
|
+
def _locate_include(file_name: str, base_dir: str, search_paths) -> str:
|
|
928
|
+
"""Resolve one ``include(file_name)`` directive to an existing file path.
|
|
929
|
+
|
|
930
|
+
Tried in order: (1) ``base_dir/file_name`` (relative to the file
|
|
931
|
+
containing the include directive itself), (2) each root in
|
|
932
|
+
``search_paths`` joined with ``file_name``, in order, then (3)
|
|
933
|
+
``os.environ["TPTP"]`` joined with ``file_name``, if that environment
|
|
934
|
+
variable is set — the de facto TPTP-library convention (an include like
|
|
935
|
+
``include('Axioms/SET001+0.ax')`` is root-relative, not relative to
|
|
936
|
+
whatever problem file referred to it).
|
|
937
|
+
|
|
938
|
+
Raises:
|
|
939
|
+
TptpParsingError: none of the tried candidates exist — names the
|
|
940
|
+
include's path and every candidate location tried.
|
|
941
|
+
"""
|
|
942
|
+
roots = [base_dir, *search_paths]
|
|
943
|
+
tptp_env = os.environ.get("TPTP")
|
|
944
|
+
if tptp_env:
|
|
945
|
+
roots.append(tptp_env)
|
|
946
|
+
tried = []
|
|
947
|
+
for root in roots:
|
|
948
|
+
candidate = os.path.join(root, file_name)
|
|
949
|
+
tried.append(candidate)
|
|
950
|
+
if os.path.isfile(candidate):
|
|
951
|
+
return candidate
|
|
952
|
+
raise TptpParsingError(
|
|
953
|
+
f"SYNTAX_ERROR: include({file_name!r}) could not be found -- tried: "
|
|
954
|
+
+ ", ".join(repr(t) for t in tried) + "."
|
|
955
|
+
)
|
|
956
|
+
|
|
957
|
+
|
|
958
|
+
def _apply_selection(items: list, selection: Tuple[str, ...], file_name: str) -> list:
|
|
959
|
+
"""Filter an included file's already-resolved ``items`` down to
|
|
960
|
+
``selection``, by each :class:`TptpFormula`'s own TPTP statement name,
|
|
961
|
+
in the order ``selection`` names them (not the file's own order).
|
|
962
|
+
|
|
963
|
+
Raises:
|
|
964
|
+
TptpParsingError: ``items`` contains anything other than
|
|
965
|
+
:class:`TptpFormula` records (a TF0 type declaration — its own
|
|
966
|
+
statement name is not tracked separately from the symbol it
|
|
967
|
+
declares, see :class:`_TffSortDecl`/:class:`_TffPredDecl`/
|
|
968
|
+
:class:`_TffFuncDecl`, so a selection list cannot be matched
|
|
969
|
+
against it), or ``selection`` names something absent from the
|
|
970
|
+
included file.
|
|
971
|
+
"""
|
|
972
|
+
non_formula = [item for item in items if not isinstance(item, TptpFormula)]
|
|
973
|
+
if non_formula:
|
|
974
|
+
raise TptpParsingError(
|
|
975
|
+
f"SYNTAX_ERROR: include({file_name!r}, [...]) cannot use a "
|
|
976
|
+
"selection list on a file that declares TFF vocabulary (a "
|
|
977
|
+
"'tff(name, type, ...).' statement) -- its own statement name "
|
|
978
|
+
"is not tracked separately from the symbol it declares, so a "
|
|
979
|
+
"selection list cannot be matched against it; include the "
|
|
980
|
+
"whole file (drop the selection list) instead."
|
|
981
|
+
)
|
|
982
|
+
by_name = {}
|
|
983
|
+
for formula in items:
|
|
984
|
+
by_name.setdefault(formula.name, formula)
|
|
985
|
+
selected = []
|
|
986
|
+
for name in selection:
|
|
987
|
+
if name not in by_name:
|
|
988
|
+
raise TptpParsingError(
|
|
989
|
+
f"SYNTAX_ERROR: include({file_name!r}, [...]) selects "
|
|
990
|
+
f"{name!r}, which is not a formula name in {file_name!r} "
|
|
991
|
+
f"(available: {sorted(by_name)!r})."
|
|
992
|
+
)
|
|
993
|
+
selected.append(by_name[name])
|
|
994
|
+
return selected
|
|
995
|
+
|
|
996
|
+
|
|
997
|
+
def _resolve_includes(items: list, base_dir: Optional[str], search_paths,
|
|
998
|
+
chain: Tuple[Tuple[str, str], ...]) -> list:
|
|
999
|
+
"""Recursively splice every :class:`_IncludeDirective` in ``items`` with
|
|
1000
|
+
the (itself include-resolved) statements of the file it names, in place
|
|
1001
|
+
— preserving surrounding statement order.
|
|
1002
|
+
|
|
1003
|
+
``chain`` is the tuple of ``(name_as_written, real_path)`` pairs for
|
|
1004
|
+
every include currently "open" (the stack of files that led here),
|
|
1005
|
+
used both to detect a cycle (a repeated ``real_path``) and to name the
|
|
1006
|
+
full trail in the resulting error. :func:`_load_and_resolve` seeds it
|
|
1007
|
+
with the loaded file's own path; a bare-text :func:`parse_tptp` call
|
|
1008
|
+
starts it empty.
|
|
1009
|
+
|
|
1010
|
+
Raises:
|
|
1011
|
+
TptpParsingError: an include is encountered with ``base_dir=None``
|
|
1012
|
+
(no including file's directory is known — see the module
|
|
1013
|
+
docstring), the named file cannot be found (see
|
|
1014
|
+
:func:`_locate_include`), or the chain cycles back to a file
|
|
1015
|
+
already open.
|
|
1016
|
+
"""
|
|
1017
|
+
resolved = []
|
|
1018
|
+
for item in items:
|
|
1019
|
+
if not isinstance(item, _IncludeDirective):
|
|
1020
|
+
resolved.append(item)
|
|
1021
|
+
continue
|
|
1022
|
+
if base_dir is None:
|
|
1023
|
+
raise TptpParsingError(
|
|
1024
|
+
f"SYNTAX_ERROR: include({item.file_name!r}) cannot be "
|
|
1025
|
+
"resolved -- no including file's directory is known "
|
|
1026
|
+
"(base_dir=None, the default, for bare text); read the "
|
|
1027
|
+
"problem via load_tptp / load_tptp_problem / "
|
|
1028
|
+
"load_tff_problem, or pass base_dir explicitly."
|
|
1029
|
+
)
|
|
1030
|
+
path = _locate_include(item.file_name, base_dir, search_paths)
|
|
1031
|
+
real = os.path.realpath(path)
|
|
1032
|
+
if any(real == seen_real for _seen_name, seen_real in chain):
|
|
1033
|
+
trail = " -> ".join(repr(name) for name, _real in chain)
|
|
1034
|
+
raise TptpParsingError(
|
|
1035
|
+
f"SYNTAX_ERROR: circular include: {trail} -> "
|
|
1036
|
+
f"{item.file_name!r}."
|
|
1037
|
+
)
|
|
1038
|
+
with open(path, "r", encoding="utf-8") as handle:
|
|
1039
|
+
sub_text = handle.read()
|
|
1040
|
+
sub_items = _parse(sub_text, _FILE_PARSER, "problem",
|
|
1041
|
+
fast=_FILE_PARSER_FAST)
|
|
1042
|
+
sub_resolved = _resolve_includes(
|
|
1043
|
+
sub_items, os.path.dirname(path), search_paths,
|
|
1044
|
+
chain + ((item.file_name, real),))
|
|
1045
|
+
if item.selection is not None:
|
|
1046
|
+
sub_resolved = _apply_selection(sub_resolved, item.selection, item.file_name)
|
|
1047
|
+
resolved.extend(sub_resolved)
|
|
1048
|
+
return resolved
|
|
1049
|
+
|
|
1050
|
+
|
|
1051
|
+
def _finalize_formulas(items: list) -> list:
|
|
1052
|
+
"""Confirm every item in an include-resolved ``items`` list is a
|
|
1053
|
+
:class:`TptpFormula` — the shared tail of :func:`parse_tptp` /
|
|
1054
|
+
:func:`load_tptp` / :func:`parse_tptp_problem` / :func:`load_tptp_problem`,
|
|
1055
|
+
none of which can represent a TF0 type declaration."""
|
|
1056
|
+
for item in items:
|
|
1057
|
+
if not isinstance(item, TptpFormula):
|
|
1058
|
+
raise TptpParsingError(
|
|
1059
|
+
"SYNTAX_ERROR: this problem contains a TFF type declaration "
|
|
1060
|
+
"('tff(name, type, ...).'), which parse_tptp/load_tptp cannot "
|
|
1061
|
+
"represent as a TptpFormula — use parse_tff_problem() instead, "
|
|
1062
|
+
"which returns the declared Signature alongside the formulas."
|
|
1063
|
+
)
|
|
1064
|
+
return items
|
|
1065
|
+
|
|
1066
|
+
|
|
1067
|
+
def _load_and_resolve(path: str, search_paths) -> Tuple[list, str]:
|
|
1068
|
+
"""Read ``path``, parse it, and resolve its includes with the cycle
|
|
1069
|
+
``chain`` (see :func:`_resolve_includes`) seeded by ``path`` itself —
|
|
1070
|
+
so a chain that loops back to the very file
|
|
1071
|
+
:func:`load_tptp`/:func:`load_tptp_problem`/:func:`load_tff_problem`
|
|
1072
|
+
started from is caught, not only a cycle confined to files reached
|
|
1073
|
+
purely via nested includes.
|
|
1074
|
+
|
|
1075
|
+
Returns:
|
|
1076
|
+
``(items, text)`` — ``items`` fully include-resolved (no
|
|
1077
|
+
:class:`_IncludeDirective` survives), ``text`` the loaded file's own
|
|
1078
|
+
raw contents (for :func:`load_tptp_problem`'s header scan, which
|
|
1079
|
+
reads only the top-level file's ``%`` comments, never an included
|
|
1080
|
+
file's).
|
|
1081
|
+
"""
|
|
1082
|
+
real_path = os.path.realpath(path)
|
|
1083
|
+
with open(path, "r", encoding="utf-8") as handle:
|
|
1084
|
+
text = handle.read()
|
|
1085
|
+
items = _parse(text, _FILE_PARSER, "problem", fast=_FILE_PARSER_FAST)
|
|
1086
|
+
items = _resolve_includes(
|
|
1087
|
+
items, os.path.dirname(path), search_paths, ((path, real_path),))
|
|
1088
|
+
return items, text
|
|
1089
|
+
|
|
1090
|
+
|
|
1091
|
+
def parse_tptp_formula(text: str) -> Node:
|
|
1092
|
+
"""Parse a single bare TPTP FOF/CNF formula (no ``fof(...)`` wrapper) into a Node.
|
|
1093
|
+
|
|
1094
|
+
Args:
|
|
1095
|
+
text: a TPTP formula, e.g. ``"![X]: (p(X) => q(X))"``.
|
|
1096
|
+
|
|
1097
|
+
Returns:
|
|
1098
|
+
The formula as a toolkit :class:`Node`.
|
|
1099
|
+
|
|
1100
|
+
Raises:
|
|
1101
|
+
ParsingError: if ``text`` is not a well-formed TPTP formula.
|
|
1102
|
+
"""
|
|
1103
|
+
return _parse(text, _FORMULA_PARSER, "formula",
|
|
1104
|
+
fast=_FORMULA_PARSER_FAST)
|
|
1105
|
+
|
|
1106
|
+
|
|
1107
|
+
def parse_tptp(text: str, *, base_dir: Optional[str] = None, search_paths=()) -> list:
|
|
1108
|
+
"""Parse a whole TPTP problem into a list of :class:`TptpFormula` records.
|
|
1109
|
+
|
|
1110
|
+
Args:
|
|
1111
|
+
text: the contents of a TPTP problem — one or more ``fof(...)`` /
|
|
1112
|
+
``cnf(...)`` / ``tff(...)`` statements, and/or ``include(...)``
|
|
1113
|
+
directives (``%`` line comments and ``/* */`` block comments are
|
|
1114
|
+
ignored). A ``tff`` AXIOM/CONJECTURE/... statement is accepted
|
|
1115
|
+
exactly like ``fof`` (its typed quantifiers build
|
|
1116
|
+
:class:`~unicode_logic_kit.fol.nodes.SortedQuantifier`), but a
|
|
1117
|
+
``tff(name, type, ...).`` TYPE DECLARATION is not (see
|
|
1118
|
+
``Raises`` below).
|
|
1119
|
+
base_dir: the directory an ``include(...)`` in ``text`` resolves
|
|
1120
|
+
relative to first. ``None`` (the default) means "no including
|
|
1121
|
+
file's directory is known" — an ``include`` then raises (see
|
|
1122
|
+
``Raises``); :func:`load_tptp` sets this to the loaded file's
|
|
1123
|
+
own directory automatically. See the module docstring's
|
|
1124
|
+
"Includes" section for the full resolution order.
|
|
1125
|
+
search_paths: further roots tried, in order, after ``base_dir``,
|
|
1126
|
+
for an ``include`` not found relative to it (plus
|
|
1127
|
+
``os.environ["TPTP"]``, if set) — see "Includes".
|
|
1128
|
+
|
|
1129
|
+
Returns:
|
|
1130
|
+
A list of :class:`TptpFormula` ``(name, role, formula)`` in source
|
|
1131
|
+
order, with every ``include`` spliced in at its own position.
|
|
1132
|
+
|
|
1133
|
+
Raises:
|
|
1134
|
+
ParsingError: if the text is not a well-formed TPTP problem, if it
|
|
1135
|
+
contains a ``tff(name, type, ...).`` type declaration — this
|
|
1136
|
+
function has no ``TptpFormula`` shape to return a type
|
|
1137
|
+
declaration as; use :func:`parse_tff_problem` instead, which
|
|
1138
|
+
returns the declared :class:`~unicode_logic_kit.fol.signature
|
|
1139
|
+
.Signature` alongside the formulas — or if an ``include``
|
|
1140
|
+
cannot be resolved (``base_dir=None``, a missing file, or a
|
|
1141
|
+
circular chain; see "Includes").
|
|
1142
|
+
"""
|
|
1143
|
+
items = _parse(text, _FILE_PARSER, "problem", fast=_FILE_PARSER_FAST)
|
|
1144
|
+
items = _resolve_includes(items, base_dir, search_paths, ())
|
|
1145
|
+
return _finalize_formulas(items)
|
|
1146
|
+
|
|
1147
|
+
|
|
1148
|
+
def load_tptp(path: str, *, search_paths=()) -> list:
|
|
1149
|
+
"""Read a TPTP problem file and :func:`parse_tptp` its contents.
|
|
1150
|
+
|
|
1151
|
+
Args:
|
|
1152
|
+
path: path to a ``.p`` / ``.tptp`` file. Every ``include(...)`` it
|
|
1153
|
+
contains resolves relative to ``path``'s own directory first
|
|
1154
|
+
(then ``search_paths`` — see :func:`parse_tptp`), and a cycle
|
|
1155
|
+
back to ``path`` itself is caught, not only one confined to
|
|
1156
|
+
files reached purely via nested includes.
|
|
1157
|
+
search_paths: see :func:`parse_tptp`.
|
|
1158
|
+
|
|
1159
|
+
Returns:
|
|
1160
|
+
A list of :class:`TptpFormula` records.
|
|
1161
|
+
"""
|
|
1162
|
+
items, _text = _load_and_resolve(path, search_paths)
|
|
1163
|
+
return _finalize_formulas(items)
|
|
1164
|
+
|
|
1165
|
+
|
|
1166
|
+
def _conflict(kind: str, name: str, existing, new) -> TptpParsingError:
|
|
1167
|
+
return TptpParsingError(
|
|
1168
|
+
f"SYNTAX_ERROR: {kind} '{name}' is declared more than once with "
|
|
1169
|
+
f"different types ({existing!r} vs {new!r})."
|
|
1170
|
+
)
|
|
1171
|
+
|
|
1172
|
+
|
|
1173
|
+
def _build_signature_and_formulas(items: list) -> Tuple[Signature, List[TptpFormula]]:
|
|
1174
|
+
"""The shared, include-resolution-independent guts of
|
|
1175
|
+
:func:`parse_tff_problem` / :func:`load_tff_problem`: split an already
|
|
1176
|
+
include-resolved ``items`` list into a
|
|
1177
|
+
:class:`~unicode_logic_kit.fol.signature.Signature` and promoted formulas.
|
|
1178
|
+
See :func:`parse_tff_problem` for the full contract."""
|
|
1179
|
+
decls = [i for i in items if not isinstance(i, TptpFormula)]
|
|
1180
|
+
formulas = [i for i in items if isinstance(i, TptpFormula)]
|
|
1181
|
+
|
|
1182
|
+
sorts: set = set()
|
|
1183
|
+
predicates: Dict[str, PredicateDecl] = {}
|
|
1184
|
+
functions: Dict[str, FunctionDecl] = {}
|
|
1185
|
+
constants: Dict[str, ConstantDecl] = {}
|
|
1186
|
+
const_sorts: Dict[str, str] = {}
|
|
1187
|
+
|
|
1188
|
+
for d in decls:
|
|
1189
|
+
if isinstance(d, _TffSortDecl):
|
|
1190
|
+
sorts.add(d.name)
|
|
1191
|
+
elif isinstance(d, _TffPredDecl):
|
|
1192
|
+
new_decl = PredicateDecl(d.name, len(d.arg_sorts), d.arg_sorts)
|
|
1193
|
+
existing = predicates.get(d.name)
|
|
1194
|
+
if existing is not None and existing != new_decl:
|
|
1195
|
+
raise _conflict("predicate", d.name, existing, new_decl)
|
|
1196
|
+
predicates[d.name] = new_decl
|
|
1197
|
+
elif isinstance(d, _TffFuncDecl):
|
|
1198
|
+
if not d.arg_sorts:
|
|
1199
|
+
new_const = ConstantDecl(d.name, d.result_sort)
|
|
1200
|
+
existing_const = constants.get(d.name)
|
|
1201
|
+
if existing_const is not None and existing_const != new_const:
|
|
1202
|
+
raise _conflict("constant", d.name, existing_const, new_const)
|
|
1203
|
+
constants[d.name] = new_const
|
|
1204
|
+
if d.result_sort is not None:
|
|
1205
|
+
const_sorts[d.name] = d.result_sort
|
|
1206
|
+
else:
|
|
1207
|
+
new_func = FunctionDecl(d.name, len(d.arg_sorts), d.arg_sorts, d.result_sort)
|
|
1208
|
+
existing_func = functions.get(d.name)
|
|
1209
|
+
if existing_func is not None and existing_func != new_func:
|
|
1210
|
+
raise _conflict("function", d.name, existing_func, new_func)
|
|
1211
|
+
functions[d.name] = new_func
|
|
1212
|
+
else: # pragma: no cover -- file/stmt_type can only produce the three above
|
|
1213
|
+
raise AssertionError(f"parse_tff_problem: unreachable decl type {type(d).__name__!r}")
|
|
1214
|
+
|
|
1215
|
+
for decl in predicates.values():
|
|
1216
|
+
sorts.update(s for s in decl.arg_sorts if s is not None)
|
|
1217
|
+
for decl in functions.values():
|
|
1218
|
+
sorts.update(s for s in decl.arg_sorts if s is not None)
|
|
1219
|
+
if decl.result_sort is not None:
|
|
1220
|
+
sorts.add(decl.result_sort)
|
|
1221
|
+
for decl in constants.values():
|
|
1222
|
+
if decl.sort is not None:
|
|
1223
|
+
sorts.add(decl.sort)
|
|
1224
|
+
for f in formulas:
|
|
1225
|
+
for n in f.formula.walk():
|
|
1226
|
+
if isinstance(n, SortedQuantifier):
|
|
1227
|
+
sorts.add(n.sort)
|
|
1228
|
+
|
|
1229
|
+
signature = Signature(predicates=predicates, functions=functions,
|
|
1230
|
+
constants=constants, sorts=frozenset(sorts))
|
|
1231
|
+
|
|
1232
|
+
def _promote(node: Node) -> Node:
|
|
1233
|
+
node = node.map_children(_promote)
|
|
1234
|
+
if isinstance(node, Constant) and node.name in const_sorts:
|
|
1235
|
+
return SortedConstant(node.name, const_sorts[node.name])
|
|
1236
|
+
return node
|
|
1237
|
+
|
|
1238
|
+
promoted = [TptpFormula(f.name, f.role, _promote(f.formula)) for f in formulas]
|
|
1239
|
+
return signature, promoted
|
|
1240
|
+
|
|
1241
|
+
|
|
1242
|
+
def parse_tff_problem(
|
|
1243
|
+
text: str, *, base_dir: Optional[str] = None, search_paths=()
|
|
1244
|
+
) -> Tuple[Signature, List[TptpFormula]]:
|
|
1245
|
+
"""Parse a whole TF0 (monomorphic typed) TPTP ``tff`` problem.
|
|
1246
|
+
|
|
1247
|
+
The typed sibling of :func:`parse_tptp`: reads every ``tff(name, type,
|
|
1248
|
+
...).`` TYPE DECLARATION into a :class:`~unicode_logic_kit.fol.signature
|
|
1249
|
+
.Signature` (one entry per declared sort/predicate/function/constant —
|
|
1250
|
+
an argument or result type of ``$i`` resolves to ``None`` — "no concrete
|
|
1251
|
+
sort", TPTP's own default — everything else to the corresponding
|
|
1252
|
+
capitalised kit-level sort name), and every other statement
|
|
1253
|
+
(``axiom``/``conjecture``/...) into a :class:`TptpFormula` exactly like
|
|
1254
|
+
:func:`parse_tptp` does, EXTRA step: any bare
|
|
1255
|
+
:class:`~unicode_logic_kit.fol.nodes.Constant` occurrence in a formula
|
|
1256
|
+
whose name was declared with a concrete sort is promoted to a
|
|
1257
|
+
:class:`~unicode_logic_kit.fol.nodes.SortedConstant` — a TFF formula BODY
|
|
1258
|
+
carries no inline sort annotation for a constant (only a bound variable
|
|
1259
|
+
does, via ``X: sort``), so this is the only place that information can
|
|
1260
|
+
be recovered and reattached to the AST.
|
|
1261
|
+
|
|
1262
|
+
``include(...)`` directives are resolved exactly like :func:`parse_tptp`
|
|
1263
|
+
(splicing recursively, ``base_dir``/``search_paths`` work the same way —
|
|
1264
|
+
see the module docstring's "Includes" section) — WITH ONE RESTRICTION: a
|
|
1265
|
+
``include('path', [name1, ...])`` selection list can only be applied to
|
|
1266
|
+
an included file that is itself pure ``fof``/``cnf``/``tff`` FORMULAS,
|
|
1267
|
+
never one that also declares TFF vocabulary (a ``tff(name, type,
|
|
1268
|
+
...).`` statement) — a type declaration's own statement name is not
|
|
1269
|
+
tracked separately from the symbol it declares (see
|
|
1270
|
+
:class:`_TffSortDecl`/:class:`_TffPredDecl`/:class:`_TffFuncDecl`), so a
|
|
1271
|
+
selection list cannot be matched against it; that combination raises
|
|
1272
|
+
(see ``Raises``). A *whole-file* include (no selection list) always
|
|
1273
|
+
works, decls and formulas alike.
|
|
1274
|
+
|
|
1275
|
+
Args:
|
|
1276
|
+
text: the contents of a TF0 TPTP problem — a mix of
|
|
1277
|
+
``tff(name, type, Sym: Type).`` declarations,
|
|
1278
|
+
``tff(name, axiom|conjecture|..., <formula>).`` statements, and
|
|
1279
|
+
``include(...)`` directives (``fof``/``cnf`` statements may also
|
|
1280
|
+
appear; their formulas are included unchanged, exactly as
|
|
1281
|
+
:func:`parse_tptp` would read them).
|
|
1282
|
+
base_dir: see :func:`parse_tptp`; :func:`load_tff_problem` sets this
|
|
1283
|
+
automatically.
|
|
1284
|
+
search_paths: see :func:`parse_tptp`.
|
|
1285
|
+
|
|
1286
|
+
Returns:
|
|
1287
|
+
``(signature, formulas)`` — ``formulas`` in source order (type
|
|
1288
|
+
declarations are consumed into ``signature``, not returned as
|
|
1289
|
+
formulas). ``signature.sorts`` also includes every sort named by a
|
|
1290
|
+
:class:`~unicode_logic_kit.fol.nodes.SortedQuantifier` occurring in
|
|
1291
|
+
the formulas even if that sort was never separately declared with
|
|
1292
|
+
its own ``$tType`` statement (real TF0 files always do, but this
|
|
1293
|
+
keeps the reader lenient rather than dropping information).
|
|
1294
|
+
|
|
1295
|
+
Raises:
|
|
1296
|
+
ParsingError: the text is not a well-formed TPTP problem, the same
|
|
1297
|
+
symbol is declared twice with two DIFFERENT types, or an
|
|
1298
|
+
``include`` cannot be resolved (unresolvable path, circular
|
|
1299
|
+
chain, or a selection list combined with TFF vocabulary, all as
|
|
1300
|
+
described above).
|
|
1301
|
+
NotImplementedError: a type declaration uses TF1 polymorphism
|
|
1302
|
+
(``!> [...] : ...``) or one of TPTP's arithmetic sorts
|
|
1303
|
+
(``$int``/``$rat``/``$real``) — see :func:`_resolve_type_str` /
|
|
1304
|
+
``_TptpTransformer.tff_poly_decl``.
|
|
1305
|
+
"""
|
|
1306
|
+
items = _parse(text, _FILE_PARSER, "problem", fast=_FILE_PARSER_FAST)
|
|
1307
|
+
items = _resolve_includes(items, base_dir, search_paths, ())
|
|
1308
|
+
return _build_signature_and_formulas(items)
|
|
1309
|
+
|
|
1310
|
+
|
|
1311
|
+
def load_tff_problem(path: str, *, search_paths=()) -> Tuple[Signature, List[TptpFormula]]:
|
|
1312
|
+
"""Read a TF0 TPTP problem file and :func:`parse_tff_problem` its
|
|
1313
|
+
contents, resolving ``include(...)`` directives exactly like
|
|
1314
|
+
:func:`load_tptp` (``base_dir`` set to ``path``'s own directory, cycle
|
|
1315
|
+
detection seeded with ``path`` itself — see :func:`load_tptp`)."""
|
|
1316
|
+
items, _text = _load_and_resolve(path, search_paths)
|
|
1317
|
+
return _build_signature_and_formulas(items)
|
|
1318
|
+
|
|
1319
|
+
|
|
1320
|
+
# ---------------------------------------------------------------------------
|
|
1321
|
+
# TPTP header metadata
|
|
1322
|
+
# ---------------------------------------------------------------------------
|
|
1323
|
+
#
|
|
1324
|
+
# TPTP problem files carry a standardised block of '%' comment lines near the top
|
|
1325
|
+
# recording the file's provenance and, for problems drawn from the TPTP library,
|
|
1326
|
+
# its ground-truth verdict and empirical difficulty rating, e.g.:
|
|
1327
|
+
#
|
|
1328
|
+
# % File : PUZ001-1 : TPTP v8.1.0. Released v1.0.0.
|
|
1329
|
+
# % Domain : Puzzles
|
|
1330
|
+
# % Problem : Dreadbury Mansion
|
|
1331
|
+
# % Status : Theorem
|
|
1332
|
+
# % Rating : 0.43 v8.1.0, 0.36 v7.4.0
|
|
1333
|
+
#
|
|
1334
|
+
# _GRAMMAR above ``%``-ignores every comment, so none of this ever reaches the Lark
|
|
1335
|
+
# parser or the TptpFormula records built from it. The functions below recover it by
|
|
1336
|
+
# a separate, deterministic line scan of the *raw* text, run independently of (and
|
|
1337
|
+
# before) the grammar-based formula parse, then hand formula parsing off unchanged to
|
|
1338
|
+
# :func:`parse_tptp` — this extends the module's contract rather than altering it (see
|
|
1339
|
+
# the regression tests pinning parse_tptp / parse_tptp_formula / load_tptp / TptpFormula
|
|
1340
|
+
# unchanged, in tests/test_tptp_header.py).
|
|
1341
|
+
|
|
1342
|
+
# A header field line: optional leading whitespace, '%', optional whitespace, one of
|
|
1343
|
+
# the five recognised TPTP field names (matched case-sensitively, per the TPTP
|
|
1344
|
+
# standard — a lowercase "% status : ..." line is not a field line), optional
|
|
1345
|
+
# whitespace, ':', optional whitespace, then the rest of the line as the raw value.
|
|
1346
|
+
_HEADER_FIELD_RE = re.compile(
|
|
1347
|
+
r"^\s*%\s*(File|Domain|Problem|Status|Rating)\s*:\s*(.*)$"
|
|
1348
|
+
)
|
|
1349
|
+
# The first float-looking token anywhere in a Rating value, e.g. "0.43" out of
|
|
1350
|
+
# "0.43 v8.1.0, 0.36 v7.4.0" (a version tag like "v8.1.0" is not itself a match
|
|
1351
|
+
# candidate since it lacks a leading digit, but even if it were, "0.43" still wins
|
|
1352
|
+
# because re.search returns the leftmost match and it appears first in the string).
|
|
1353
|
+
_RATING_RE = re.compile(r"[-+]?[0-9]+\.[0-9]+")
|
|
1354
|
+
|
|
1355
|
+
|
|
1356
|
+
@dataclass(frozen=True)
|
|
1357
|
+
class TptpHeader:
|
|
1358
|
+
"""Metadata recovered from a TPTP problem file's ``%`` comment header.
|
|
1359
|
+
|
|
1360
|
+
``status`` / ``rating`` / ``domain`` / ``problem`` / ``file`` are ``None`` when
|
|
1361
|
+
the corresponding ``% <Field> : ...`` line is absent (or, for ``status``/
|
|
1362
|
+
``rating``, present but unparseable — see :func:`_parse_tptp_header`).
|
|
1363
|
+
``comments`` always holds *every* raw ``%`` line verbatim (stripped only of its
|
|
1364
|
+
trailing newline), in source order, so nothing is lost even for header lines this
|
|
1365
|
+
reader does not specifically interpret (``% Version``, ``% Refs``, banner lines
|
|
1366
|
+
of dashes, ...).
|
|
1367
|
+
"""
|
|
1368
|
+
|
|
1369
|
+
status: Optional[str]
|
|
1370
|
+
rating: Optional[float]
|
|
1371
|
+
domain: Optional[str]
|
|
1372
|
+
problem: Optional[str]
|
|
1373
|
+
file: Optional[str]
|
|
1374
|
+
comments: Tuple[str, ...]
|
|
1375
|
+
|
|
1376
|
+
|
|
1377
|
+
@dataclass(frozen=True)
|
|
1378
|
+
class TptpProblem:
|
|
1379
|
+
"""A whole TPTP problem: its parsed ``formulas`` plus the recovered ``header``."""
|
|
1380
|
+
|
|
1381
|
+
formulas: Tuple[TptpFormula, ...]
|
|
1382
|
+
header: TptpHeader
|
|
1383
|
+
|
|
1384
|
+
|
|
1385
|
+
def _parse_tptp_header(text: str) -> TptpHeader:
|
|
1386
|
+
"""Scan raw TPTP text for its ``%`` header block, independent of the grammar.
|
|
1387
|
+
|
|
1388
|
+
Every line whose first non-whitespace character is ``%`` is collected verbatim
|
|
1389
|
+
(trailing newline stripped) into ``comments``, in source order. Among those, a
|
|
1390
|
+
line matching :data:`_HEADER_FIELD_RE` (``% <Field> : <value>``, field names
|
|
1391
|
+
matched case-sensitively, whitespace around ``%``/the field name/``:``
|
|
1392
|
+
unconstrained) populates the matching :class:`TptpHeader` attribute — the
|
|
1393
|
+
*first* occurrence of each field wins; later repeats of the same field are still
|
|
1394
|
+
recorded in ``comments`` but do not overwrite it.
|
|
1395
|
+
|
|
1396
|
+
Field values are interpreted as follows:
|
|
1397
|
+
|
|
1398
|
+
- ``Status``: the first whitespace-delimited token of the value (e.g.
|
|
1399
|
+
``"Theorem"`` out of ``"Theorem"``, or ``"CounterSatisfiable"``), kept as a
|
|
1400
|
+
plain string — not validated against a fixed vocabulary. ``None`` if the value
|
|
1401
|
+
has no token (e.g. it is empty).
|
|
1402
|
+
- ``Rating``: the first float literal found anywhere in the value (e.g. ``0.43``
|
|
1403
|
+
out of ``"0.43 v8.1.0, 0.36 v7.4.0"``); ``None`` if none is found (e.g. the
|
|
1404
|
+
value is ``"?"``).
|
|
1405
|
+
- ``File`` / ``Domain`` / ``Problem``: the value, whitespace-trimmed, verbatim
|
|
1406
|
+
(further ``:`` characters inside the value, e.g. a ``File`` value's own
|
|
1407
|
+
``name : version`` sub-structure, are kept as literal text).
|
|
1408
|
+
|
|
1409
|
+
Args:
|
|
1410
|
+
text: the raw contents of a TPTP problem file (or any TPTP text).
|
|
1411
|
+
|
|
1412
|
+
Returns:
|
|
1413
|
+
A :class:`TptpHeader`; every field is ``None`` and ``comments`` is ``()`` if
|
|
1414
|
+
the text has no ``%`` lines at all.
|
|
1415
|
+
"""
|
|
1416
|
+
comments = []
|
|
1417
|
+
fields = {}
|
|
1418
|
+
for line in text.splitlines():
|
|
1419
|
+
if not line.lstrip().startswith("%"):
|
|
1420
|
+
continue
|
|
1421
|
+
comments.append(line)
|
|
1422
|
+
match = _HEADER_FIELD_RE.match(line)
|
|
1423
|
+
if not match:
|
|
1424
|
+
continue
|
|
1425
|
+
field, value = match.group(1), match.group(2)
|
|
1426
|
+
if field not in fields:
|
|
1427
|
+
fields[field] = value
|
|
1428
|
+
|
|
1429
|
+
status_raw = fields.get("Status")
|
|
1430
|
+
status_tokens = status_raw.split() if status_raw is not None else []
|
|
1431
|
+
status = status_tokens[0] if status_tokens else None
|
|
1432
|
+
|
|
1433
|
+
rating_raw = fields.get("Rating")
|
|
1434
|
+
rating_match = _RATING_RE.search(rating_raw) if rating_raw is not None else None
|
|
1435
|
+
rating = float(rating_match.group(0)) if rating_match else None
|
|
1436
|
+
|
|
1437
|
+
domain = fields.get("Domain")
|
|
1438
|
+
problem = fields.get("Problem")
|
|
1439
|
+
file_ = fields.get("File")
|
|
1440
|
+
|
|
1441
|
+
return TptpHeader(
|
|
1442
|
+
status=status,
|
|
1443
|
+
rating=rating,
|
|
1444
|
+
domain=domain.strip() if domain is not None else None,
|
|
1445
|
+
problem=problem.strip() if problem is not None else None,
|
|
1446
|
+
file=file_.strip() if file_ is not None else None,
|
|
1447
|
+
comments=tuple(comments),
|
|
1448
|
+
)
|
|
1449
|
+
|
|
1450
|
+
|
|
1451
|
+
def parse_tptp_problem(
|
|
1452
|
+
text: str, *, base_dir: Optional[str] = None, search_paths=()
|
|
1453
|
+
) -> TptpProblem:
|
|
1454
|
+
"""Parse a whole TPTP problem into its formulas *and* its header metadata.
|
|
1455
|
+
|
|
1456
|
+
Combines :func:`parse_tptp` (formula parsing, delegated to unchanged) with an
|
|
1457
|
+
independent raw-text scan for the standard ``%`` header block (see
|
|
1458
|
+
:func:`_parse_tptp_header`) — the two do not interact, so ``formulas`` here is
|
|
1459
|
+
exactly what :func:`parse_tptp` returns for the same ``text``/``base_dir``/
|
|
1460
|
+
``search_paths`` (same order, same :class:`TptpFormula` records — every
|
|
1461
|
+
``include`` spliced in), just wrapped in a tuple alongside the header.
|
|
1462
|
+
The header scan itself only ever reads ``text``'s OWN ``%`` lines, never
|
|
1463
|
+
an included file's.
|
|
1464
|
+
|
|
1465
|
+
Args:
|
|
1466
|
+
text: the contents of a TPTP problem file.
|
|
1467
|
+
base_dir: see :func:`parse_tptp`; :func:`load_tptp_problem` sets
|
|
1468
|
+
this automatically.
|
|
1469
|
+
search_paths: see :func:`parse_tptp`.
|
|
1470
|
+
|
|
1471
|
+
Returns:
|
|
1472
|
+
A :class:`TptpProblem` with ``formulas`` (in source order) and ``header``.
|
|
1473
|
+
|
|
1474
|
+
Raises:
|
|
1475
|
+
ParsingError: if the text is not a well-formed TPTP problem, or an
|
|
1476
|
+
``include`` cannot be resolved (same conditions as
|
|
1477
|
+
:func:`parse_tptp`; the header scan alone never raises).
|
|
1478
|
+
"""
|
|
1479
|
+
return TptpProblem(
|
|
1480
|
+
formulas=tuple(parse_tptp(text, base_dir=base_dir, search_paths=search_paths)),
|
|
1481
|
+
header=_parse_tptp_header(text),
|
|
1482
|
+
)
|
|
1483
|
+
|
|
1484
|
+
|
|
1485
|
+
def load_tptp_problem(path: str, *, search_paths=()) -> TptpProblem:
|
|
1486
|
+
"""Read a TPTP problem file and :func:`parse_tptp_problem` its contents.
|
|
1487
|
+
|
|
1488
|
+
Mirrors :func:`load_tptp`'s file handling (whole-file read, UTF-8,
|
|
1489
|
+
``base_dir``/cycle-chain seeded from ``path`` itself — see
|
|
1490
|
+
:func:`load_tptp`).
|
|
1491
|
+
|
|
1492
|
+
Args:
|
|
1493
|
+
path: path to a ``.p`` / ``.tptp`` file.
|
|
1494
|
+
search_paths: see :func:`parse_tptp`.
|
|
1495
|
+
|
|
1496
|
+
Returns:
|
|
1497
|
+
A :class:`TptpProblem`.
|
|
1498
|
+
"""
|
|
1499
|
+
items, text = _load_and_resolve(path, search_paths)
|
|
1500
|
+
return TptpProblem(
|
|
1501
|
+
formulas=tuple(_finalize_formulas(items)),
|
|
1502
|
+
header=_parse_tptp_header(text),
|
|
1503
|
+
)
|