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,1153 @@
|
|
|
1
|
+
"""Entailment checking via the Prover9 theorem prover (LADR backend).
|
|
2
|
+
|
|
3
|
+
Builds a Prover9 ``.in`` file (``set(prolog_style_variables)``, one
|
|
4
|
+
``formulas(assumptions)`` list of premises, one ``formulas(goals)`` list
|
|
5
|
+
holding the conclusion) and looks for ``THEOREM PROVED`` in Prover9's stdout.
|
|
6
|
+
|
|
7
|
+
**ASCII/legality sanitisation (problem-level seam).** ``Node.to_prover9()``
|
|
8
|
+
renders a predicate/function name completely verbatim (no transliteration,
|
|
9
|
+
no case change at all) and a Constant name transliterated to ASCII
|
|
10
|
+
(:func:`~unicode_logic_kit.fol._fol_nodes.constant_name_to_ascii`) but without
|
|
11
|
+
fixing a digit-leading result (and, for a name Prover9 would read as a
|
|
12
|
+
variable, in double quotes: see the next section). Prover9's own reader grammar for a symbol
|
|
13
|
+
token is ``NAME: /[A-Za-z_][A-Za-z0-9_]*/`` (see
|
|
14
|
+
:data:`unicode_logic_kit.fol.prover9_input`'s ``NAME`` terminal) — a
|
|
15
|
+
non-ASCII character or a digit-leading name is not a legal token there at
|
|
16
|
+
all. Neither gap could be reached before the toolkit's own identifier
|
|
17
|
+
grammar was widened to accept Unicode letters and digit-leading names; both
|
|
18
|
+
can now. Exactly like :mod:`atp._tptp_problem`, the fix has to live at
|
|
19
|
+
problem level, not inside ``Node.to_prover9()`` itself (see that module's
|
|
20
|
+
docstring for the full reasoning — independent per-node renaming loses
|
|
21
|
+
injectivity and never reaches a caller that needs to invert it):
|
|
22
|
+
:func:`_sanitize_for_prover9` walks every premise and the conclusion
|
|
23
|
+
TOGETHER, replaces only the names that are not already Prover9-legal with
|
|
24
|
+
an ASCII, non-digit-leading, whole-problem-injective replacement, and
|
|
25
|
+
returns a :class:`Prover9NameMap` recording exactly what was renamed. An
|
|
26
|
+
already-legal name — including an upper-case-initial predicate WITH arguments,
|
|
27
|
+
the kit's own ``Human(x)`` convention — passes through completely unchanged,
|
|
28
|
+
with ONE exception, a symbol written in arity-0 position that Prover9 would read
|
|
29
|
+
as a variable (next section: a CONSTANT, and a NULLARY predicate), so this
|
|
30
|
+
sanitisation step never alters the output for a formula this module was already
|
|
31
|
+
exporting correctly. A name that is no word at all (a space, a dot, a hyphen, a
|
|
32
|
+
non-ASCII letter) is replaced by an ASCII word, each character that is no letter,
|
|
33
|
+
digit or underscore by the reversible escape ``uXXXX`` the transliteration of a
|
|
34
|
+
non-ASCII character uses; a name that begins with ``$`` is refused (below).
|
|
35
|
+
|
|
36
|
+
**One name, several symbols (the key is kind, name and arity).** Prover9 keeps ONE
|
|
37
|
+
symbol per spelling, and refuses a file that uses a spelling at two arities, or both
|
|
38
|
+
as a relation and as a function (measured on Prover9 2026-8A: ``The following symbols
|
|
39
|
+
are used with multiple arities: P/2, P/1`` and ``...used as both relation and function
|
|
40
|
+
symbols: p/0``). The kit keeps them apart: ``P(x)`` and ``P(x, y)`` are two predicates, a
|
|
41
|
+
nullary ``p`` a third, the constant ``p`` and the function ``p(x)`` two more, and a
|
|
42
|
+
sort ``S`` is the unary predicate ``S`` (ONE symbol, as everywhere in this kit). The
|
|
43
|
+
writer therefore keys every symbol on ``(kind, name, arity)`` (kind is ``"predicate"``
|
|
44
|
+
or ``"function"``; a constant is a function of arity 0, a proposition a predicate of
|
|
45
|
+
arity 0) and gives the FIRST symbol of a spelling that spelling, the second and every
|
|
46
|
+
later one a replacement that is no other symbol's spelling (``P`` at arity 2 becomes
|
|
47
|
+
``P2``, a numeric suffix as everywhere else). The map records each symbol
|
|
48
|
+
(:attr:`Prover9NameMap.symbols`); a name that has one symbol only is looked up as
|
|
49
|
+
before. The comparison symbols and the arithmetic operators written infix are no
|
|
50
|
+
symbols of this kind, and ``$true`` / ``$false`` as nullary atoms are Prover9's truth
|
|
51
|
+
constants ``$T`` and ``$F``.
|
|
52
|
+
|
|
53
|
+
**Numerals.** A numeral is an uninterpreted constant here, as it is for the Z3 route
|
|
54
|
+
(``Number.to_z3`` names it by its value, so ``⊢ 1 ≠ 2`` is not valid for Z3 and not
|
|
55
|
+
provable for Prover9: Prover9 has no arithmetic either). It is ONE constant per VALUE,
|
|
56
|
+
written in double quotes: ``Number(1)`` and ``Number(1.0)`` are equal nodes and are both
|
|
57
|
+
``"1"``, ``2.5`` is ``"2.5"`` and ``-1`` is ``"-1"`` (see
|
|
58
|
+
:meth:`~unicode_logic_kit.fol._fol_nodes.Number.to_prover9`), so ``P(1) ⊢ P(1.0)`` is
|
|
59
|
+
written ``P("1")`` and ``P("1")``. Every numeral is quoted, a non-negative integer too:
|
|
60
|
+
Mace4, which reads the same formula lists, takes a bare integer for a domain element of its
|
|
61
|
+
own and all of them for distinct elements (``1 != 2`` has no countermodel there), which the
|
|
62
|
+
kit's numerals are not; a quoted symbol is a plain constant for both programs. (Mace4 does not
|
|
63
|
+
read the whole file this writer writes: it stops at ``set(auto_denials)`` and at the
|
|
64
|
+
``clear(print_...)`` flags with "Flag not recognized", measured on Mace4 2026-8A. With those
|
|
65
|
+
lines left out, or with ``set(prolog_style_variables)`` alone before the lists, it reads the
|
|
66
|
+
problem and finds a countermodel of an invalid one. The kit has no Mace4 route.) A number and a
|
|
67
|
+
constant spelled like it (``Number(1)`` and ``Constant('1')``, ``Number(1.0)`` and
|
|
68
|
+
``Constant('1.0')``) are ONE symbol for Z3 and for the TPTP writers, so the pair is
|
|
69
|
+
refused by name here too.
|
|
70
|
+
|
|
71
|
+
**Upper-case constants (the variable rule).** Every file written here sets
|
|
72
|
+
``prolog_style_variables``, under which Prover9 reads a symbol in TERM position
|
|
73
|
+
as a VARIABLE iff it begins with an upper-case letter (LADR ``symbols.c``
|
|
74
|
+
``variable_name``: ``*s >= 'A' && *s <= 'Z'``; the manual: "If this flag is set,
|
|
75
|
+
variables in clauses start with (upper case) 'A' through 'Z'"). That applies to
|
|
76
|
+
every arity-0 symbol in term position, so a :class:`Constant` named ``Gaseous``
|
|
77
|
+
(an ontology individual looks exactly like that) written verbatim makes
|
|
78
|
+
``P(Gaseous)`` read as ``for all X, P(X)`` and the route prove non-theorems.
|
|
79
|
+
:func:`_sanitize_for_prover9` therefore treats such a constant as NOT legal and
|
|
80
|
+
renames it, like a non-ASCII name, to a lower-case-initial token that is
|
|
81
|
+
injective over the whole problem; the rename is recorded in
|
|
82
|
+
:attr:`Prover9NameMap.constants` (and mirrored in ``mapping`` when the name is
|
|
83
|
+
not also a predicate/function name). What is NOT renamed: a predicate or
|
|
84
|
+
function WITH arguments (``Human(x)``, ``Gaseous(c)`` -- the symbol is not a
|
|
85
|
+
constant term, only its arguments are read for variables), so the kit's
|
|
86
|
+
Capitalised predicate convention is untouched, and the same word may be a
|
|
87
|
+
predicate and a constant at once (an OWL pun: class ``Person`` and individual
|
|
88
|
+
``Person``) with the two getting different tokens. A leading underscore is
|
|
89
|
+
treated like an upper-case letter: the LADR source reads only ``A``..``Z`` and
|
|
90
|
+
``_x`` is a constant to Prover9 2026-8A (measured, with the flag and without it), and to this
|
|
91
|
+
kit's own Prover9 reader, but the Prolog convention reads it as a variable, and the rename is
|
|
92
|
+
harmless where Prover9 would not have needed it.
|
|
93
|
+
|
|
94
|
+
A bare NULLARY predicate is the same shape. LADR applies ``set_vars_recurse`` to
|
|
95
|
+
the atom term itself, and a nullary atom IS an arity-0 term, so an upper-case
|
|
96
|
+
``Rain`` is read as a variable too (measured on Prover9 2026-8A: the bare
|
|
97
|
+
``Rain & Wind`` is refused, "cannot be used as atomic formulas, because they are
|
|
98
|
+
variables"). It is renamed by the same mechanism as a constant, to a
|
|
99
|
+
lower-case-initial token that is injective over the whole problem and recorded in
|
|
100
|
+
:attr:`Prover9NameMap.nullary_predicates` (mirrored in ``mapping`` when the word
|
|
101
|
+
has no other role): ``Atom("Rain", [])`` is written ``rain``. A predicate WITH
|
|
102
|
+
arguments (``Rain(x)``) stays as it is. A nullary predicate and a constant of the
|
|
103
|
+
same spelling get two different tokens, because Prover9 keeps one symbol per
|
|
104
|
+
spelling and refuses a file in which it is both a proposition and a constant.
|
|
105
|
+
|
|
106
|
+
The single ``Constant.to_prover9()`` and ``Atom.to_prover9()`` cannot rename (they
|
|
107
|
+
have no view of the other formulas), so they write such a name in double quotes
|
|
108
|
+
instead, ``P("Gaseous")`` and ``"Rain"``: a double-quoted symbol is never a
|
|
109
|
+
variable in Prover9. This writer never reaches that branch, because after the
|
|
110
|
+
rename no constant or proposition is variable-shaped.
|
|
111
|
+
|
|
112
|
+
**``$``-words.** A name that begins with ``$`` (a predicate, a function, a constant
|
|
113
|
+
or a sort) is refused by name: Prover9 keeps those words for itself (``$T``, ``$F``,
|
|
114
|
+
``$ANSWER``), so a symbol spelled like one is not a name of the user's.
|
|
115
|
+
|
|
116
|
+
**The quantifier words.** ``all`` and ``exists`` are not names either. LADR reads
|
|
117
|
+
``exists(X) & ...`` as the quantifier ``exists X`` followed by a stray ``&`` (measured on
|
|
118
|
+
Prover9 2026-8A: it echoes ``(exists W exists W &(Q(c) & R(c)))`` and refuses the file with
|
|
119
|
+
``symbols used with multiple arities: &/1, &/2``), so a sort named ``exists`` (whose guard
|
|
120
|
+
atom is ``exists(X)``), or any predicate or function of that name, is not read as a symbol.
|
|
121
|
+
The writer renames such a symbol like one whose spelling is taken (``exists2``). The single
|
|
122
|
+
renderers write the word in double quotes. ``v``, the third identifier-shaped word of
|
|
123
|
+
Prover9's operator table, is read as an ordinary symbol in every position tried.
|
|
124
|
+
|
|
125
|
+
**Reserved words by arity.** Three more spellings are syntax to LADR or to a reader of its
|
|
126
|
+
files only at ONE number of arguments (measured on Prover9 2026-8A, with a sweep over
|
|
127
|
+
the words of its source): ``if`` with three arguments (LADR reads the first argument as a
|
|
128
|
+
formula, so ``(all W if(W, a, a))`` is refused and ``if(a, b, c)`` makes ``a`` a relation
|
|
129
|
+
symbol), ``end_of_list`` with none (the bare word ends a list) and ``formulas`` with one (it
|
|
130
|
+
is also the header of a list). The writer gives the symbol of such a name and arity a token
|
|
131
|
+
of its own and leaves the same word at another arity alone; the single renderers write it
|
|
132
|
+
in double quotes (:data:`~unicode_logic_kit.fol._fol_nodes._PROVER9_RESERVED_SYMBOLS`).
|
|
133
|
+
|
|
134
|
+
**Counting quantifiers.** ``∃≥n x φ`` is written as ``n`` distinct witnesses, which
|
|
135
|
+
need names. Every name the writer mints for one is fresh against EVERY name of the
|
|
136
|
+
whole problem, of every kind (predicate, function, constant, sort, variable), compared
|
|
137
|
+
case-folded because Prover9 writes a variable in upper case and reads ``X0`` and ``x0`` as
|
|
138
|
+
one (:func:`~unicode_logic_kit.fol._identifiers.symbol_names`), not only against the matrix it
|
|
139
|
+
expands, so that the expansion never rebinds a variable of the formula around it
|
|
140
|
+
(``(all X0 (A(X0) -> (exists X0 ...)))``).
|
|
141
|
+
|
|
142
|
+
**Re-bound binders.** LADR renames a variable that a quantifier binds inside the scope of a
|
|
143
|
+
quantifier of the same name (``(all W (all W P(W, x0)))``), to a symbol it picks itself,
|
|
144
|
+
the first of ``x0``, ``x1``, ... that is no variable in scope; it does not look at the
|
|
145
|
+
constants of the formula, so a constant ``x0`` is then bound by the quantifier (measured
|
|
146
|
+
on Prover9 2026-8A: that formula is clausified to ``P(A, A)``, and the premise
|
|
147
|
+
``∀w ∀w P(w, x0)`` proves ``P(alpha, alpha)``, which it does not entail). The writer
|
|
148
|
+
renames a binder that sits inside the scope of one of its own name itself, to a fresh variable
|
|
149
|
+
(:func:`_rename_rebound_binders`; names are compared as Prover9 reads them, in upper case),
|
|
150
|
+
so LADR has nothing to rename, and keeps the spellings LADR picks (``x`` or ``y`` followed
|
|
151
|
+
by digits, with no leading zero) away from the constants and propositions of the problem
|
|
152
|
+
(:data:`_LADR_MINTED_VARIABLE`: ``x0`` is written ``x0_``). Two quantifiers of one name that
|
|
153
|
+
are siblings are no re-binding, which was measured as well, and are written as they are. The
|
|
154
|
+
single ``Node.to_prover9()`` makes the same renaming of the node it writes, with this very
|
|
155
|
+
function (and the same counting witnesses), so the text it writes means the node as well.
|
|
156
|
+
The Skolem names LADR gives (``c1``, ``f1``, ...) skip every symbol the file has (measured:
|
|
157
|
+
a constant ``c1`` is left alone and the Skolem constant becomes ``c2``), so a user symbol of
|
|
158
|
+
that spelling keeps it.
|
|
159
|
+
|
|
160
|
+
**Free variables.** A free variable of a problem is a PARAMETER: one unknown element, the
|
|
161
|
+
same in every premise and in the conclusion, which is what Z3 and the tableau read and the
|
|
162
|
+
assignment-wise consequence relation of the textbooks (``P(x) ⊢ P(alpha)`` is not valid:
|
|
163
|
+
universe {0, 1}, ``x`` = 1, ``alpha`` = 0, ``P`` = {1}). Prover9 would close each formula
|
|
164
|
+
universally on its own (``P(X)`` is ``∀X P(X)``), which is another question and proves it. The
|
|
165
|
+
writer therefore replaces every free variable, problem-wide, by a constant of a name no other
|
|
166
|
+
symbol of the problem has (the variable's own name when it is free), written like every other
|
|
167
|
+
constant, and records it in :attr:`Prover9NameMap.free_variables` (variable name to token;
|
|
168
|
+
:meth:`Prover9NameMap.reverse` gives the variable's name back). The text written has no free
|
|
169
|
+
variable: its closure is the identity.
|
|
170
|
+
|
|
171
|
+
**Łukasiewicz connectives.** ``p ∨ ¬p`` with the weak disjunction and the Łukasiewicz negation
|
|
172
|
+
is the maximum of ``p`` and ``1 − p``, which is ``1/2`` at ``p = 1/2``: not valid, while its
|
|
173
|
+
classical image is. Their classical collapse is a different logic, so the writer refuses a
|
|
174
|
+
formula that holds one, by name, as a single ``Node.to_prover9()`` does (``to_fol`` collapses
|
|
175
|
+
explicitly, and the result is written as any classical formula).
|
|
176
|
+
|
|
177
|
+
There is currently no Prover9 "detailed" route reading a proof or
|
|
178
|
+
countermodel back out of Prover9's own output (:func:`check_logical_entailment`
|
|
179
|
+
and :class:`~unicode_logic_kit.atp.protocol.Prover9Backend` both report a bare
|
|
180
|
+
``bool``/PROVED-or-UNKNOWN verdict, nothing that carries a Prover9-chosen
|
|
181
|
+
symbol name back to the caller) — so there is no Rückweg to wire up here
|
|
182
|
+
today. :class:`Prover9NameMap` still exists and is still returned by
|
|
183
|
+
:func:`generate_prover9_input_with_mapping` (mirroring
|
|
184
|
+
:mod:`atp._tptp_problem`'s ``..._with_mapping``/plain-wrapper split) so a
|
|
185
|
+
future detailed route has the same reversible mapping available without
|
|
186
|
+
redesigning this module.
|
|
187
|
+
|
|
188
|
+
**A refused problem.** :func:`check_logical_entailment` returns ``False`` for
|
|
189
|
+
every run without a ``THEOREM PROVED`` line, and by default that includes a
|
|
190
|
+
problem Prover9 refused to read (its fatal-error exit, code 1 in the Prover9
|
|
191
|
+
manual) and a binary that could not be started (exit 127 "not found" or 126 "not
|
|
192
|
+
executable", which is what ``wsl.exe <path>`` reports for a path that does not
|
|
193
|
+
exist inside WSL). ``raise_on_rejection=True`` separates the two by raising
|
|
194
|
+
:class:`Prover9Rejected` with Prover9's own message;
|
|
195
|
+
:class:`~unicode_logic_kit.atp.protocol.Prover9Backend` passes it and reports
|
|
196
|
+
the refusal as an ERROR verdict rather than "found no proof". Prover9's output
|
|
197
|
+
is decoded as UTF-8 with undecodable bytes replaced: its fatal message quotes the
|
|
198
|
+
input around the error and can cut a multi-byte character in half, which must
|
|
199
|
+
reach the caller as a refusal with a message, not as a decoding error.
|
|
200
|
+
|
|
201
|
+
**Prover9 inside WSL.** ``use_wsl=True`` (the Prover9 backend reads it from the
|
|
202
|
+
``use_wsl`` option and ``$UFK_PROVER9_WSL=1``, the way the Vampire backend does)
|
|
203
|
+
runs ``wsl.exe <prover9_path> -f <file>``, where ``prover9_path`` is the path
|
|
204
|
+
INSIDE WSL and the Windows temp file is translated with ``wslpath``. The
|
|
205
|
+
translation happens before the timeout window opens: a slow ``wslpath`` is a
|
|
206
|
+
failure to start (``OSError``), never a Prover9 timeout. Prover9 never reads the
|
|
207
|
+
caller's standard input (``stdin`` is the null device): it reads the problem from
|
|
208
|
+
stdin when it is not given ``-f``, and would block on an inherited pipe.
|
|
209
|
+
|
|
210
|
+
**A run the kit stopped.** ``timeout`` (seconds, default 30) is the wall-clock
|
|
211
|
+
budget handed to the subprocess; by default a run it cuts off is ``False`` like
|
|
212
|
+
any run without a proof. ``raise_on_timeout=True`` separates it by raising
|
|
213
|
+
:class:`Prover9TimedOut`, which the backend reports as UNKNOWN / ``"timeout"``
|
|
214
|
+
(its own ``timeout`` argument, in milliseconds, is the budget) instead of "found
|
|
215
|
+
no proof". Prover9's own exit codes 2-7 stay "found no proof": the kit writes no
|
|
216
|
+
``max_seconds`` into the problem, so none of them is the kit's budget.
|
|
217
|
+
|
|
218
|
+
**Many-sorted (MSFOL) soundness.** A sort ``S`` is the extension of the unary
|
|
219
|
+
predicate ``S`` (one symbol), never empty, and a sorted constant ``c:S`` lies in
|
|
220
|
+
it. The writer LOWERS every sorted node first (the auto-reduction
|
|
221
|
+
``fol.nodes.to_fol``: ``∀x:S φ`` is ``∀x (S(x) → φ)``, ``c:S`` is the plain constant
|
|
222
|
+
``c``), which by itself says neither that the sort is non-empty nor that ``c`` is in
|
|
223
|
+
it, so it states both as their own extra lines in ``formulas(assumptions)`` —
|
|
224
|
+
alongside the premises, i.e. Prover9 may assume them freely, exactly what an
|
|
225
|
+
entailment's premise side means; never inside ``formulas(goals)``, and never folded into
|
|
226
|
+
``Node.to_prover9()`` itself, which stays polarity-blind:
|
|
227
|
+
|
|
228
|
+
* ``unicode_logic_kit.fol._msfl_nodes.nonempty_sort_axioms(premises +
|
|
229
|
+
[conclusion])``: one ``∃x S(x)`` per sort;
|
|
230
|
+
* ``unicode_logic_kit.fol._msfl_nodes.sort_membership_axioms`` of the same sentences, one
|
|
231
|
+
atom ``S(c)`` per sorted constant ``c:S``. A sorted and a plain constant of one
|
|
232
|
+
name are one symbol and get one token; a constant with two sorts gets two atoms.
|
|
233
|
+
Without them ``∀x:Human Mortal(x) ⊢ Mortal(socrates:Human)`` is not proved, because
|
|
234
|
+
the plain text forgets that ``socrates`` is a ``Human``.
|
|
235
|
+
|
|
236
|
+
The lowered formulas and these axioms then go through :func:`_sanitize_for_prover9`
|
|
237
|
+
TOGETHER, so a sort name is a name like any other: ``S(x)`` in the guard of a
|
|
238
|
+
quantifier, in an axiom and in a plain atom ``S(c)`` is one symbol, a non-ASCII sort
|
|
239
|
+
is written under an ASCII replacement, a sort named like a constant of the problem
|
|
240
|
+
(``person`` and ``person``) gets the constant another token, and a predicate ``S`` of
|
|
241
|
+
another arity is another symbol. All of this is empty for an unsorted problem, so the
|
|
242
|
+
generated text is byte-identical to before.
|
|
243
|
+
"""
|
|
244
|
+
|
|
245
|
+
import os
|
|
246
|
+
import re
|
|
247
|
+
import subprocess
|
|
248
|
+
import tempfile
|
|
249
|
+
from dataclasses import dataclass, field, replace
|
|
250
|
+
from typing import Any, Dict, List, Tuple, cast
|
|
251
|
+
|
|
252
|
+
from ..fol._fol_nodes import (
|
|
253
|
+
_PROVER9_QUANTIFIERS, _PROVER9_RESERVED_SYMBOLS, _number_text, _prover9_reads_as_variable,
|
|
254
|
+
)
|
|
255
|
+
from ..fol._identifiers import fresh_variables, symbol_names
|
|
256
|
+
from ..fol._msfl_nodes import (
|
|
257
|
+
_LUK_NO_CLASSICAL_EXPORT, _SORTED_NODE_TYPES, LukEquivalence, LukImplication, LukNegation,
|
|
258
|
+
SortedConstant, StrongConjunction, StrongDisjunction, WeakConjunction, WeakDisjunction,
|
|
259
|
+
free_variables, nonempty_sort_axioms, sort_membership_axioms, substitute,
|
|
260
|
+
)
|
|
261
|
+
from ..fol._numeral_symbols import numeral_name
|
|
262
|
+
from ..fol._team_nodes import SlashedExists
|
|
263
|
+
from ..fol._tptp_symbols import check_variable_names, is_tptp_boolean_atom
|
|
264
|
+
from ..fol.nodes import (
|
|
265
|
+
Atom, Constant, Contrast, Count, Function, Measure, Node, Number, And, Quantifier, Variable,
|
|
266
|
+
)
|
|
267
|
+
from ..fol.prover9_input import _RESERVED_SYMBOLS as _READER_RESERVED_SYMBOLS
|
|
268
|
+
from ._ascii_names import ascii_safe_base
|
|
269
|
+
from .vampire_entailment import _to_wsl_path
|
|
270
|
+
|
|
271
|
+
__all__ = ["check_logical_entailment", "Prover9NameMap", "Prover9Rejected",
|
|
272
|
+
"Prover9TimedOut", "generate_prover9_input_with_mapping"]
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
# ---------------------------------------------------------------------------
|
|
276
|
+
# ASCII/legality sanitisation — see the module docstring.
|
|
277
|
+
# ---------------------------------------------------------------------------
|
|
278
|
+
|
|
279
|
+
#: The text the refusals of the writer start with.
|
|
280
|
+
_WRITER = "generate_prover9_input_with_mapping"
|
|
281
|
+
|
|
282
|
+
_PREDICATE = "predicate"
|
|
283
|
+
_FUNCTION = "function"
|
|
284
|
+
|
|
285
|
+
#: One symbol of a problem as the kit sees it: ``(kind, name, arity)``, kind being
|
|
286
|
+
#: ``"predicate"`` or ``"function"`` (a constant is a function of arity 0, a
|
|
287
|
+
#: proposition a predicate of arity 0, a sort the predicate of that name at arity 1).
|
|
288
|
+
Symbol = Tuple[str, str, int]
|
|
289
|
+
|
|
290
|
+
# A raw kit-level name that is ALREADY a legal Prover9 NAME token (matches
|
|
291
|
+
# fol/prover9_input.py's own ``NAME: /[A-Za-z_][A-Za-z0-9_]*/`` terminal) and
|
|
292
|
+
# is pure ASCII (Node.to_prover9 never transliterates a predicate/function
|
|
293
|
+
# name, only a Constant, and even that doesn't fix digit-leading — see the
|
|
294
|
+
# module docstring). Names the widened parser can now produce never contain
|
|
295
|
+
# anything outside unicode letters/digits/underscore/combining marks, so
|
|
296
|
+
# this is the exact complement of "needs a replacement".
|
|
297
|
+
_PROVER9_SAFE_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
|
|
298
|
+
|
|
299
|
+
_NOT_A_WORD_CHARACTER = re.compile(r"[^A-Za-z0-9_]")
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _is_prover9_safe(name: str) -> bool:
|
|
303
|
+
return bool(name) and name.isascii() and bool(_PROVER9_SAFE_RE.fullmatch(name))
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
#: The words LADR reads as SYNTAX and never as a name: the identifier-shaped
|
|
307
|
+
#: members of the reserved set the kit's own Prover9 reader keeps
|
|
308
|
+
#: (``fol.prover9_input._RESERVED_SYMBOLS``: ``all``, ``exists`` and the infix
|
|
309
|
+
#: operator ``v``; the rest of that set is punctuation, which no name can be).
|
|
310
|
+
#: A rename that lands on one of these is a text Prover9 rejects or reads as
|
|
311
|
+
#: something else -- ``Constant("All")`` lower-cases to ``all`` -- so the
|
|
312
|
+
#: renamer treats each as TAKEN and the new token gets its numeric suffix
|
|
313
|
+
#: (``all2``), like any other token that is already in use.
|
|
314
|
+
_PROVER9_KEYWORDS = frozenset(
|
|
315
|
+
symbol for symbol in _READER_RESERVED_SYMBOLS if _PROVER9_SAFE_RE.fullmatch(symbol))
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _lowercase_initial(base: str) -> str:
|
|
319
|
+
"""Force a lowercase-initial result for a SYNTHESISED (previously
|
|
320
|
+
illegal) name — never applied to an already-legal passthrough name, so
|
|
321
|
+
this never touches the kit's own upper-case-initial predicate
|
|
322
|
+
convention (see the module docstring's ``Atom("Rain", [])`` note).
|
|
323
|
+
Lowercase-initial avoids Prover9's own ``prolog_style_variables``
|
|
324
|
+
ambiguity (an upper-case- or underscore-initial symbol reads as a
|
|
325
|
+
VARIABLE) for names this module is choosing fresh, rather than
|
|
326
|
+
reproducing that ambiguity for brand-new tokens nobody has to preserve
|
|
327
|
+
the case of.
|
|
328
|
+
"""
|
|
329
|
+
if base and base[0] == "_":
|
|
330
|
+
# An underscore-initial token is read as a variable too (module
|
|
331
|
+
# docstring, "Upper-case constants").
|
|
332
|
+
return "c" + base
|
|
333
|
+
return base[0].lower() + base[1:] if base else base
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _ascii_token_base(name: str) -> str:
|
|
337
|
+
"""The word a name that is no legal token is replaced by (before the
|
|
338
|
+
lower-casing and the numeric suffix): the name transliterated to ASCII with a
|
|
339
|
+
non-digit start (:func:`~unicode_logic_kit.atp._ascii_names.ascii_safe_base`),
|
|
340
|
+
then every character that is still no letter, digit or underscore (a space, a
|
|
341
|
+
dot, a hyphen) as the reversible ``uXXXX`` escape that transliteration uses for
|
|
342
|
+
a non-ASCII character."""
|
|
343
|
+
return _NOT_A_WORD_CHARACTER.sub(lambda m: "u%04x" % ord(m.group()),
|
|
344
|
+
ascii_safe_base(name, "s"))
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
#: The names LADR gives to the variables it renames itself: ``x0``, ``x1``, ... when it
|
|
348
|
+
#: clausifies a formula that binds a name inside the scope of a binder of the same name
|
|
349
|
+
#: (LADR's ``cnf.c``: ``unique_qvars`` and ``skolem``; the same source has ``y0``, ``y1``,
|
|
350
|
+
#: ... in its ``eliminate_rebinding``, which the clausification of a problem file was
|
|
351
|
+
#: measured not to reach). LADR picks the first such name that is no variable IN SCOPE, and
|
|
352
|
+
#: does not look at the constants of the formula, so a constant (or a proposition) spelled
|
|
353
|
+
#: like one is bound by the quantifier that took the name over (measured on Prover9
|
|
354
|
+
#: 2026-8A: ``(all W (all W P(W, x0)))`` is clausified to ``P(A, A)``, and so is the same
|
|
355
|
+
#: formula with a constant ``x1`` under three nested binders of one name). The writer never
|
|
356
|
+
#: leaves a binder inside the scope of one of the same name (:func:`_rename_rebound_binders`),
|
|
357
|
+
#: so LADR has nothing to rename; the spelling is kept away from the constants and
|
|
358
|
+
#: propositions of the problem all the same, so that no name of the user's is ever the one
|
|
359
|
+
#: LADR would choose. LADR counts from 0 and writes no leading zero. The Skolem names
|
|
360
|
+
#: LADR gives (``c1``, ``c2``, ... and ``f1``, ``f2``, ...) skip every symbol the file
|
|
361
|
+
#: already has, and are not a case of this (measured: a constant ``c1`` of the problem is
|
|
362
|
+
#: left alone and the Skolem constant becomes ``c2``).
|
|
363
|
+
_LADR_MINTED_VARIABLE = re.compile(r"[xy](?:0|[1-9][0-9]*)")
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _is_ladr_minted(name: str) -> bool:
|
|
367
|
+
"""Whether ``name`` is spelled like a variable LADR mints for itself
|
|
368
|
+
(:data:`_LADR_MINTED_VARIABLE`)."""
|
|
369
|
+
return _LADR_MINTED_VARIABLE.fullmatch(name) is not None
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _keeps_spelling(name: str, arity: int) -> bool:
|
|
373
|
+
"""Whether a symbol of this name and arity is written under its own name: it is
|
|
374
|
+
a legal token, not an arity-0 symbol Prover9 would read as a variable, not an
|
|
375
|
+
arity-0 symbol spelled like a variable LADR renames to (:data:`_LADR_MINTED_VARIABLE`),
|
|
376
|
+
not a word reserved at this arity (:data:`_PROVER9_RESERVED_SYMBOLS`: ``if`` with three
|
|
377
|
+
arguments, ``end_of_list`` with none, ``formulas`` with one), and not one of the
|
|
378
|
+
quantifier words ``all`` and ``exists``, which LADR reads as a quantifier whenever the
|
|
379
|
+
symbol stands first and is applied to a variable (so ``exists(X) & ...`` is no atom: the
|
|
380
|
+
guard of a sort named ``exists`` is exactly that)."""
|
|
381
|
+
return (_is_prover9_safe(name) and name not in _PROVER9_QUANTIFIERS
|
|
382
|
+
and (name, arity) not in _PROVER9_RESERVED_SYMBOLS
|
|
383
|
+
and not (arity == 0 and (_prover9_reads_as_variable(name) or _is_ladr_minted(name))))
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _reserve_token(base: str, used: set, arity: int) -> str:
|
|
387
|
+
"""The first token of ``base``, ``base2``, ``base3``, ... that is not in ``used`` and is
|
|
388
|
+
no word reserved at this arity, reserved (the numeric-suffix scheme of
|
|
389
|
+
:func:`~unicode_logic_kit.atp._ascii_names.reserve_rendered`), and, for a symbol of arity 0,
|
|
390
|
+
is not spelled like a variable LADR renames to either (:data:`_LADR_MINTED_VARIABLE`).
|
|
391
|
+
Such a base gets a trailing underscore first (``x0`` becomes ``x0_``, then ``x0_2``):
|
|
392
|
+
every ``x`` or ``y`` followed by digits is that kind of name, so no numeric suffix
|
|
393
|
+
could leave the family."""
|
|
394
|
+
nullary = arity == 0
|
|
395
|
+
if nullary and _is_ladr_minted(base):
|
|
396
|
+
base += "_"
|
|
397
|
+
candidate, index = base, 2
|
|
398
|
+
while candidate in used or (candidate, arity) in _PROVER9_RESERVED_SYMBOLS:
|
|
399
|
+
if nullary and base in ("x", "y"):
|
|
400
|
+
base += "_"
|
|
401
|
+
candidate = base
|
|
402
|
+
continue
|
|
403
|
+
candidate = f"{base}{index}"
|
|
404
|
+
index += 1
|
|
405
|
+
used.add(candidate)
|
|
406
|
+
return candidate
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _symbol_what(kind: str, name: str, arity: int, sorts) -> str:
|
|
410
|
+
"""What a symbol is called in a refusal."""
|
|
411
|
+
if kind == _PREDICATE and arity == 1 and name in sorts:
|
|
412
|
+
return "the sort"
|
|
413
|
+
if arity == 0:
|
|
414
|
+
return "the proposition" if kind == _PREDICATE else "the constant"
|
|
415
|
+
return "the predicate" if kind == _PREDICATE else "the function"
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _dollar_refusal(kind: str, name: str, arity: int, sorts) -> NotImplementedError:
|
|
419
|
+
return NotImplementedError(
|
|
420
|
+
f"{_WRITER}: {_symbol_what(kind, name, arity, sorts)} {name!r} is a '$'-word. "
|
|
421
|
+
"Prover9 keeps the words that begin with '$' for itself (its truth constants are "
|
|
422
|
+
"$T and $F, its answer literal $ANSWER), so a symbol spelled like that is not a "
|
|
423
|
+
"name of the user's and would be read as one of those. The nullary atoms $true "
|
|
424
|
+
"and $false are the truth constants and are written $T and $F; rename any other "
|
|
425
|
+
"symbol before exporting this problem.")
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def _comparison_refusal(name: str, arity: int) -> NotImplementedError:
|
|
429
|
+
return NotImplementedError(
|
|
430
|
+
f"{_WRITER}: the comparison {name!r} is written infix and takes exactly two "
|
|
431
|
+
f"arguments, but it is applied to {arity}; Prover9 has no other reading of it. "
|
|
432
|
+
"Rename the predicate before exporting this problem.")
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def _numeral_clash_refusal(text: str, name: str) -> NotImplementedError:
|
|
436
|
+
return NotImplementedError(
|
|
437
|
+
f"{_WRITER}: the number {text} and the constant {name!r} are spelled alike: they "
|
|
438
|
+
"are ONE symbol for the Z3 route (and the TPTP writers refuse the pair), and this "
|
|
439
|
+
"writer would write them as two, a number as its value in double quotes and "
|
|
440
|
+
"a constant renamed to a word, so the problem would not mean what the formulas "
|
|
441
|
+
"say. Rename the constant before exporting this problem.")
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
@dataclass
|
|
445
|
+
class Prover9NameMap:
|
|
446
|
+
"""The renamings :func:`_sanitize_for_prover9` chose for one problem.
|
|
447
|
+
|
|
448
|
+
The key of a symbol is ``(kind, name, arity)`` (see the module docstring):
|
|
449
|
+
:attr:`symbols` maps every symbol of the problem to the token it is written
|
|
450
|
+
as. Prover9 keeps ONE symbol per spelling, so the first symbol of a spelling
|
|
451
|
+
keeps it and a later one with the same name (another arity, or a predicate
|
|
452
|
+
next to a function or a constant) gets a token that no other symbol has. A
|
|
453
|
+
SINGLE flat namespace across predicate/function/constant names (unlike
|
|
454
|
+
:mod:`atp._tptp_problem`'s predicate-vs-term split): Prover9's own export
|
|
455
|
+
never case-folds, so an already-legal name can never collide with another
|
|
456
|
+
already-legal name the way TPTP's first-letter fold can, and being
|
|
457
|
+
conservative about a SYNTHESISED replacement never colliding with ANY other
|
|
458
|
+
name in the problem — predicate, function, or constant alike — costs nothing
|
|
459
|
+
but an occasional extra numeric suffix. ``mapping`` is the original-kit-name ->
|
|
460
|
+
token dict (for a name with several symbols, the token of the first one that was
|
|
461
|
+
not renamed because of the variable rule); an original name that was already
|
|
462
|
+
legal maps to itself.
|
|
463
|
+
"""
|
|
464
|
+
|
|
465
|
+
mapping: Dict[str, str] = field(default_factory=dict)
|
|
466
|
+
used: set = field(default_factory=set)
|
|
467
|
+
#: Constants whose own name Prover9 would read as a VARIABLE (upper-case or
|
|
468
|
+
#: underscore initial, see the module docstring), original name -> token.
|
|
469
|
+
#: Kept apart from ``mapping`` because the same word can also be a
|
|
470
|
+
#: predicate/function name, which is NOT renamed; ``mapping`` carries the
|
|
471
|
+
#: same entry too whenever the word has no such second role.
|
|
472
|
+
constants: Dict[str, str] = field(default_factory=dict)
|
|
473
|
+
#: NULLARY predicates whose own name Prover9 would read as a VARIABLE (the
|
|
474
|
+
#: same rule: an atom with no arguments is an arity-0 term), original name ->
|
|
475
|
+
#: token. Kept apart from ``mapping`` and from ``constants`` for the same
|
|
476
|
+
#: reason: ``Rain`` may also be a predicate WITH arguments (not renamed) and a
|
|
477
|
+
#: constant (renamed to its own token); ``mapping`` carries the same entry
|
|
478
|
+
#: whenever the word has no other role.
|
|
479
|
+
nullary_predicates: Dict[str, str] = field(default_factory=dict)
|
|
480
|
+
#: Every symbol of the problem, ``(kind, name, arity)`` -> the token it is
|
|
481
|
+
#: written as, in the order the symbols were first met.
|
|
482
|
+
symbols: Dict[Symbol, str] = field(default_factory=dict)
|
|
483
|
+
#: Every numeral of the problem, the text of its value (``2.5``; ``1`` for both
|
|
484
|
+
#: ``Number(1)`` and ``Number(1.0)``) -> the text it is written as (``'"2.5"'``).
|
|
485
|
+
numerals: Dict[str, str] = field(default_factory=dict)
|
|
486
|
+
#: Every FREE variable of the problem, its name -> the token of the constant it is
|
|
487
|
+
#: written as (see the module docstring, "Free variables"). Empty for a problem
|
|
488
|
+
#: whose formulas are all closed.
|
|
489
|
+
free_variables: Dict[str, str] = field(default_factory=dict)
|
|
490
|
+
_seen: Dict[Symbol, None] = field(default_factory=dict)
|
|
491
|
+
_claimed: Dict[str, Symbol] = field(default_factory=dict)
|
|
492
|
+
_numeral_texts: set = field(default_factory=set)
|
|
493
|
+
_sorts: frozenset = frozenset()
|
|
494
|
+
|
|
495
|
+
def collect_symbol(self, kind: str, name: str, arity: int) -> None:
|
|
496
|
+
"""First pass: register the symbol ``(kind, name, arity)``. A symbol that is
|
|
497
|
+
written under its own name keeps it if no earlier symbol has it (order-independent
|
|
498
|
+
for the renamed ones — see :class:`~atp._tptp_problem._Renamer`'s docstring: the
|
|
499
|
+
name is reserved immediately, so that a synthesised token never takes it); a
|
|
500
|
+
symbol of an already claimed spelling, an illegal name, or an arity-0 name that
|
|
501
|
+
Prover9 would read as a variable waits for :meth:`finalize`.
|
|
502
|
+
|
|
503
|
+
Raises:
|
|
504
|
+
NotImplementedError: the name begins with ``$``."""
|
|
505
|
+
if name.startswith("$"):
|
|
506
|
+
raise _dollar_refusal(kind, name, arity, self._sorts)
|
|
507
|
+
key = (kind, name, arity)
|
|
508
|
+
if key in self._seen:
|
|
509
|
+
return
|
|
510
|
+
self._seen[key] = None
|
|
511
|
+
if _keeps_spelling(name, arity) and name not in self._claimed:
|
|
512
|
+
self._claimed[name] = key
|
|
513
|
+
self.used.add(name)
|
|
514
|
+
self.symbols[key] = name
|
|
515
|
+
|
|
516
|
+
def collect_numeral(self, value) -> None:
|
|
517
|
+
"""First pass: register the numeral ``value`` (written as
|
|
518
|
+
:meth:`~unicode_logic_kit.fol._fol_nodes.Number.to_prover9` writes it): one entry per
|
|
519
|
+
VALUE, so ``1`` and ``1.0`` are one numeral. The texts a constant must not carry
|
|
520
|
+
(they would be this symbol under another spelling) are the name of the value, the
|
|
521
|
+
text the node prints and ``str`` of the value."""
|
|
522
|
+
key = numeral_name(value)
|
|
523
|
+
self._numeral_texts.update((key, _number_text(value), str(value)))
|
|
524
|
+
self.numerals.setdefault(key, Number(value).to_prover9())
|
|
525
|
+
|
|
526
|
+
def collect(self, name: str, constant: bool = False, nullary: bool = False) -> None:
|
|
527
|
+
"""First pass for ONE name: a constant (``constant=True``), a nullary
|
|
528
|
+
predicate (``nullary=True``) or, by default, a predicate with arguments.
|
|
529
|
+
:meth:`collect_symbol` is the form that carries the kind and the arity."""
|
|
530
|
+
if constant:
|
|
531
|
+
self.collect_symbol(_FUNCTION, name, 0)
|
|
532
|
+
elif nullary:
|
|
533
|
+
self.collect_symbol(_PREDICATE, name, 0)
|
|
534
|
+
else:
|
|
535
|
+
self.collect_symbol(_PREDICATE, name, 1)
|
|
536
|
+
|
|
537
|
+
def reserve_sort(self, sort: str) -> None:
|
|
538
|
+
"""Keep every synthesised token off the name of a sort of the problem. A sort is
|
|
539
|
+
the unary predicate of its name and is collected as that symbol, which reserves
|
|
540
|
+
its name; this is for a caller that knows a sort name and not the atom."""
|
|
541
|
+
if _is_prover9_safe(sort):
|
|
542
|
+
self.used.add(sort)
|
|
543
|
+
|
|
544
|
+
def finalize(self) -> None:
|
|
545
|
+
"""Second pass: give a token to every symbol that is not written under its own
|
|
546
|
+
name, now that every legal name of the WHOLE problem is reserved (and the LADR
|
|
547
|
+
keywords, which no name may be: :data:`_PROVER9_KEYWORDS`).
|
|
548
|
+
|
|
549
|
+
Raises:
|
|
550
|
+
NotImplementedError: a number and a constant are spelled alike."""
|
|
551
|
+
for kind, name, arity in self._seen:
|
|
552
|
+
if kind == _FUNCTION and arity == 0 and name in self._numeral_texts:
|
|
553
|
+
raise _numeral_clash_refusal(name, name)
|
|
554
|
+
self.used.update(_PROVER9_KEYWORDS)
|
|
555
|
+
illegal: List[Symbol] = []
|
|
556
|
+
variable_like: List[Symbol] = []
|
|
557
|
+
nullary_like: List[Symbol] = []
|
|
558
|
+
clashing: List[Symbol] = []
|
|
559
|
+
for key in self._seen:
|
|
560
|
+
if key in self.symbols:
|
|
561
|
+
continue
|
|
562
|
+
kind, name, arity = key
|
|
563
|
+
if not _is_prover9_safe(name):
|
|
564
|
+
illegal.append(key)
|
|
565
|
+
elif arity == 0 and _prover9_reads_as_variable(name):
|
|
566
|
+
(variable_like if kind == _FUNCTION else nullary_like).append(key)
|
|
567
|
+
else:
|
|
568
|
+
clashing.append(key)
|
|
569
|
+
for key in illegal:
|
|
570
|
+
self.symbols[key] = _reserve_token(
|
|
571
|
+
_lowercase_initial(_ascii_token_base(key[1])), self.used, key[2])
|
|
572
|
+
for key in variable_like + nullary_like:
|
|
573
|
+
self.symbols[key] = _reserve_token(_lowercase_initial(key[1]), self.used, key[2])
|
|
574
|
+
for key in clashing:
|
|
575
|
+
self.symbols[key] = _reserve_token(key[1], self.used, key[2])
|
|
576
|
+
# The views by name, in the order of first occurrence.
|
|
577
|
+
first_plain: Dict[str, str] = {}
|
|
578
|
+
first_any: Dict[str, str] = {}
|
|
579
|
+
for key in self._seen:
|
|
580
|
+
kind, name, arity = key
|
|
581
|
+
token = self.symbols[key]
|
|
582
|
+
first_any.setdefault(name, token)
|
|
583
|
+
if arity == 0 and _is_prover9_safe(name) and _prover9_reads_as_variable(name):
|
|
584
|
+
(self.constants if kind == _FUNCTION else self.nullary_predicates)[name] = token
|
|
585
|
+
else:
|
|
586
|
+
first_plain.setdefault(name, token)
|
|
587
|
+
for name, token in first_any.items():
|
|
588
|
+
self.mapping[name] = first_plain.get(name, token)
|
|
589
|
+
|
|
590
|
+
def get(self, name: str) -> str:
|
|
591
|
+
"""The token for a predicate/function name (or a constant that needed
|
|
592
|
+
no variable-rule rename): the first symbol of that name; see
|
|
593
|
+
:meth:`get_symbol` for one of several."""
|
|
594
|
+
return self.mapping[name]
|
|
595
|
+
|
|
596
|
+
def get_symbol(self, kind: str, name: str, arity: int) -> str:
|
|
597
|
+
"""The token of the symbol ``(kind, name, arity)``."""
|
|
598
|
+
return self.symbols[(kind, name, arity)]
|
|
599
|
+
|
|
600
|
+
def get_constant(self, name: str) -> str:
|
|
601
|
+
"""The token for ``name`` in CONSTANT position (arity-0 term)."""
|
|
602
|
+
return self.symbols[(_FUNCTION, name, 0)]
|
|
603
|
+
|
|
604
|
+
def get_nullary(self, name: str) -> str:
|
|
605
|
+
"""The token for the predicate ``name`` written WITHOUT arguments."""
|
|
606
|
+
return self.symbols[(_PREDICATE, name, 0)]
|
|
607
|
+
|
|
608
|
+
def get_sort(self, sort: str) -> str:
|
|
609
|
+
"""The token for the sort ``sort``: its guard predicate, the unary predicate
|
|
610
|
+
of that name."""
|
|
611
|
+
return self.symbols[(_PREDICATE, sort, 1)]
|
|
612
|
+
|
|
613
|
+
def reverse(self) -> Dict[str, str]:
|
|
614
|
+
"""Flat token -> original dict (Prover9 never case-folds, so the
|
|
615
|
+
token stored in ``symbols`` is exactly what would come back from any
|
|
616
|
+
future reader of Prover9's own output — no fold/cap asymmetry to
|
|
617
|
+
account for, unlike TPTP's predicate namespace). Tokens are unique
|
|
618
|
+
over the whole problem, so the symbols merge without a clash. The token
|
|
619
|
+
of the constant a free variable was replaced by (:attr:`free_variables`) maps
|
|
620
|
+
back to the NAME OF THE VARIABLE, which is what the caller wrote."""
|
|
621
|
+
reverse = {token: name for (_kind, name, _arity), token in self.symbols.items()}
|
|
622
|
+
reverse.update({token: name for name, token in self.free_variables.items()})
|
|
623
|
+
return reverse
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
def _is_builtin_function(node: Function) -> bool:
|
|
627
|
+
"""Whether ``node`` is written with a notation of its own (``(a + b)``, ``-(a)``,
|
|
628
|
+
``-(a, b)``) rather than as a symbol of the problem."""
|
|
629
|
+
return node.name in Function.INFIX_OPS and (
|
|
630
|
+
len(node.args) == 2 or (node.name == "-" and len(node.args) == 1))
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def _is_infix_atom(node: Atom) -> bool:
|
|
634
|
+
return node.predicate in Atom.INFIX_PREDS_P9 and len(node.args) == 2
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def _sanitize_node_for_prover9(node: Node, names: Prover9NameMap) -> Node:
|
|
638
|
+
"""Rebuild ``node`` with every non-Prover9-legal symbol name replaced.
|
|
639
|
+
|
|
640
|
+
Mirrors :func:`atp._tptp_problem._sanitize_node_for_tptp`'s structural
|
|
641
|
+
recursion (``Node.map_children`` for everything that is not itself an
|
|
642
|
+
Atom/Function/Constant); an already-legal name comes back as the exact
|
|
643
|
+
same string, so ``Node.to_prover9()`` on the result is byte-identical to
|
|
644
|
+
``Node.to_prover9()`` on the original wherever every name involved was
|
|
645
|
+
already legal (R1).
|
|
646
|
+
"""
|
|
647
|
+
if isinstance(node, Atom):
|
|
648
|
+
if _is_infix_atom(node) or is_tptp_boolean_atom(node):
|
|
649
|
+
pred = node.predicate
|
|
650
|
+
else:
|
|
651
|
+
pred = names.get_symbol(_PREDICATE, node.predicate, len(node.args))
|
|
652
|
+
return Atom(pred, [_sanitize_node_for_prover9(a, names) for a in node.args])
|
|
653
|
+
if isinstance(node, Function):
|
|
654
|
+
if _is_builtin_function(node):
|
|
655
|
+
name = node.name
|
|
656
|
+
else:
|
|
657
|
+
name = names.get_symbol(_FUNCTION, node.name, len(node.args))
|
|
658
|
+
return Function(name, [_sanitize_node_for_prover9(a, names) for a in node.args])
|
|
659
|
+
if isinstance(node, Constant):
|
|
660
|
+
return Constant(names.get_constant(node.name))
|
|
661
|
+
if isinstance(node, SortedConstant):
|
|
662
|
+
# Renders as the plain Constant of the same name (to_fol), and is that
|
|
663
|
+
# constant: it takes the same token, whatever the name (a variable-shaped,
|
|
664
|
+
# non-ASCII or digit-leading name is renamed exactly like a plain constant),
|
|
665
|
+
# so the premise occurrence and its membership atom are one symbol.
|
|
666
|
+
return SortedConstant(names.get_constant(node.name), names.get_sort(node.sort))
|
|
667
|
+
rebuilt = node.map_children(lambda c: _sanitize_node_for_prover9(c, names))
|
|
668
|
+
if isinstance(node, _SORTED_NODE_TYPES):
|
|
669
|
+
# The sort is the unary predicate of its name, one symbol with the guard it lowers to.
|
|
670
|
+
return replace(cast(Any, rebuilt), sort=names.get_sort(node.sort))
|
|
671
|
+
return rebuilt
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
def _collect_names_for_prover9(node: Node, names: Prover9NameMap) -> None:
|
|
675
|
+
"""First pass (see :meth:`Prover9NameMap.collect_symbol`): register every
|
|
676
|
+
symbol ``node`` (and its descendants) uses, without rewriting anything yet."""
|
|
677
|
+
for n in node.walk():
|
|
678
|
+
if isinstance(n, Atom):
|
|
679
|
+
if _is_infix_atom(n) or is_tptp_boolean_atom(n):
|
|
680
|
+
continue
|
|
681
|
+
if n.predicate in Atom.INFIX_PREDS_P9:
|
|
682
|
+
raise _comparison_refusal(n.predicate, len(n.args))
|
|
683
|
+
names.collect_symbol(_PREDICATE, n.predicate, len(n.args))
|
|
684
|
+
elif isinstance(n, Function):
|
|
685
|
+
if not _is_builtin_function(n):
|
|
686
|
+
names.collect_symbol(_FUNCTION, n.name, len(n.args))
|
|
687
|
+
elif isinstance(n, (Constant, SortedConstant)):
|
|
688
|
+
names.collect_symbol(_FUNCTION, n.name, 0)
|
|
689
|
+
elif isinstance(n, Number):
|
|
690
|
+
names.collect_numeral(n.value)
|
|
691
|
+
if isinstance(n, _SORTED_NODE_TYPES):
|
|
692
|
+
names.collect_symbol(_PREDICATE, n.sort, 1)
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
def _sanitize_for_prover9(formulas: List[Node], sorts=()) -> Tuple[List[Node], Prover9NameMap]:
|
|
696
|
+
"""Sanitise every formula's predicate/function/constant names for Prover9.
|
|
697
|
+
|
|
698
|
+
Returns ``(sanitised_formulas, mapping)`` — see the module docstring.
|
|
699
|
+
The single namespace is shared across ALL of ``formulas``, so the same
|
|
700
|
+
original name maps to the same token everywhere (R2), and a synthesised
|
|
701
|
+
token can never collide with any name anywhere in the problem,
|
|
702
|
+
regardless of where each one appears
|
|
703
|
+
(:meth:`Prover9NameMap.collect_symbol`/:meth:`~Prover9NameMap.finalize`'s
|
|
704
|
+
two-pass split — see :class:`atp._tptp_problem._Renamer`'s docstring for
|
|
705
|
+
why a single combined pass would be order-dependent). ``sorts`` names the
|
|
706
|
+
sorts of the problem, for the wording of a refusal only.
|
|
707
|
+
"""
|
|
708
|
+
names = Prover9NameMap()
|
|
709
|
+
names._sorts = frozenset(sorts)
|
|
710
|
+
for f in formulas:
|
|
711
|
+
_collect_names_for_prover9(f, names)
|
|
712
|
+
names.finalize()
|
|
713
|
+
sanitised = [_sanitize_node_for_prover9(f, names) for f in formulas]
|
|
714
|
+
return sanitised, names
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
_LUKASIEWICZ_NODES = (WeakConjunction, WeakDisjunction, StrongConjunction, StrongDisjunction,
|
|
718
|
+
LukNegation, LukImplication, LukEquivalence)
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
def _refuse_lukasiewicz(formula: Node) -> None:
|
|
722
|
+
"""Refuse a formula that holds a Łukasiewicz connective, by name.
|
|
723
|
+
|
|
724
|
+
Their classical collapse (``to_msfol``) is a different LOGIC, not a spelling of the
|
|
725
|
+
same one: the weak disjunction ``p ∨ ¬p`` is the maximum of ``p`` and ``1 − p``, which is
|
|
726
|
+
``1/2`` at ``p = 1/2``, so it is not valid, while its classical image is. A single
|
|
727
|
+
``Node.to_prover9()`` refuses these nodes for that reason, and so does this writer.
|
|
728
|
+
|
|
729
|
+
Raises:
|
|
730
|
+
NotImplementedError: a Łukasiewicz node occurs in ``formula``.
|
|
731
|
+
"""
|
|
732
|
+
for node in formula.walk():
|
|
733
|
+
if isinstance(node, _LUKASIEWICZ_NODES):
|
|
734
|
+
raise NotImplementedError(
|
|
735
|
+
f"{_WRITER}: {type(node).__name__} — " + _LUK_NO_CLASSICAL_EXPORT)
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
def _fresh_constant_names(names, taken: set) -> Dict[str, str]:
|
|
739
|
+
"""For each variable name in ``names``, a constant name that is no name of ``taken``
|
|
740
|
+
(compared case-folded; the names chosen are added to ``taken``): the variable's own
|
|
741
|
+
name when it is free, else that name with ``_1``, ``_2``, ... appended."""
|
|
742
|
+
chosen: Dict[str, str] = {}
|
|
743
|
+
for name in names:
|
|
744
|
+
candidate, index = name, 1
|
|
745
|
+
while candidate.casefold() in taken:
|
|
746
|
+
candidate = f"{name}_{index}"
|
|
747
|
+
index += 1
|
|
748
|
+
taken.add(candidate.casefold())
|
|
749
|
+
chosen[name] = candidate
|
|
750
|
+
return chosen
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
def _replace_free_variables(formulas: List[Node]) -> Tuple[List[Node], Dict[str, str]]:
|
|
754
|
+
"""``formulas`` with every free variable replaced, problem-wide, by a fresh constant.
|
|
755
|
+
|
|
756
|
+
A free variable of a problem is a PARAMETER of it: one unknown element, the same in
|
|
757
|
+
every premise and in the conclusion (the assignment-wise consequence relation:
|
|
758
|
+
``Γ ⊨ φ`` iff every structure AND assignment that satisfies ``Γ`` satisfies ``φ``), so
|
|
759
|
+
``P(x) ⊢ P(alpha)`` is not valid. Prover9 would read each formula on its own as closed
|
|
760
|
+
universally (``P(X)`` is ``∀X P(X)``), which is another question; and
|
|
761
|
+
``Γ(x) ⊨ φ(x)`` assignment-wise holds iff ``Γ(c) ⊨ φ(c)`` for a constant ``c`` that no
|
|
762
|
+
symbol of the problem is called. The constant of a free variable carries the variable's
|
|
763
|
+
own name when no other symbol of the problem (of any kind, case-folded) has it.
|
|
764
|
+
|
|
765
|
+
Returns the formulas and the variable name -> constant name table (the names are
|
|
766
|
+
the kit's; the writer's own renaming of a constant applies to them afterwards, so a
|
|
767
|
+
free variable ``X`` or ``x0`` is written as a constant Prover9 reads as one).
|
|
768
|
+
"""
|
|
769
|
+
# A slashed quantifier has no classical export and is refused when it is written; a substitution
|
|
770
|
+
# into its slash set would turn it into a plain quantifier first, so a formula that holds one is
|
|
771
|
+
# left as it is.
|
|
772
|
+
free_per_formula = [set() if any(isinstance(n, SlashedExists) for n in f.walk())
|
|
773
|
+
else {v.name for v in free_variables(f) if isinstance(v, Variable)}
|
|
774
|
+
for f in formulas]
|
|
775
|
+
order: List[str] = []
|
|
776
|
+
for formula, free in zip(formulas, free_per_formula):
|
|
777
|
+
for node in formula.walk():
|
|
778
|
+
if isinstance(node, Variable) and node.name in free and node.name not in order:
|
|
779
|
+
order.append(node.name)
|
|
780
|
+
if not order:
|
|
781
|
+
return list(formulas), {}
|
|
782
|
+
# The names that are taken are those of the problem without the free occurrences: a
|
|
783
|
+
# free variable ``x`` is not a reason to call its own constant anything else.
|
|
784
|
+
stand_in = Number(0)
|
|
785
|
+
stripped = []
|
|
786
|
+
for formula, free in zip(formulas, free_per_formula):
|
|
787
|
+
for name in free:
|
|
788
|
+
formula = substitute(formula, Variable(name), stand_in)
|
|
789
|
+
stripped.append(formula)
|
|
790
|
+
constants = _fresh_constant_names(order, set(symbol_names(*stripped, fold=str.casefold)))
|
|
791
|
+
replaced = []
|
|
792
|
+
for formula, free in zip(formulas, free_per_formula):
|
|
793
|
+
for name in sorted(free):
|
|
794
|
+
formula = substitute(formula, Variable(name), Constant(constants[name]))
|
|
795
|
+
replaced.append(formula)
|
|
796
|
+
return replaced, constants
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
def _rename_rebound_binders(node: Node, avoid: set, scope: frozenset = frozenset()) -> Node:
|
|
800
|
+
"""``node`` with every binder that sits inside the scope of a binder of the same name
|
|
801
|
+
renamed to a fresh variable (alpha-conversion).
|
|
802
|
+
|
|
803
|
+
LADR renames such a variable itself, to a symbol it chooses (``x0``, ``x1``, ...,
|
|
804
|
+
:data:`_LADR_MINTED_VARIABLE`), and takes a constant of that spelling for it; a text
|
|
805
|
+
in which no binder is re-bound leaves LADR nothing to choose. Names are compared as
|
|
806
|
+
Prover9 reads them (the upper-case of a variable name is what is written). A binder
|
|
807
|
+
whose name is not in the scope of another stays as it is, so the text of a formula
|
|
808
|
+
without re-bound binders is the text it always was (two sibling quantifiers on one
|
|
809
|
+
name are not re-bound: ``(all X P(X)) & (all X Q(X))``). The new name is one of
|
|
810
|
+
``avoid`` — every name of the whole problem, case-folded — and is added to it.
|
|
811
|
+
|
|
812
|
+
``scope`` is the set of upper-case names that already bind where ``node`` stands (the free
|
|
813
|
+
variables of a formula that Prover9 closes universally). The traversal keeps its own stack
|
|
814
|
+
and carries the renaming that holds below a renamed binder as a table (old name -> new name),
|
|
815
|
+
which is what substituting into the body would do: a formula nested thousands of levels deep
|
|
816
|
+
is renamed as readily as a shallow one, without the recursion limit. The fresh names are
|
|
817
|
+
minted in the order of the text, outermost binder first, left to right.
|
|
818
|
+
"""
|
|
819
|
+
results: List[Node] = []
|
|
820
|
+
work: list = [("enter", node, scope, {})]
|
|
821
|
+
while work:
|
|
822
|
+
step = work.pop()
|
|
823
|
+
if step[0] == "enter":
|
|
824
|
+
_, current, in_scope, renames = step
|
|
825
|
+
if isinstance(current, Quantifier):
|
|
826
|
+
variable, below = current.variable, renames
|
|
827
|
+
if variable.name.upper() in in_scope:
|
|
828
|
+
first = variable.name[:1].lower()
|
|
829
|
+
letter = first if first.isascii() and first.isalpha() else "x"
|
|
830
|
+
fresh = Variable(fresh_variables(1, letter=letter, avoid=avoid)[0])
|
|
831
|
+
avoid.add(fresh.name.casefold())
|
|
832
|
+
below = {**renames, variable.name: fresh.name}
|
|
833
|
+
variable = fresh
|
|
834
|
+
work.append(("binder", current.type, variable))
|
|
835
|
+
work.append(("enter", current.formula, in_scope | {variable.name.upper()}, below))
|
|
836
|
+
elif isinstance(current, Variable):
|
|
837
|
+
results.append(Variable(renames[current.name]) if current.name in renames else current)
|
|
838
|
+
else:
|
|
839
|
+
children = current._child_nodes()
|
|
840
|
+
work.append(("rebuild", current, len(children)))
|
|
841
|
+
work.extend(("enter", child, in_scope, renames) for child in reversed(children))
|
|
842
|
+
elif step[0] == "binder":
|
|
843
|
+
results.append(Quantifier(step[1], step[2], results.pop()))
|
|
844
|
+
else:
|
|
845
|
+
_, current, count = step
|
|
846
|
+
rebuilt = iter(results[len(results) - count:])
|
|
847
|
+
del results[len(results) - count:]
|
|
848
|
+
results.append(current.map_children(lambda child: next(rebuilt)))
|
|
849
|
+
return results[0]
|
|
850
|
+
|
|
851
|
+
|
|
852
|
+
def _expand_for_prover9(node: Node, avoid: set) -> Node:
|
|
853
|
+
"""The reduction :func:`~unicode_logic_kit.fol._msfl_nodes._reduce_nl_nodes` makes,
|
|
854
|
+
with the counting witnesses minted fresh against ``avoid`` (every name of the whole
|
|
855
|
+
problem, case-folded: Prover9 writes a variable in upper case, so ``X0`` and ``x0`` are
|
|
856
|
+
one; the names minted are added to it), and a ``Measure`` as the binary function
|
|
857
|
+
``measure`` it is written as, so that it is a symbol of the problem like any other."""
|
|
858
|
+
node = node.map_children(lambda c: _expand_for_prover9(c, avoid))
|
|
859
|
+
if isinstance(node, Contrast):
|
|
860
|
+
return And(node.left, node.right)
|
|
861
|
+
if isinstance(node, Count):
|
|
862
|
+
return node._expand(avoid)
|
|
863
|
+
if isinstance(node, Measure):
|
|
864
|
+
return Function("measure", (node.entity, node.dimension))
|
|
865
|
+
return node
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
def _lower_for_prover9(node: Node, avoid: set) -> Node:
|
|
869
|
+
"""``node`` as plain first-order logic: ``to_fol``'s reduction (sorted quantifiers
|
|
870
|
+
guarded, sorted constants plain), with the counting witnesses of
|
|
871
|
+
:func:`_expand_for_prover9`. The sort facts are NOT part of it (they are the
|
|
872
|
+
problem's own assumptions)."""
|
|
873
|
+
return _expand_for_prover9(node.to_msfol()._relativize([]), avoid)
|
|
874
|
+
|
|
875
|
+
|
|
876
|
+
def generate_prover9_input_with_mapping(premises: List[Node], conclusion: Node
|
|
877
|
+
) -> Tuple[str, Prover9NameMap]:
|
|
878
|
+
"""Like :func:`_generate_prover9_input`, but also returns the
|
|
879
|
+
:class:`Prover9NameMap` recording every ASCII-legality rename applied.
|
|
880
|
+
|
|
881
|
+
No current caller in this module reads a Prover9-chosen symbol name back
|
|
882
|
+
out of its output (see the module docstring), but this is the function
|
|
883
|
+
a future "detailed" Prover9 route would call instead of
|
|
884
|
+
:func:`_generate_prover9_input`, exactly the way
|
|
885
|
+
:mod:`atp.vampire_entailment`'s detailed route uses
|
|
886
|
+
:func:`atp._tptp_problem.generate_tptp_problem_with_mapping`.
|
|
887
|
+
|
|
888
|
+
Raises:
|
|
889
|
+
NotImplementedError: two variables of ONE formula that Prover9 reads as one
|
|
890
|
+
(``x`` and ``X`` are both written ``X``: ``∀x ∃X R(x, X)`` would be
|
|
891
|
+
``(all X (exists X R(X, X)))``). The check is the one the TPTP writers
|
|
892
|
+
use (:func:`unicode_logic_kit.fol._tptp_symbols.check_variable_names`), and
|
|
893
|
+
it refuses every such pair of one formula, a harmless one too (two
|
|
894
|
+
quantifiers side by side, ``(∀x P(x)) ∧ (∀X Q(X))``). A single
|
|
895
|
+
``Node.to_prover9()`` has no whole-problem view: it renames a binder inside
|
|
896
|
+
the scope of another (``∀x ∃X R(x, X)`` is written ``(all X (exists X0 R(X, X0)))``),
|
|
897
|
+
and refuses by name only the pairs that no renaming of a binder repairs.
|
|
898
|
+
A symbol named with a ``$`` (Prover9's own words), a comparison symbol
|
|
899
|
+
at other than two arguments, a number spelled like a constant of the
|
|
900
|
+
problem, and a Łukasiewicz connective (it has no classical reading) are
|
|
901
|
+
refused too (see the module docstring).
|
|
902
|
+
|
|
903
|
+
A free variable of the problem is written as a constant, the same in every formula, and
|
|
904
|
+
recorded in :attr:`Prover9NameMap.free_variables`; a binder inside the scope of a binder
|
|
905
|
+
of its own name is renamed (see the module docstring).
|
|
906
|
+
"""
|
|
907
|
+
originals = list(premises) + [conclusion]
|
|
908
|
+
for formula in originals:
|
|
909
|
+
_refuse_lukasiewicz(formula)
|
|
910
|
+
check_variable_names(formula, where=_WRITER, subject="problem", dialect="prover9")
|
|
911
|
+
|
|
912
|
+
# A free variable is one unknown element of the whole problem: a constant (see
|
|
913
|
+
# _replace_free_variables), before anything else reads the formulas.
|
|
914
|
+
originals, free_constants = _replace_free_variables(originals)
|
|
915
|
+
|
|
916
|
+
# Sorted nodes are lowered FIRST: the guard images and the sort facts are plain
|
|
917
|
+
# first-order sentences, and go through the same sanitiser as every other symbol.
|
|
918
|
+
nonempty = list(nonempty_sort_axioms(*originals))
|
|
919
|
+
membership = list(sort_membership_axioms(*originals))
|
|
920
|
+
# Every name of the whole problem, of every kind, case-folded (Prover9 reads ``x0`` and
|
|
921
|
+
# ``X0`` as one variable): what the witnesses of a counting quantifier and the fresh
|
|
922
|
+
# binders below must differ from.
|
|
923
|
+
avoid = set(symbol_names(*originals, *nonempty, *membership, fold=str.casefold))
|
|
924
|
+
lowered = [_lower_for_prover9(f, avoid) for f in originals]
|
|
925
|
+
# No binder may sit inside the scope of one of its own name (see _rename_rebound_binders).
|
|
926
|
+
# The sort facts are one binder each and are written as they are.
|
|
927
|
+
lowered = [_rename_rebound_binders(f, avoid) for f in lowered]
|
|
928
|
+
sorts = {n.sort for f in originals for n in f.walk() if isinstance(n, _SORTED_NODE_TYPES)}
|
|
929
|
+
sanitised, mapping = _sanitize_for_prover9(lowered + nonempty + membership, sorts=sorts)
|
|
930
|
+
mapping.free_variables = {
|
|
931
|
+
name: mapping.symbols[(_FUNCTION, constant, 0)]
|
|
932
|
+
for name, constant in free_constants.items()
|
|
933
|
+
if (_FUNCTION, constant, 0) in mapping.symbols}
|
|
934
|
+
count = len(premises)
|
|
935
|
+
sanitised_premises, sanitised_conclusion = sanitised[:count], sanitised[count]
|
|
936
|
+
sanitised_nonempty = sanitised[count + 1:count + 1 + len(nonempty)]
|
|
937
|
+
sanitised_membership = sanitised[count + 1 + len(nonempty):]
|
|
938
|
+
|
|
939
|
+
lines = []
|
|
940
|
+
lines.append("set(prolog_style_variables).")
|
|
941
|
+
lines.append("set(auto_denials).")
|
|
942
|
+
lines.append("clear(print_initial_clauses).")
|
|
943
|
+
lines.append("clear(print_kept).")
|
|
944
|
+
lines.append("clear(print_given).")
|
|
945
|
+
lines.append("")
|
|
946
|
+
|
|
947
|
+
lines.append("formulas(assumptions).")
|
|
948
|
+
for premise in sanitised_premises:
|
|
949
|
+
lines.append(f" {premise.to_prover9()}.")
|
|
950
|
+
# Many-sorted background facts — see the module docstring: non-emptiness of every
|
|
951
|
+
# sort, and the membership S(c) of every sorted constant. Both are built from the
|
|
952
|
+
# ORIGINAL sentences and sanitised with the rest, so the sort predicate and the
|
|
953
|
+
# constant of each fact are the very tokens the premises use.
|
|
954
|
+
for axiom in sanitised_nonempty:
|
|
955
|
+
lines.append(f" {axiom.to_prover9()}.")
|
|
956
|
+
for fact in sanitised_membership:
|
|
957
|
+
lines.append(f" {fact.to_prover9()}.")
|
|
958
|
+
lines.append("end_of_list.")
|
|
959
|
+
lines.append("")
|
|
960
|
+
|
|
961
|
+
lines.append("formulas(goals).")
|
|
962
|
+
lines.append(f" {sanitised_conclusion.to_prover9()}.")
|
|
963
|
+
lines.append("end_of_list.")
|
|
964
|
+
|
|
965
|
+
return "\n".join(lines), mapping
|
|
966
|
+
|
|
967
|
+
|
|
968
|
+
def _generate_prover9_input(premises: List[Node], conclusion: Node) -> str:
|
|
969
|
+
"""
|
|
970
|
+
Generates a Prover9 input string from given premises and conclusion.
|
|
971
|
+
|
|
972
|
+
Every predicate/function/constant name is first made Prover9-ASCII-legal
|
|
973
|
+
(see the module docstring's sanitisation section) — a name that was
|
|
974
|
+
already legal renders byte-identically to before that step existed.
|
|
975
|
+
|
|
976
|
+
Args:
|
|
977
|
+
premises (list[Node]): List of premise formulas in FOL.
|
|
978
|
+
conclusion (Node): Conclusion formula in FOL.
|
|
979
|
+
|
|
980
|
+
Returns:
|
|
981
|
+
str: Formatted Prover9 input string.
|
|
982
|
+
"""
|
|
983
|
+
text, _mapping = generate_prover9_input_with_mapping(premises, conclusion)
|
|
984
|
+
return text
|
|
985
|
+
|
|
986
|
+
|
|
987
|
+
#: Prover9's documented exit code for "a fatal error occurred (the user's syntax
|
|
988
|
+
#: error, or Prover9's own bug)" -- LADR/Prover9 manual, "Exit codes". The other
|
|
989
|
+
#: codes are ordinary ends of a search: 2 the sos list ran empty, 3 max_megs,
|
|
990
|
+
#: 4 max_seconds, 5 max_given, 6 max_kept, 7 an action, and 0 a proof.
|
|
991
|
+
_PROVER9_FATAL_EXIT = 1
|
|
992
|
+
|
|
993
|
+
#: The shell's own exit codes for "the command was found but cannot run" (126)
|
|
994
|
+
#: and "no such command" (127). Prover9 never exits with them. Behind
|
|
995
|
+
#: ``wsl.exe <path> ...`` a path that does not exist inside WSL ends the run this
|
|
996
|
+
#: way, with the shell's message and no Prover9 output at all: that is a binary
|
|
997
|
+
#: that was never started, not a search that ended without a proof.
|
|
998
|
+
_SHELL_NOT_STARTED_EXITS = frozenset({126, 127})
|
|
999
|
+
|
|
1000
|
+
|
|
1001
|
+
class Prover9Rejected(RuntimeError):
|
|
1002
|
+
"""Prover9 refused to read the problem (or died) instead of searching.
|
|
1003
|
+
|
|
1004
|
+
Raised by :func:`check_logical_entailment` ONLY when called with
|
|
1005
|
+
``raise_on_rejection=True``; the default keeps the historic contract of
|
|
1006
|
+
returning ``False``. ``returncode`` is Prover9's exit code and ``output``
|
|
1007
|
+
its own message (stderr, else stdout), so the caller can quote it.
|
|
1008
|
+
"""
|
|
1009
|
+
|
|
1010
|
+
def __init__(self, returncode: int, output: str):
|
|
1011
|
+
super().__init__(f"Prover9 exited with code {returncode}: {output.strip()[:300]}")
|
|
1012
|
+
self.returncode = returncode
|
|
1013
|
+
self.output = output
|
|
1014
|
+
|
|
1015
|
+
|
|
1016
|
+
class Prover9TimedOut(RuntimeError):
|
|
1017
|
+
"""The kit stopped Prover9 because its wall-clock budget ran out.
|
|
1018
|
+
|
|
1019
|
+
Raised by :func:`check_logical_entailment` ONLY when called with
|
|
1020
|
+
``raise_on_timeout=True``; the default keeps the historic contract of
|
|
1021
|
+
returning ``False``. ``timeout`` is the budget in seconds.
|
|
1022
|
+
"""
|
|
1023
|
+
|
|
1024
|
+
def __init__(self, timeout: float):
|
|
1025
|
+
super().__init__(f"Prover9 was stopped after its {timeout:g}-second budget "
|
|
1026
|
+
"ran out; no proof was found in that time")
|
|
1027
|
+
self.timeout = timeout
|
|
1028
|
+
|
|
1029
|
+
|
|
1030
|
+
def _prover9_rejected(returncode: int, stdout: str, stderr: str) -> bool:
|
|
1031
|
+
"""True iff this exit is a refusal, not the end of a search: Prover9's
|
|
1032
|
+
fatal-error exit, or the shell's "cannot run the binary" exit (126, 127)."""
|
|
1033
|
+
return (returncode == _PROVER9_FATAL_EXIT or returncode in _SHELL_NOT_STARTED_EXITS
|
|
1034
|
+
or "Fatal error" in stdout or "Fatal error" in stderr)
|
|
1035
|
+
|
|
1036
|
+
|
|
1037
|
+
def _prover9_command(prover9_path: str, problem_file: str, use_wsl: bool) -> list:
|
|
1038
|
+
"""The command line that runs Prover9 on ``problem_file``.
|
|
1039
|
+
|
|
1040
|
+
Natively ``[prover9_path, "-f", problem_file]``. With ``use_wsl`` it is
|
|
1041
|
+
``["wsl.exe", prover9_path, "-f", <wslpath of problem_file>]``: the path of the
|
|
1042
|
+
binary is the one INSIDE WSL, and the Windows temp file is translated to its
|
|
1043
|
+
``/mnt/...`` form so that the Linux Prover9 can read it. The translation runs a
|
|
1044
|
+
process of its own, and a timeout of THAT process is no Prover9 timeout, so the
|
|
1045
|
+
caller builds the command before it opens the Prover9 time window.
|
|
1046
|
+
|
|
1047
|
+
Raises:
|
|
1048
|
+
OSError: ``wsl.exe`` cannot be started, does not answer, or cannot translate
|
|
1049
|
+
the path; the Prover9 backend reports it as an error verdict.
|
|
1050
|
+
"""
|
|
1051
|
+
if not use_wsl:
|
|
1052
|
+
return [prover9_path, "-f", problem_file]
|
|
1053
|
+
try:
|
|
1054
|
+
wsl_file = _to_wsl_path(problem_file)
|
|
1055
|
+
except subprocess.TimeoutExpired:
|
|
1056
|
+
raise OSError("wslpath did not answer within its time limit while translating "
|
|
1057
|
+
f"{problem_file!r}; is WSL available?") from None
|
|
1058
|
+
except RuntimeError as exc:
|
|
1059
|
+
raise OSError(str(exc)) from None
|
|
1060
|
+
return ["wsl.exe", prover9_path, "-f", wsl_file]
|
|
1061
|
+
|
|
1062
|
+
|
|
1063
|
+
def _run_prover9(input: str, prover9_path: str, timeout: int = 30,
|
|
1064
|
+
raise_on_rejection: bool = False,
|
|
1065
|
+
raise_on_timeout: bool = False,
|
|
1066
|
+
use_wsl: bool = False) -> bool:
|
|
1067
|
+
"""Run the prover9 command line tool.
|
|
1068
|
+
|
|
1069
|
+
``raise_on_rejection=False`` (the default) is the historic contract: any
|
|
1070
|
+
run without a ``THEOREM PROVED`` line -- a search that ended without a
|
|
1071
|
+
proof, a timeout, AND a problem Prover9 refused to read -- is ``False``.
|
|
1072
|
+
With ``True`` the last of those raises :class:`Prover9Rejected` instead, so
|
|
1073
|
+
a caller that can report an ERROR does not mistake a refused problem for
|
|
1074
|
+
one that was searched and not proved; a binary that could not be started
|
|
1075
|
+
(the shell's exit 126 or 127) is such a refusal too. ``raise_on_timeout=True``
|
|
1076
|
+
does the same for a run the kit cut off after ``timeout`` seconds: it raises
|
|
1077
|
+
:class:`Prover9TimedOut` instead of reporting ``False``.
|
|
1078
|
+
|
|
1079
|
+
``use_wsl=True`` runs a Linux Prover9 through ``wsl.exe`` (see
|
|
1080
|
+
:func:`_prover9_command`). The problem file is written as UTF-8, Prover9 never
|
|
1081
|
+
sees the caller's standard input, and its output is decoded as UTF-8 with
|
|
1082
|
+
undecodable bytes replaced, because its fatal message can cut a multi-byte
|
|
1083
|
+
character of the input in half.
|
|
1084
|
+
"""
|
|
1085
|
+
|
|
1086
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.in', delete=False,
|
|
1087
|
+
encoding='utf-8', newline='\n') as temp_file:
|
|
1088
|
+
temp_file.write(input)
|
|
1089
|
+
temp_filename = temp_file.name
|
|
1090
|
+
|
|
1091
|
+
try:
|
|
1092
|
+
command = _prover9_command(prover9_path, temp_filename, use_wsl)
|
|
1093
|
+
result = subprocess.run(
|
|
1094
|
+
command,
|
|
1095
|
+
capture_output=True,
|
|
1096
|
+
text=True,
|
|
1097
|
+
encoding='utf-8',
|
|
1098
|
+
errors='replace',
|
|
1099
|
+
stdin=subprocess.DEVNULL,
|
|
1100
|
+
timeout=timeout
|
|
1101
|
+
)
|
|
1102
|
+
success = "THEOREM PROVED" in result.stdout
|
|
1103
|
+
if (not success and raise_on_rejection
|
|
1104
|
+
and _prover9_rejected(result.returncode, result.stdout or "",
|
|
1105
|
+
result.stderr or "")):
|
|
1106
|
+
raise Prover9Rejected(result.returncode,
|
|
1107
|
+
(result.stderr or "").strip() or (result.stdout or ""))
|
|
1108
|
+
except subprocess.TimeoutExpired:
|
|
1109
|
+
if raise_on_timeout:
|
|
1110
|
+
raise Prover9TimedOut(timeout) from None
|
|
1111
|
+
success = False
|
|
1112
|
+
finally:
|
|
1113
|
+
# Always remove the temp file, even when subprocess.run raises (e.g.
|
|
1114
|
+
# FileNotFoundError for a wrong prover9_path); the exception still
|
|
1115
|
+
# propagates to the caller.
|
|
1116
|
+
try:
|
|
1117
|
+
os.unlink(temp_filename)
|
|
1118
|
+
except OSError:
|
|
1119
|
+
pass
|
|
1120
|
+
|
|
1121
|
+
return success
|
|
1122
|
+
|
|
1123
|
+
|
|
1124
|
+
def check_logical_entailment(premises: list[Node], conclusion: Node, prover9_path: str,
|
|
1125
|
+
raise_on_rejection: bool = False,
|
|
1126
|
+
timeout: int = 30,
|
|
1127
|
+
raise_on_timeout: bool = False,
|
|
1128
|
+
use_wsl: bool = False) -> bool:
|
|
1129
|
+
"""Checks if a conclusion entails from the defined premises by using prover9.
|
|
1130
|
+
|
|
1131
|
+
``False`` means "no proof was found" -- including, by default, a problem
|
|
1132
|
+
Prover9 refused to read and a run the kit stopped. ``raise_on_rejection=True``
|
|
1133
|
+
separates the first: a refused problem (Prover9's fatal-error exit, or a
|
|
1134
|
+
binary that could not be started) raises :class:`Prover9Rejected` carrying
|
|
1135
|
+
Prover9's own message, which is what
|
|
1136
|
+
:class:`~unicode_logic_kit.atp.protocol.Prover9Backend` reports as an ERROR
|
|
1137
|
+
verdict instead of an unknown that reads like a timeout.
|
|
1138
|
+
|
|
1139
|
+
``timeout`` is the wall-clock budget in SECONDS (default 30) the subprocess
|
|
1140
|
+
may use; ``raise_on_timeout=True`` separates a run cut off by it by raising
|
|
1141
|
+
:class:`Prover9TimedOut`, which the backend reports as UNKNOWN / ``"timeout"``.
|
|
1142
|
+
|
|
1143
|
+
``use_wsl=True`` runs a Linux Prover9 through ``wsl.exe``; ``prover9_path`` is
|
|
1144
|
+
then the path inside WSL (for example
|
|
1145
|
+
``/mnt/d/prover9/Prover9-LADR-2026-8A/bin/prover9``). See :func:`_run_prover9`.
|
|
1146
|
+
"""
|
|
1147
|
+
|
|
1148
|
+
prover9_input = _generate_prover9_input(premises, conclusion)
|
|
1149
|
+
success = _run_prover9(prover9_input, prover9_path, timeout=timeout,
|
|
1150
|
+
raise_on_rejection=raise_on_rejection,
|
|
1151
|
+
raise_on_timeout=raise_on_timeout,
|
|
1152
|
+
use_wsl=use_wsl)
|
|
1153
|
+
return success
|