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,1385 @@
|
|
|
1
|
+
"""cvc5 as a second, independent SMT decision procedure for classical FOL.
|
|
2
|
+
|
|
3
|
+
cvc5 (BSD-3, https://cvc5.github.io) is a full SMT solver with its own
|
|
4
|
+
quantifier-instantiation engine (E-matching, enumerative and syntax-guided
|
|
5
|
+
instantiation, finite model finding), independent of and often complementary
|
|
6
|
+
to Z3's. This module wires it in as a :class:`~unicode_logic_kit.atp.protocol
|
|
7
|
+
.ProverBackend` (:class:`Cvc5Backend`, registry name ``"cvc5"``) using
|
|
8
|
+
exactly the same classical-FOL fragment Z3 decides in
|
|
9
|
+
:class:`unicode_logic_kit.atp.protocol.Z3Backend`: whatever ``Node.to_z3()``
|
|
10
|
+
can translate (uninterpreted sort + equality, no arithmetic — see
|
|
11
|
+
``fol/_fol_nodes.py``); substructural nodes (linear logic, Lambek calculus)
|
|
12
|
+
reject with ``NotImplementedError`` from ``to_z3()`` itself and are reported
|
|
13
|
+
UNKNOWN/``"unsupported"`` here, never guessed at.
|
|
14
|
+
|
|
15
|
+
Translation route — SMT-LIB2 text, not the pythonic term API
|
|
16
|
+
--------------------------------------------------------------
|
|
17
|
+
cvc5's Python API (1.3.x) builds terms through its own :class:`cvc5.Solver`
|
|
18
|
+
/ ``TermManager``, which do not accept Z3 expressions. Re-walking every kit
|
|
19
|
+
``Node`` a second time against cvc5's term constructors would duplicate the
|
|
20
|
+
entire ``to_z3`` translation and risk it drifting out of sync. Instead this
|
|
21
|
+
backend reuses ``to_z3()`` as already trusted by :class:`Z3Backend`, hands
|
|
22
|
+
the resulting Z3 expression to a throwaway ``z3.Solver`` for canonical
|
|
23
|
+
SMT-LIB2 serialisation (``Solver.to_smt2()`` — sorts, functions and the goal
|
|
24
|
+
all print correctly, including quantifiers), and replays that text into
|
|
25
|
+
cvc5 via ``cvc5.InputParser``. Each parsed command is invoked on the cvc5
|
|
26
|
+
solver immediately (the parser resolves later symbols against earlier
|
|
27
|
+
declarations, so streaming invocation is required — buffering all commands
|
|
28
|
+
before invoking any breaks the sort/symbol lookups); the ``(check-sat)``
|
|
29
|
+
command in the text is skipped and ``Solver.checkSat()`` is called directly
|
|
30
|
+
so a genuine :class:`cvc5.Result` (not a string) drives the verdict.
|
|
31
|
+
|
|
32
|
+
Validity is asked as an UNSAT question, mirroring Z3Backend: ``unsat`` on
|
|
33
|
+
``¬((⋀ premises) → φ)`` proves the entailment; ``sat`` produces a genuine
|
|
34
|
+
countermodel (a best-effort variable/function assignment read back off the
|
|
35
|
+
cvc5 model — one term at a time, so a model cvc5 cannot print for some
|
|
36
|
+
symbol does not blank out the whole witness); ``unknown`` is honestly
|
|
37
|
+
UNKNOWN, with ``reason="timeout"`` iff cvc5's own explanation says the time
|
|
38
|
+
budget (``tlimit-per`` and ``tlimit``, set from the ``timeout`` argument,
|
|
39
|
+
milliseconds) was the cause, else ``"incomplete"`` (quantified UF is
|
|
40
|
+
undecidable in general; cvc5 gave up without exhausting time or hitting a
|
|
41
|
+
bound it can name).
|
|
42
|
+
|
|
43
|
+
Optional dependency: this backend needs ``pip install cvc5`` (extra
|
|
44
|
+
``unicode-logic-kit[cvc5]``). :meth:`Cvc5Backend.available` is pure discovery
|
|
45
|
+
(``importlib.util.find_spec``, no import) so probing it never pays the
|
|
46
|
+
binding's load cost; ``cvc5`` itself is imported lazily inside ``decide()``.
|
|
47
|
+
|
|
48
|
+
**ASCII/legality sanitisation (problem-level seam) — narrower than TPTP's.**
|
|
49
|
+
``Node.to_z3()`` hands a symbol's name to Z3's Python API completely raw —
|
|
50
|
+
no transliteration, no fold — and that is FINE for Z3 itself: a Z3 symbol
|
|
51
|
+
name is an arbitrary Python string, not text that has to satisfy any
|
|
52
|
+
lexical grammar. The gap this module has is specifically in the SMT-LIB2
|
|
53
|
+
TEXT round trip described above (``Solver.to_smt2()`` -> ``InputParser``):
|
|
54
|
+
Z3's own ``to_smt2()`` already pipe-quotes (``|...|``) any name that is not
|
|
55
|
+
already a legal SMT-LIB2 ``simple_symbol`` — verified live, a non-ASCII
|
|
56
|
+
name such as ``świątek`` round-trips through it correctly ALREADY, with no
|
|
57
|
+
help from this module — except for the cases :func:`_is_smtlib_safe` lists: a
|
|
58
|
+
name that is pure ASCII, made only of ``simple_symbol``-legal characters, but
|
|
59
|
+
starts with a DIGIT (``2008SummerOlympics``) or reads as a numeral (``-1``);
|
|
60
|
+
a name that IS one of the SMT-LIB2 ``<reserved>`` words the readers treat
|
|
61
|
+
specially as syntax (``!``, ``_``, ``as``, ``exists``, ``forall``, ``let``,
|
|
62
|
+
``match``, ``par``); a name that begins with ``.`` or ``@`` (reserved for
|
|
63
|
+
the solver) or holds ``|``, ``\\`` or ``'`` (which Z3 prints in a form no
|
|
64
|
+
reader reads back); a name that is one of the names Z3's printer mints for a
|
|
65
|
+
shared sub-term (``$x24``, ``?x10``: :data:`_PRINTER_NAME`), because the
|
|
66
|
+
printer writes ``(let (($x24 ...)) ...)`` without looking at the symbols the
|
|
67
|
+
text declares and shadows a declared symbol of that spelling inside the ``let``
|
|
68
|
+
(the text then says another formula, and cvc5 ends the process on it);
|
|
69
|
+
and a name that is a symbol of an SMT-LIB theory
|
|
70
|
+
(:data:`_SMTLIB_THEORIES`), which cvc5 already knows with a fixed signature
|
|
71
|
+
under the logic ``ALL``. SMT-LIB2 v2.6's grammar (Sec. 3.1) lists five more
|
|
72
|
+
words as ``<reserved>`` (``BINARY``, ``DECIMAL``, ``HEXADECIMAL``,
|
|
73
|
+
``NUMERAL``, ``STRING``) but neither Z3's parser nor cvc5 special-cases any
|
|
74
|
+
of them when they appear as an ordinary declared symbol — verified live, in
|
|
75
|
+
every role this module can emit one (bare declaration, applied as a
|
|
76
|
+
predicate/function head, used as an argument) — so renaming them would
|
|
77
|
+
violate R1 below for no reason. SMT-LIB2's grammar requires a
|
|
78
|
+
``simple_symbol`` to start with a non-digit and to not BE a reserved word,
|
|
79
|
+
so a digit-leading name needs quoting and so do these, but Z3's serialiser
|
|
80
|
+
adds it for none of them — verified live: a function named ``let`` prints as
|
|
81
|
+
the undecorated head of ``(let x)``, which its OWN parser then reads as the
|
|
82
|
+
``let``-BINDING form, not an application of a symbol named ``let``. Either
|
|
83
|
+
way the resulting ``.smt2`` text fails to parse (``z3.parse_smt2_string``
|
|
84
|
+
raises; reproduced live for the digit-leading case, and feeding one such name
|
|
85
|
+
to this backend segfaults the whole process before :meth:`Cvc5Backend.decide`
|
|
86
|
+
ever gets to return an ERROR ``Verdict``, since a native crash is not a Python
|
|
87
|
+
exception ``decide()`` can catch). So, unlike :mod:`atp._tptp_problem` and
|
|
88
|
+
:mod:`atp.prover9_entailment` (which must fix BOTH non-ASCII and
|
|
89
|
+
digit-leading names — Vampire/E/Prover9 have no automatic quoting of their
|
|
90
|
+
own), :func:`_sanitize_for_smtlib` only ever touches a name that
|
|
91
|
+
:func:`_is_smtlib_safe` refuses; every other name, including every non-ASCII
|
|
92
|
+
one, is left completely untouched — touching one would change the export
|
|
93
|
+
for a name this backend already handles correctly today, which R1
|
|
94
|
+
forbids. The one thing that is renamed although the name is legal is the
|
|
95
|
+
SECOND symbol of a name — ``P`` at two arities, a predicate and a function of
|
|
96
|
+
one name — because two declarations of one name are an error to SMT-LIB and a
|
|
97
|
+
native crash in cvc5 (see :class:`SmtNameMap`), and a constant or a variable whose
|
|
98
|
+
name ends in a mark of the Z3 codec (``!v``, ``!c``: see :func:`_is_marked`), because
|
|
99
|
+
the reader of the text decodes such a symbol as another one. The sanitised goal's
|
|
100
|
+
``sat`` countermodel is translated back via :func:`_reverse_map_assignment` before it
|
|
101
|
+
reaches the caller, and the unsat core and the proof text via
|
|
102
|
+
:func:`_reverse_map_smtlib_text`, so a caller always sees the ORIGINAL kit-level
|
|
103
|
+
symbol name, never the synthesised token.
|
|
104
|
+
|
|
105
|
+
**One namespace.** In SMT-LIB text a bound variable, a predicate, a function and a
|
|
106
|
+
sort are ONE identifier when they are spelled alike, and cvc5 ends the process on
|
|
107
|
+
``(exists ((x0 S)) (x0 x0))`` instead of reporting a type error. So no name may be
|
|
108
|
+
minted AFTER the sanitiser has chosen its tokens: the counting quantifiers are
|
|
109
|
+
expanded and the sort axioms made before it runs (:func:`_lower_counting_for_smtlib`,
|
|
110
|
+
:func:`~unicode_logic_kit.fol._msfl_nodes.sort_axioms`), every witness is fresh against
|
|
111
|
+
every name of the whole problem, and the sanitiser then keeps every symbol of every
|
|
112
|
+
kind apart. The Z3 route needs none of this: its variables are the symbols ``x0!v``,
|
|
113
|
+
which no name of a problem is.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
import importlib.metadata
|
|
117
|
+
import importlib.util
|
|
118
|
+
import json
|
|
119
|
+
import re
|
|
120
|
+
import subprocess
|
|
121
|
+
import sys
|
|
122
|
+
import time
|
|
123
|
+
from dataclasses import dataclass, field, replace
|
|
124
|
+
from typing import Callable, Dict, List, Optional, Sequence, Tuple, cast
|
|
125
|
+
|
|
126
|
+
from ..fol._fol_nodes import numeral_constant_clash, numeral_key
|
|
127
|
+
from ..fol._identifiers import symbol_names
|
|
128
|
+
from ..fol._msfl_nodes import lower_counting, sort_axioms
|
|
129
|
+
from ..fol._tptp_symbols import is_tptp_boolean_atom as _is_tptp_boolean_atom
|
|
130
|
+
from ..fol.nodes import (
|
|
131
|
+
Atom, Constant, Count, Function, Measure, Node, Number, Variable, And, Implies,
|
|
132
|
+
SortedCardinality, SortedConstant, SortedCount, SortedQuantifier, Z3Env,
|
|
133
|
+
)
|
|
134
|
+
from ._ascii_names import ascii_safe_base, reserve_rendered
|
|
135
|
+
from .protocol import ProverBackend, Verdict, PROVED, REFUTED, UNKNOWN, ERROR
|
|
136
|
+
from .z3_models import declaration_keys, separate_variables
|
|
137
|
+
|
|
138
|
+
__all__ = ["Cvc5Backend"]
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# ---------------------------------------------------------------------------
|
|
142
|
+
# ASCII/legality sanitisation — see the module docstring's sanitisation
|
|
143
|
+
# section for why this is narrower than atp._tptp_problem's / atp
|
|
144
|
+
# .prover9_entailment's (only digit-leading pure-ASCII names and SMT-LIB2's
|
|
145
|
+
# own reserved words are unsafe here; everything else, including every
|
|
146
|
+
# non-ASCII name, already round-trips correctly through Z3's own SMT-LIB2
|
|
147
|
+
# serialisation).
|
|
148
|
+
# ---------------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
#: The EIGHT SMT-LIB2 <reserved> words that cannot be a declared name. Seven of
|
|
151
|
+
#: the thirteen the v2.6 grammar's Sec. 3.1 lists (``!``, ``_``, ``as``,
|
|
152
|
+
#: ``exists``, ``forall``, ``let``, ``match``) Z3's OWN parser treats as syntax
|
|
153
|
+
#: rather than an ordinary <symbol> — none of these is a legal plain <symbol>
|
|
154
|
+
#: when used as a declared name, even though nothing else about the string
|
|
155
|
+
#: looks illegal (no digit, no non-ASCII character, no special character Z3
|
|
156
|
+
#: would quote). Z3's own ``to_smt2()`` does not quote any of them either
|
|
157
|
+
#: (verified live: a function declared under the name ``let`` prints as the
|
|
158
|
+
#: undecorated head of ``(let x)``, which its own parser then reads as the
|
|
159
|
+
#: ``let``-BINDING form, not a call to a symbol named ``let``) — see the
|
|
160
|
+
#: module docstring. The eighth is ``par``, which Z3 round-trips but cvc5 reads
|
|
161
|
+
#: as the keyword of a parametric declaration: a constant or a function named
|
|
162
|
+
#: ``par`` ends the Python process with a native access violation (measured on
|
|
163
|
+
#: cvc5 1.3.4, in a child process). The other five <reserved> words
|
|
164
|
+
#: (``BINARY``, ``DECIMAL``, ``HEXADECIMAL``, ``NUMERAL``, ``STRING``) are
|
|
165
|
+
#: DELIBERATELY excluded: verified live that both Z3's parser and cvc5 accept
|
|
166
|
+
#: every one of them as a bare declaration, as an applied predicate/function
|
|
167
|
+
#: head, and as an argument — renaming them would be an unforced, undocumented
|
|
168
|
+
#: rename of a name this backend already handles correctly, which R1 (see the
|
|
169
|
+
#: module docstring) forbids.
|
|
170
|
+
_SMTLIB_RESERVED_WORDS = frozenset({
|
|
171
|
+
"!", "_", "as", "exists", "forall", "let", "match", "par",
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
#: The function symbols of SMT-LIB's Core theory, which every logic has. This
|
|
175
|
+
#: kit reads a predicate or function of one of these names as an ordinary
|
|
176
|
+
#: UNINTERPRETED symbol over its one sort (that is what ``to_z3`` declares, and
|
|
177
|
+
#: Z3 accepts the declaration), but cvc5 already knows the name with a fixed
|
|
178
|
+
#: signature: a ``declare-fun`` of ``distinct`` / ``=>`` / ``xor`` is a parse
|
|
179
|
+
#: error, and one of ``not`` / ``and`` / ``or`` / ``ite`` / ``true`` / ``false``
|
|
180
|
+
#: ends the Python process with a native access violation (measured on cvc5
|
|
181
|
+
#: 1.3.4, each in a child process). So they are renamed like a reserved word.
|
|
182
|
+
#: ``=`` is in :data:`_SMTLIB_THEORIES` below but not here: ``Atom.to_z3`` maps
|
|
183
|
+
#: a binary ``=`` / ``≠`` atom to the native equality, so that one is never
|
|
184
|
+
#: declared; a CONSTANT or FUNCTION named ``=`` is, and is renamed.
|
|
185
|
+
_SMTLIB_CORE_SYMBOLS = frozenset({
|
|
186
|
+
"true", "false", "not", "and", "or", "xor", "=>", "distinct", "ite",
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
#: The symbols the standard SMT-LIB theories declare, by theory (every symbol of
|
|
190
|
+
#: Core, Ints, Reals, Reals_Ints, ArraysEx, FixedSizeBitVectors, FloatingPoint
|
|
191
|
+
#: and Strings, sort names included), and then the further names cvc5 knows (the
|
|
192
|
+
#: groups that begin with ``cvc5``: every name that a measurement on cvc5 1.3.4
|
|
193
|
+
#: showed to be known, found by trying the candidates in a child process).
|
|
194
|
+
#:
|
|
195
|
+
#: This kit reads a predicate, a function or a constant of one of these names as
|
|
196
|
+
#: an ordinary UNINTERPRETED symbol over its one sort (that is what ``to_z3``
|
|
197
|
+
#: declares, and Z3 accepts every one of them), but under the logic ``ALL`` — and
|
|
198
|
+
#: for some of them under ``UF`` too — cvc5 already knows the name with a fixed
|
|
199
|
+
#: signature: ``declare-fun`` of it is a parse error or, for a function or a
|
|
200
|
+
#: constant, a native access violation that ends the Python process. A kit symbol
|
|
201
|
+
#: named like any of them is renamed under EVERY logic, so a caller's ``logic=``
|
|
202
|
+
#: never decides whether a name crashes. A name of the standard theories that cvc5
|
|
203
|
+
#: does not know (``Float16``, ``re.loop``) is renamed for nothing, which costs
|
|
204
|
+
#: nothing; a symbol that a later cvc5 adds is a name this table has to learn
|
|
205
|
+
#: (``tests/test_cvc5_theory_symbols.py`` runs every name of it through cvc5 under
|
|
206
|
+
#: ``ALL``, in a child process).
|
|
207
|
+
_SMTLIB_THEORIES: Dict[str, Tuple[str, ...]] = {
|
|
208
|
+
"Core": ("Bool", "true", "false", "not", "=>", "and", "or", "xor", "=", "distinct", "ite"),
|
|
209
|
+
"Ints": ("Int", "-", "+", "*", "div", "mod", "abs", "<=", "<", ">=", ">", "divisible"),
|
|
210
|
+
"Reals": ("Real", "-", "+", "*", "/", "<=", "<", ">=", ">"),
|
|
211
|
+
"Reals_Ints": ("to_real", "to_int", "is_int"),
|
|
212
|
+
"ArraysEx": ("Array", "select", "store"),
|
|
213
|
+
"FixedSizeBitVectors": (
|
|
214
|
+
"BitVec", "concat", "extract", "bvnot", "bvneg", "bvand", "bvor", "bvxor", "bvnand",
|
|
215
|
+
"bvnor", "bvxnor", "bvcomp", "bvadd", "bvsub", "bvmul", "bvudiv", "bvurem", "bvsdiv",
|
|
216
|
+
"bvsrem", "bvsmod", "bvshl", "bvlshr", "bvashr", "bvult", "bvule", "bvugt", "bvuge",
|
|
217
|
+
"bvslt", "bvsle", "bvsgt", "bvsge", "repeat", "zero_extend", "sign_extend",
|
|
218
|
+
"rotate_left", "rotate_right", "bv2nat", "nat2bv", "int2bv", "ubv_to_int", "sbv_to_int",
|
|
219
|
+
"int_to_bv", "bvnego", "bvuaddo", "bvsaddo", "bvumulo", "bvsmulo", "bvusubo", "bvssubo",
|
|
220
|
+
"bvsdivo", "bvultbv", "bvsltbv", "bvite", "bvredor", "bvredand"),
|
|
221
|
+
"FloatingPoint": (
|
|
222
|
+
"Float16", "Float32", "Float64", "Float128", "FloatingPoint", "RoundingMode", "RNE",
|
|
223
|
+
"RNA", "RTP", "RTN", "RTZ", "roundNearestTiesToEven", "roundNearestTiesToAway",
|
|
224
|
+
"roundTowardPositive", "roundTowardNegative", "roundTowardZero", "fp", "fp.abs",
|
|
225
|
+
"fp.neg", "fp.add", "fp.sub", "fp.mul", "fp.div", "fp.fma", "fp.sqrt", "fp.rem",
|
|
226
|
+
"fp.roundToIntegral", "fp.min", "fp.max", "fp.leq", "fp.lt", "fp.geq", "fp.gt", "fp.eq",
|
|
227
|
+
"fp.isNormal", "fp.isSubnormal", "fp.isZero", "fp.isInfinite", "fp.isNaN",
|
|
228
|
+
"fp.isNegative", "fp.isPositive", "to_fp", "to_fp_unsigned", "fp.to_ubv", "fp.to_sbv",
|
|
229
|
+
"fp.to_real", "+oo", "-oo", "+zero", "-zero", "NaN"),
|
|
230
|
+
"Strings": (
|
|
231
|
+
"String", "RegLan", "str.++", "str.len", "str.<", "str.<=", "str.at", "str.substr",
|
|
232
|
+
"str.prefixof", "str.suffixof", "str.contains", "str.indexof", "str.replace",
|
|
233
|
+
"str.replace_all", "str.replace_re", "str.replace_re_all", "str.is_digit",
|
|
234
|
+
"str.to_code", "str.from_code", "str.to_int", "str.from_int", "str.in_re", "str.to_re",
|
|
235
|
+
"re.none", "re.all", "re.allchar", "re.++", "re.union", "re.inter", "re.*", "re.+",
|
|
236
|
+
"re.opt", "re.range", "re.comp", "re.diff", "re.loop", "re.^", "str.in.re", "str.to.re",
|
|
237
|
+
"int.to.str", "str.to.int", "re.nostr", "str.lt", "str.leq"),
|
|
238
|
+
"cvc5 strings": ("str.rev", "str.to_lower", "str.to_upper", "str.update", "str.indexof_re"),
|
|
239
|
+
"cvc5 arithmetic": (
|
|
240
|
+
"sin", "cos", "tan", "csc", "sec", "cot", "arcsin", "arccos", "arctan", "arccsc",
|
|
241
|
+
"arcsec", "arccot", "exp", "sqrt", "real.pi", "int.pow2", "int.log2", "^", "/_total",
|
|
242
|
+
"div_total", "mod_total", "piand"),
|
|
243
|
+
"cvc5 arrays": ("eqrange",),
|
|
244
|
+
"cvc5 sets and relations": (
|
|
245
|
+
"set.empty", "set.universe", "set.singleton", "set.union", "set.inter", "set.minus",
|
|
246
|
+
"set.subset", "set.member", "set.card", "set.insert", "set.complement", "set.choose",
|
|
247
|
+
"set.is_singleton", "set.is_empty", "set.map", "set.filter", "set.all", "set.some",
|
|
248
|
+
"set.fold", "set.comprehension", "rel.transpose", "rel.product", "rel.join",
|
|
249
|
+
"rel.tclosure", "rel.iden", "rel.group", "rel.aggr", "rel.project", "rel.table_join",
|
|
250
|
+
"rel.join_image"),
|
|
251
|
+
"cvc5 bags and tables": (
|
|
252
|
+
"bag", "bag.empty", "bag.union_max", "bag.union_disjoint", "bag.inter_min",
|
|
253
|
+
"bag.difference_subtract", "bag.difference_remove", "bag.subbag", "bag.count",
|
|
254
|
+
"bag.member", "bag.setof", "bag.card", "bag.choose", "bag.map", "bag.filter",
|
|
255
|
+
"bag.all", "bag.some", "bag.fold", "bag.partition", "table.product", "table.project",
|
|
256
|
+
"table.join", "table.group", "table.aggr"),
|
|
257
|
+
"cvc5 sequences": (
|
|
258
|
+
"seq.empty", "seq.unit", "seq.nth", "seq.len", "seq.++", "seq.update", "seq.at",
|
|
259
|
+
"seq.extract", "seq.contains", "seq.indexof", "seq.replace", "seq.replace_all",
|
|
260
|
+
"seq.rev", "seq.prefixof", "seq.suffixof"),
|
|
261
|
+
"cvc5 tuples, nullables, finite fields, separation logic": (
|
|
262
|
+
"tuple", "tuple.project", "tuple.unit", "nullable.some", "nullable.val",
|
|
263
|
+
"nullable.is_null", "nullable.is_some", "nullable.null", "nullable.lift", "ff.add",
|
|
264
|
+
"ff.mul", "ff.neg", "ff.bitsum", "sep", "pto", "wand", "sep.nil", "sep.emp"),
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
#: Every name of :data:`_SMTLIB_THEORIES`, flat.
|
|
268
|
+
_SMTLIB_THEORY_SYMBOLS = frozenset(
|
|
269
|
+
name for names in _SMTLIB_THEORIES.values() for name in names)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
#: The names Z3's SMT-LIB printer mints for the shared sub-terms of a formula: it writes
|
|
273
|
+
#: ``(let (($x24 (R c d))) ...)`` for a Boolean term and ``(let ((?x10 (g c))) ...)`` for any
|
|
274
|
+
#: other, the sign ``$`` or ``?``, the letter ``x`` and the number of the term in its own table
|
|
275
|
+
#: (measured over several hundred printed problems: no other name opens a ``let``). It does not
|
|
276
|
+
#: look at the symbols the text declares, so a declared symbol or a bound variable spelled alike
|
|
277
|
+
#: would be shadowed inside that ``let`` (the text then says another formula, or is ill-sorted, and
|
|
278
|
+
#: cvc5 ends its process on it). No symbol of a problem is spelled so (:func:`_is_smtlib_safe`),
|
|
279
|
+
#: and no token the sanitiser makes is: a token is a stem that :func:`_is_smtlib_safe` lets
|
|
280
|
+
#: through, or such a stem with a number appended
|
|
281
|
+
#: (:func:`~unicode_logic_kit.atp._ascii_names.reserve_rendered`), and a number turns into a printer
|
|
282
|
+
#: name only the stems ``$x`` and ``?x``. A symbol of that name is kept as it is (it is no printer
|
|
283
|
+
#: name) and every later symbol of the name gets the stem ``$x_<arity>``, so no renamed symbol has one
|
|
284
|
+
#: of them as its stem.
|
|
285
|
+
_PRINTER_NAME = re.compile(r"[$?]x[0-9]+")
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _is_smtlib_safe(name: str) -> bool:
|
|
289
|
+
"""Can a declared symbol of this name be handed to cvc5 as it is?
|
|
290
|
+
|
|
291
|
+
Not when it is empty; not when it is one of the <reserved> words or the name
|
|
292
|
+
of a symbol of an SMT-LIB theory (:data:`_SMTLIB_THEORY_SYMBOLS`); not when
|
|
293
|
+
it begins with a digit (ASCII), with ``.`` or ``@`` (reserved for the solver
|
|
294
|
+
in SMT-LIB), or with a minus sign and a digit (``-1``, which cvc5 reads as a
|
|
295
|
+
numeral); not when it holds ``|``, ``\\`` or ``'``, which Z3 prints in a
|
|
296
|
+
form no SMT-LIB reader reads back as the same symbol; not when it is a name
|
|
297
|
+
Z3's printer gives a shared sub-term (``$x24``, ``?x10``: :data:`_PRINTER_NAME`).
|
|
298
|
+
Every other name — a non-ASCII one, one with a space or a quote that Z3
|
|
299
|
+
pipe-quotes correctly — is left as it is.
|
|
300
|
+
"""
|
|
301
|
+
if not name:
|
|
302
|
+
return False
|
|
303
|
+
if (name in _SMTLIB_RESERVED_WORDS or name in _SMTLIB_CORE_SYMBOLS
|
|
304
|
+
or name in _SMTLIB_THEORY_SYMBOLS):
|
|
305
|
+
return False
|
|
306
|
+
if name.isascii() and name[0].isdigit():
|
|
307
|
+
return False
|
|
308
|
+
if name[0] in ".@" or (name[0] == "-" and name[1:2].isdigit()):
|
|
309
|
+
return False
|
|
310
|
+
if _PRINTER_NAME.fullmatch(name):
|
|
311
|
+
return False
|
|
312
|
+
return not any(ch in name for ch in "|\\'")
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
_PLAIN_NUMERAL = re.compile(r"[0-9]+|[0-9]+(?:\.[0-9]+)?[eE][+-]?[0-9]+")
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _is_plain_numeral(text: str) -> bool:
|
|
319
|
+
"""Is the text of a numeral one that cvc5 takes as the symbol Z3 prints?
|
|
320
|
+
|
|
321
|
+
Digits only (``12``), or digits with an exponent (``1e-07``, ``1.5e+16``,
|
|
322
|
+
which is how Python writes a very small or a very large float). A decimal
|
|
323
|
+
(``2.5``) or a negative number (``-1``) is not: cvc5 ends its process on a
|
|
324
|
+
declaration of a symbol of that text, so such a numeral is renamed like any
|
|
325
|
+
other digit-leading name.
|
|
326
|
+
"""
|
|
327
|
+
return _PLAIN_NUMERAL.fullmatch(text) is not None
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _unquote_smtlib(s: str) -> str:
|
|
331
|
+
"""Strip an SMT-LIB2 ``|...|`` quoted-symbol wrapper, if present.
|
|
332
|
+
|
|
333
|
+
A quoted symbol has no escape mechanism (the only characters forbidden
|
|
334
|
+
INSIDE one are ``|`` and ``\\``, per the SMT-LIB2 spec), so stripping the
|
|
335
|
+
outer pair is a lossless, exact inverse of the quoting Z3's ``to_smt2()``
|
|
336
|
+
already applies to any name it did not consider a plain ``simple_symbol``
|
|
337
|
+
(see the module docstring) — no unescaping needed, unlike a string
|
|
338
|
+
literal.
|
|
339
|
+
"""
|
|
340
|
+
if len(s) >= 2 and s[0] == "|" and s[-1] == "|":
|
|
341
|
+
return s[1:-1]
|
|
342
|
+
return s
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
#: The kinds of declared symbol. A constant is a function of no arguments; a
|
|
346
|
+
#: proposition (a predicate of no arguments) is a predicate; a variable is a symbol
|
|
347
|
+
#: of its own, apart from a constant of the same name.
|
|
348
|
+
_PREDICATE, _FUNCTION, _VARIABLE = "predicate", "function", "variable"
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
#: What the names of the constants and the variables that Z3 reads back end in (see
|
|
352
|
+
#: :func:`~unicode_logic_kit.fol._fol_nodes.kit_name_of_z3_symbol`): ``x!v`` is read as the
|
|
353
|
+
#: variable ``x``. No symbol of the text this module writes ends in one, so that the text
|
|
354
|
+
#: is read as the symbols it declares.
|
|
355
|
+
_Z3_SYMBOL_MARKS = ("!v", "!c")
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _is_marked(name: str, kind: str, arity: int) -> bool:
|
|
359
|
+
"""Is a symbol of this kind and name one that a reader of the text would decode?
|
|
360
|
+
|
|
361
|
+
Only a nullary symbol of the one sort is (a constant, a function of no arguments, a
|
|
362
|
+
variable), and only when its name ends in a mark of the Z3 codec (:data:`_Z3_SYMBOL_MARKS`).
|
|
363
|
+
Such a symbol is renamed like an illegal name, and every token is made to end in
|
|
364
|
+
neither mark, so that no declared symbol is read as another (``a!c`` as ``a``, a constant
|
|
365
|
+
``a!v`` as the variable ``a``) and none is spelled like what the environment writes for
|
|
366
|
+
another (a constant ``a!v`` is the symbol ``a!v!c`` of Z3, the very name of a predicate
|
|
367
|
+
that is called so).
|
|
368
|
+
"""
|
|
369
|
+
return arity == 0 and kind in (_FUNCTION, _VARIABLE) and name.endswith(_Z3_SYMBOL_MARKS)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _smtlib_token_base(name: str) -> str:
|
|
373
|
+
"""The stem of the token a symbol of ``name`` is renamed to.
|
|
374
|
+
|
|
375
|
+
ASCII (non-ASCII characters are spelled as
|
|
376
|
+
:func:`~atp._ascii_names.ascii_safe_base` does), with ``|``, ``\\`` and ``'``
|
|
377
|
+
— which no SMT-LIB reader reads back as part of a symbol — spelled as
|
|
378
|
+
``uXXXX`` escapes too, and prefixed with ``n`` for as long as the result is
|
|
379
|
+
still not a name :func:`_is_smtlib_safe` lets through (a reserved word, a
|
|
380
|
+
theory symbol, a digit, ``.`` or ``@`` first, a minus and a digit, a name of
|
|
381
|
+
Z3's printer). A stem that
|
|
382
|
+
ends in a mark of the Z3 codec (:data:`_Z3_SYMBOL_MARKS`) gets an underscore
|
|
383
|
+
appended, so that no token ends in one.
|
|
384
|
+
"""
|
|
385
|
+
base = "".join(f"u{ord(ch):04x}" if ch in "|\\'" else ch
|
|
386
|
+
for ch in ascii_safe_base(name, "n"))
|
|
387
|
+
while not _is_smtlib_safe(base):
|
|
388
|
+
base = "n" + base
|
|
389
|
+
while base.endswith(_Z3_SYMBOL_MARKS):
|
|
390
|
+
base += "_"
|
|
391
|
+
return base
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
@dataclass
|
|
395
|
+
class SmtNameMap:
|
|
396
|
+
"""The renamings :func:`_sanitize_many_for_smtlib` chose for one problem.
|
|
397
|
+
|
|
398
|
+
**What is one symbol.** A symbol is ``(kind, name, arity)``: a predicate, a
|
|
399
|
+
function (a constant is a function of no arguments) or a variable, its name,
|
|
400
|
+
its number of arguments. The same name at two arities, as a predicate and as a
|
|
401
|
+
function or constant, as a variable and as a constant, is two symbols —
|
|
402
|
+
``to_z3`` declares two, and so does every SMT-LIB text of them, where two
|
|
403
|
+
declarations of one name are an error (cvc5 ends the Python process on it). The
|
|
404
|
+
FIRST symbol of a name, in the order the problem presents them, keeps the name
|
|
405
|
+
when the name is legal; every later symbol of that name gets a token of its
|
|
406
|
+
own (``P_2``, ``a_0``), as does a symbol whose name is not legal. So a variable
|
|
407
|
+
``x`` and a constant ``x`` are written under two different tokens, and the
|
|
408
|
+
quantifier of the one binds nothing of the other.
|
|
409
|
+
|
|
410
|
+
A constant, a function of no arguments or a variable whose name ends in ``!v`` or
|
|
411
|
+
``!c`` is renamed too, and no token ends in either (:func:`_is_marked`): the
|
|
412
|
+
reader of the text (:func:`~unicode_logic_kit.atp.z3_input.from_z3`) decodes such a
|
|
413
|
+
symbol as a variable, or as another constant, so it never has to be one.
|
|
414
|
+
|
|
415
|
+
``mapping`` is the name → token table of the first symbol of each name,
|
|
416
|
+
``symbols`` the table of every symbol. A numeral is the symbol of the text of
|
|
417
|
+
its value (:func:`~unicode_logic_kit.fol._fol_nodes.numeral_key`: ``Number(1)``
|
|
418
|
+
and ``Number(1.0)`` are the constant ``1``): a numeral and a constant of one
|
|
419
|
+
text are refused, as ``Z3Env`` refuses them.
|
|
420
|
+
|
|
421
|
+
Built from the same :func:`~atp._ascii_names.ascii_safe_base` /
|
|
422
|
+
:func:`~atp._ascii_names.reserve_rendered` primitives
|
|
423
|
+
:mod:`atp._tptp_problem` and :mod:`atp.prover9_entailment` use, with
|
|
424
|
+
SMT-LIB2's own legality test (:func:`_is_smtlib_safe`) and no render/fold
|
|
425
|
+
step (SMT-LIB2 text is never case-folded, so the rendered form IS the raw
|
|
426
|
+
token).
|
|
427
|
+
"""
|
|
428
|
+
|
|
429
|
+
mapping: Dict[str, str] = field(default_factory=dict)
|
|
430
|
+
used: set = field(default_factory=set)
|
|
431
|
+
symbols: Dict[Tuple[str, str, int], str] = field(default_factory=dict)
|
|
432
|
+
_pending: list = field(default_factory=list)
|
|
433
|
+
_first: Dict[str, Tuple[str, str, int]] = field(default_factory=dict)
|
|
434
|
+
_numerals: set = field(default_factory=set)
|
|
435
|
+
_constants: set = field(default_factory=set)
|
|
436
|
+
|
|
437
|
+
def collect(self, name: str, kind: str = _FUNCTION, arity: int = 0,
|
|
438
|
+
numeral: bool = False) -> None:
|
|
439
|
+
"""First pass: register the symbol ``(kind, name, arity)``; an
|
|
440
|
+
already-legal name of the first symbol of that name is reserved
|
|
441
|
+
immediately (order-independent — see
|
|
442
|
+
:class:`~atp._tptp_problem._Renamer`'s docstring for why collision
|
|
443
|
+
avoidance for a synthesised name must not depend on processing
|
|
444
|
+
order relative to an unrelated already-legal name).
|
|
445
|
+
|
|
446
|
+
Raises:
|
|
447
|
+
NotImplementedError: a numeral and a constant of the same text, which
|
|
448
|
+
are one symbol (a variable of that text is another symbol).
|
|
449
|
+
"""
|
|
450
|
+
if kind == _FUNCTION and arity == 0:
|
|
451
|
+
(self._numerals if numeral else self._constants).add(name)
|
|
452
|
+
if name in self._numerals and name in self._constants:
|
|
453
|
+
numeral_constant_clash(name)
|
|
454
|
+
key = (kind, name, arity)
|
|
455
|
+
if key in self.symbols or key in self._pending:
|
|
456
|
+
return
|
|
457
|
+
if name not in self._first:
|
|
458
|
+
self._first[name] = key
|
|
459
|
+
# A numeral of plain digits (``1``) or of exponent form (``1e-07``,
|
|
460
|
+
# ``1.5e+16``) is the one digit-leading name left as it is: Z3
|
|
461
|
+
# pipe-quotes it correctly (``|1|``) and reads it back as the number.
|
|
462
|
+
# The other numerals (``2.5``, ``-1``) are quoted in a form cvc5 reads
|
|
463
|
+
# as a literal, and are renamed.
|
|
464
|
+
if ((_is_smtlib_safe(name) and not _is_marked(name, kind, arity))
|
|
465
|
+
or (numeral and _is_plain_numeral(name))):
|
|
466
|
+
self.used.add(name)
|
|
467
|
+
self.mapping[name] = name
|
|
468
|
+
self.symbols[key] = name
|
|
469
|
+
return
|
|
470
|
+
self._pending.append(key)
|
|
471
|
+
|
|
472
|
+
def finalize(self) -> None:
|
|
473
|
+
"""Second pass: synthesise a token for every queued symbol — the
|
|
474
|
+
symbols of an illegal name and every symbol after the first of one
|
|
475
|
+
name — now that every already-legal name in the problem is reserved.
|
|
476
|
+
|
|
477
|
+
:func:`~atp._ascii_names.ascii_safe_base` only prepends its prefix
|
|
478
|
+
when the transliterated result is EMPTY or DIGIT-leading — a
|
|
479
|
+
reserved word such as ``let`` is neither (it is already a plain
|
|
480
|
+
ASCII, non-digit-leading string), so it comes back unchanged and
|
|
481
|
+
would be reserved verbatim, defeating the whole point of queuing it.
|
|
482
|
+
:func:`_smtlib_token_base` catches exactly that residual case. A
|
|
483
|
+
symbol after the first of its name carries its arity in the token
|
|
484
|
+
(``P_2``), so the tokens of one name read apart.
|
|
485
|
+
"""
|
|
486
|
+
for key in self._pending:
|
|
487
|
+
_, name, arity = key
|
|
488
|
+
base = _smtlib_token_base(name)
|
|
489
|
+
if self._first[name] == key:
|
|
490
|
+
token = reserve_rendered(base, self.used)
|
|
491
|
+
self.mapping[name] = token
|
|
492
|
+
else:
|
|
493
|
+
token = reserve_rendered(_smtlib_token_base(f"{base}_{arity}"), self.used)
|
|
494
|
+
self.symbols[key] = token
|
|
495
|
+
self._pending = []
|
|
496
|
+
|
|
497
|
+
def get(self, name: str) -> str:
|
|
498
|
+
"""The token of the first symbol of ``name``."""
|
|
499
|
+
return self.mapping[name]
|
|
500
|
+
|
|
501
|
+
def symbol(self, kind: str, name: str, arity: int) -> str:
|
|
502
|
+
"""The token of the symbol ``(kind, name, arity)``."""
|
|
503
|
+
return self.symbols[(kind, name, arity)]
|
|
504
|
+
|
|
505
|
+
def reverse(self) -> Dict[str, str]:
|
|
506
|
+
"""Token → original name, for every symbol (a legal name maps to itself)."""
|
|
507
|
+
return {token: key[1] for key, token in self.symbols.items()}
|
|
508
|
+
|
|
509
|
+
def variable_tokens(self) -> frozenset:
|
|
510
|
+
"""The tokens that stand for a variable (not for a constant or a predicate)."""
|
|
511
|
+
return frozenset(token for (kind, _, _), token in self.symbols.items() if kind == _VARIABLE)
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
#: The many-sorted nodes that bind a variable over a sort; each carries the sort
|
|
515
|
+
#: NAME in a ``sort`` field, and that name becomes the guard predicate of the
|
|
516
|
+
#: sort (the sort and the unary predicate of that name are one symbol).
|
|
517
|
+
_SORT_BINDERS = (SortedQuantifier, SortedCount, SortedCardinality)
|
|
518
|
+
|
|
519
|
+
#: The counting quantifiers. Their bound ``n`` is a :class:`~fol.nodes.Number`
|
|
520
|
+
#: that is a PARAMETER of the node (and must stay one: the node refuses anything
|
|
521
|
+
#: else), not a numeral that names a symbol; ``to_z3`` expands the quantifier to
|
|
522
|
+
#: distinct witnesses, in which no symbol of that text appears.
|
|
523
|
+
_COUNTING = (Count, SortedCount)
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def _renamed_children(node: Node) -> List[Node]:
|
|
527
|
+
"""The children of ``node`` that name a symbol: every child, except the bound of a counting
|
|
528
|
+
quantifier (a parameter of the node, no numeral of the problem)."""
|
|
529
|
+
return [node.variable, node.formula] if isinstance(node, _COUNTING) else node._child_nodes()
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def _symbol_nodes(node: Node):
|
|
533
|
+
"""Yield ``node`` and every descendant that can name a symbol, in pre-order:
|
|
534
|
+
:meth:`~fol.nodes.Node.walk` without the bound of a counting quantifier.
|
|
535
|
+
|
|
536
|
+
The walk keeps its own stack: the expansion of a counting quantifier (``∃≥500 x P(x)``) is
|
|
537
|
+
five hundred quantifiers deep, and a walk that recurses on it runs out of the interpreter's
|
|
538
|
+
recursion limit.
|
|
539
|
+
"""
|
|
540
|
+
stack = [node]
|
|
541
|
+
while stack:
|
|
542
|
+
current = stack.pop()
|
|
543
|
+
yield current
|
|
544
|
+
stack.extend(reversed(_renamed_children(current)))
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def _declares_no_symbol(atom: Atom) -> bool:
|
|
548
|
+
"""Is ``atom`` read by ``to_z3`` as something that declares no predicate?
|
|
549
|
+
|
|
550
|
+
A BINARY ``=`` / ``≠`` is Z3's native (dis)equality, and ``$true`` /
|
|
551
|
+
``$false`` are the constants true and false; neither names a symbol. An
|
|
552
|
+
``=`` of three arguments is an ordinary uninterpreted predicate of that name.
|
|
553
|
+
"""
|
|
554
|
+
if atom.predicate in ("=", "≠") and len(atom.args) == 2:
|
|
555
|
+
return True
|
|
556
|
+
return _is_tptp_boolean_atom(atom)
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def _sanitize_node_for_smtlib(node: Node, names: SmtNameMap) -> Node:
|
|
560
|
+
"""Rebuild ``node`` with every illegal or shared symbol name replaced.
|
|
561
|
+
|
|
562
|
+
Mirrors :func:`atp._tptp_problem._sanitize_node_for_tptp`'s structural
|
|
563
|
+
recursion; a binary ``=``/``≠`` is excluded from renaming because
|
|
564
|
+
:meth:`~fol.nodes.Atom.to_z3` maps it to Z3's native equality
|
|
565
|
+
operators rather than an uninterpreted predicate — it is never an
|
|
566
|
+
identifier to begin with.
|
|
567
|
+
|
|
568
|
+
Every symbol is looked up by ``(kind, name, arity)`` (see
|
|
569
|
+
:class:`SmtNameMap`): ``P(a)`` and ``P(a, b)`` get two tokens, a predicate
|
|
570
|
+
and a function of one name two, a proposition and a constant two, a variable
|
|
571
|
+
and a constant two. A numeral is rewritten to the constant of the token of its
|
|
572
|
+
value's text, and a variable to the variable of its own token, so a variable named
|
|
573
|
+
like a theory symbol (``select``) is renamed too.
|
|
574
|
+
|
|
575
|
+
The many-sorted nodes are rewritten too, because ``to_z3()`` reduces them
|
|
576
|
+
to plain symbols of the SAME names: a :class:`~fol.nodes.SortedConstant`
|
|
577
|
+
``c:S`` renders as the plain constant ``c`` (so it takes the token of a
|
|
578
|
+
plain ``c`` anywhere else in the problem) and its sort ``S`` is the guard
|
|
579
|
+
predicate ``S``, a unary predicate; a sorted quantifier, counting quantifier
|
|
580
|
+
or cardinality names its sort the same way. A sort name that is not a legal
|
|
581
|
+
SMT-LIB2 symbol (``2S``) would otherwise reach cvc5 unrenamed and end the
|
|
582
|
+
process. A :class:`~fol.nodes.Measure` is the function ``measure`` of two
|
|
583
|
+
arguments and is rewritten to it.
|
|
584
|
+
|
|
585
|
+
The rewriting keeps its own stack (a node is rewritten after its children, once per node
|
|
586
|
+
object), so a formula nested deeper than the interpreter's recursion limit is rewritten as
|
|
587
|
+
readily as a shallow one: the expansion of a counting quantifier of bound 500 is five hundred
|
|
588
|
+
quantifiers deep.
|
|
589
|
+
"""
|
|
590
|
+
rewritten: Dict[int, Node] = {}
|
|
591
|
+
|
|
592
|
+
def rebuilt(child: Node) -> Node:
|
|
593
|
+
return rewritten[id(child)]
|
|
594
|
+
|
|
595
|
+
stack: List[Tuple[Node, bool]] = [(node, False)]
|
|
596
|
+
while stack:
|
|
597
|
+
current, children_done = stack.pop()
|
|
598
|
+
if id(current) in rewritten:
|
|
599
|
+
continue
|
|
600
|
+
children = _renamed_children(current)
|
|
601
|
+
if children and not children_done:
|
|
602
|
+
stack.append((current, True))
|
|
603
|
+
stack.extend((child, False) for child in children)
|
|
604
|
+
continue
|
|
605
|
+
rewritten[id(current)] = _sanitize_one_node(current, names, rebuilt)
|
|
606
|
+
return rewritten[id(node)]
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
def _sanitize_one_node(node: Node, names: SmtNameMap, recurse: Callable[[Node], Node]) -> Node:
|
|
610
|
+
""":func:`_sanitize_node_for_smtlib` for ONE node, whose children are already rewritten:
|
|
611
|
+
``recurse(child)`` is the rewriting of a child."""
|
|
612
|
+
if isinstance(node, Atom):
|
|
613
|
+
if _declares_no_symbol(node):
|
|
614
|
+
pred = node.predicate
|
|
615
|
+
else:
|
|
616
|
+
pred = names.symbol(_PREDICATE, node.predicate, len(node.args))
|
|
617
|
+
return Atom(pred, tuple(recurse(a) for a in node.args))
|
|
618
|
+
if isinstance(node, Function):
|
|
619
|
+
return Function(names.symbol(_FUNCTION, node.name, len(node.args)),
|
|
620
|
+
tuple(recurse(a) for a in node.args))
|
|
621
|
+
if isinstance(node, Constant):
|
|
622
|
+
return Constant(names.symbol(_FUNCTION, node.name, 0))
|
|
623
|
+
if isinstance(node, Variable):
|
|
624
|
+
return Variable(names.symbol(_VARIABLE, node.name, 0))
|
|
625
|
+
if isinstance(node, Number):
|
|
626
|
+
return Constant(names.symbol(_FUNCTION, numeral_key(node.value), 0))
|
|
627
|
+
if isinstance(node, Measure):
|
|
628
|
+
return Function(names.symbol(_FUNCTION, "measure", 2),
|
|
629
|
+
(recurse(node.entity), recurse(node.dimension)))
|
|
630
|
+
if isinstance(node, SortedConstant):
|
|
631
|
+
return SortedConstant(names.symbol(_FUNCTION, node.name, 0),
|
|
632
|
+
names.symbol(_PREDICATE, node.sort, 1))
|
|
633
|
+
if isinstance(node, Count):
|
|
634
|
+
return replace(node, variable=cast(Variable, recurse(node.variable)),
|
|
635
|
+
formula=recurse(node.formula))
|
|
636
|
+
if isinstance(node, SortedCount):
|
|
637
|
+
return replace(node, variable=cast(Variable, recurse(node.variable)),
|
|
638
|
+
sort=names.symbol(_PREDICATE, node.sort, 1), formula=recurse(node.formula))
|
|
639
|
+
if isinstance(node, _SORT_BINDERS):
|
|
640
|
+
body = node.map_children(recurse)
|
|
641
|
+
return replace(body, sort=names.symbol(_PREDICATE, node.sort, 1))
|
|
642
|
+
return node.map_children(recurse)
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
def _collect_names_for_smtlib(node: Node, names: SmtNameMap) -> None:
|
|
646
|
+
"""First pass (see :meth:`SmtNameMap.collect`): register every
|
|
647
|
+
symbol ``node`` uses, without rewriting anything yet: a predicate or a
|
|
648
|
+
function at its arity, a constant, a variable (a symbol of its own), a numeral
|
|
649
|
+
(the symbol of its value's text), the ``measure`` function a :class:`~fol.nodes.Measure`
|
|
650
|
+
stands for. A sorted constant registers its constant name and its sort as a
|
|
651
|
+
unary predicate (a sorted constant and a plain constant of one name are one
|
|
652
|
+
symbol), and a sorted binder registers its sort. The bound of a counting
|
|
653
|
+
quantifier is no numeral of the problem and is passed over
|
|
654
|
+
(:func:`_symbol_nodes`)."""
|
|
655
|
+
for n in _symbol_nodes(node):
|
|
656
|
+
if isinstance(n, Atom):
|
|
657
|
+
if not _declares_no_symbol(n):
|
|
658
|
+
names.collect(n.predicate, _PREDICATE, len(n.args))
|
|
659
|
+
elif isinstance(n, Function):
|
|
660
|
+
names.collect(n.name, _FUNCTION, len(n.args))
|
|
661
|
+
elif isinstance(n, Constant):
|
|
662
|
+
names.collect(n.name, _FUNCTION, 0)
|
|
663
|
+
elif isinstance(n, Variable):
|
|
664
|
+
names.collect(n.name, _VARIABLE, 0)
|
|
665
|
+
elif isinstance(n, Number):
|
|
666
|
+
names.collect(numeral_key(n.value), _FUNCTION, 0, numeral=True)
|
|
667
|
+
elif isinstance(n, Measure):
|
|
668
|
+
names.collect("measure", _FUNCTION, 2)
|
|
669
|
+
elif isinstance(n, SortedConstant):
|
|
670
|
+
names.collect(n.name, _FUNCTION, 0)
|
|
671
|
+
names.collect(n.sort, _PREDICATE, 1)
|
|
672
|
+
elif isinstance(n, _SORT_BINDERS):
|
|
673
|
+
names.collect(n.sort, _PREDICATE, 1)
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
def _sanitize_for_smtlib(node: Node) -> Tuple[Node, SmtNameMap]:
|
|
677
|
+
"""Sanitise ``node`` (the already-folded ``(∧ premises) → φ`` goal) for
|
|
678
|
+
the SMT-LIB2 round trip. Returns ``(sanitised_node, mapping)`` — the
|
|
679
|
+
two-pass collect-then-finalize split (see :class:`SmtNameMap`, mirroring
|
|
680
|
+
:class:`atp._tptp_problem._Renamer`) means a synthesised digit-safe
|
|
681
|
+
token can never collide with an already-legal name anywhere in
|
|
682
|
+
``node``, regardless of which one this walk reaches first.
|
|
683
|
+
|
|
684
|
+
Single-node case, kept for the digit-leading/R1/R2/R5 regression tests
|
|
685
|
+
that exercise it directly; :meth:`Cvc5Backend.decide` itself uses
|
|
686
|
+
:func:`_sanitize_many_for_smtlib` (below) so premises stay SEPARATE
|
|
687
|
+
SMT-LIB2 assertions rather than one folded implication — see that
|
|
688
|
+
function's docstring for why.
|
|
689
|
+
"""
|
|
690
|
+
names = SmtNameMap()
|
|
691
|
+
_collect_names_for_smtlib(node, names)
|
|
692
|
+
names.finalize()
|
|
693
|
+
return _sanitize_node_for_smtlib(node, names), names
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def _sanitize_many_for_smtlib(nodes: Sequence[Node]) -> Tuple[List[Node], SmtNameMap]:
|
|
697
|
+
"""Sanitise several nodes (this backend's premises, then the goal, in
|
|
698
|
+
that order) against ONE SHARED name map, so a symbol used across
|
|
699
|
+
several of them renames consistently — the same two-pass
|
|
700
|
+
collect-then-finalize discipline as :func:`_sanitize_for_smtlib`
|
|
701
|
+
(:class:`SmtNameMap`), just collected across the WHOLE list before any
|
|
702
|
+
renaming is finalised, rather than over one already-folded node.
|
|
703
|
+
|
|
704
|
+
:meth:`Cvc5Backend.decide` asserts each returned node as its OWN
|
|
705
|
+
``(assert ...)`` SMT-LIB2 command (see :meth:`Cvc5Backend._run`) instead
|
|
706
|
+
of folding ``premises`` into one ``(∧ premises) → φ`` implication first
|
|
707
|
+
(:func:`_implication` — still used by :func:`_sanitize_for_smtlib`'s own
|
|
708
|
+
regression tests, unrelated to this path): cvc5's ``getUnsatCore()``
|
|
709
|
+
reports relevance at the granularity of INDIVIDUAL top-level assertions,
|
|
710
|
+
so a single folded assertion would always report as "the whole thing",
|
|
711
|
+
a technically sound but useless certificate — the exact failure mode
|
|
712
|
+
this module's own C12 test suite checks for. Logically this changes
|
|
713
|
+
nothing (SMT solvers conjoin every assertion regardless of how many
|
|
714
|
+
``(assert ...)`` commands they arrived in), only the unsat-core
|
|
715
|
+
bookkeeping's resolution.
|
|
716
|
+
"""
|
|
717
|
+
names = SmtNameMap()
|
|
718
|
+
for node in nodes:
|
|
719
|
+
_collect_names_for_smtlib(node, names)
|
|
720
|
+
names.finalize()
|
|
721
|
+
return [_sanitize_node_for_smtlib(node, names) for node in nodes], names
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
#: The characters an SMT-LIB simple symbol is made of (SMT-LIB 2.6, section 3.1), besides letters and
|
|
725
|
+
#: digits. A token in a solver's text ends where the next character is none of them.
|
|
726
|
+
_SMTLIB_SYMBOL_CHARS = "~!@$%^&*_-+=<>.?/"
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
def _reverse_map_smtlib_text(text: str, reverse: Dict[str, str]) -> str:
|
|
730
|
+
"""Write the caller's names into ``text`` that a solver printed (a core term, a proof).
|
|
731
|
+
|
|
732
|
+
Replaces every occurrence of a token of ``reverse`` (token → original name) that is a whole
|
|
733
|
+
SMT-LIB symbol: not preceded and not followed by a character a simple symbol is made of
|
|
734
|
+
(:data:`_SMTLIB_SYMBOL_CHARS`, letters and digits). A word boundary is not that test: a
|
|
735
|
+
token such as ``n<`` or ``n+`` (the renaming of the theory symbols ``<`` and ``+``)
|
|
736
|
+
ends in a character that is no word character, so there is no boundary after it and a
|
|
737
|
+
``\\b`` pattern never found it. The longest token is tried first, so a token that begins another is
|
|
738
|
+
not matched short. A name that was left as it is needs no rewriting.
|
|
739
|
+
|
|
740
|
+
A quoted symbol (``|is n<|``) is ONE symbol, whatever characters it holds: a token inside it is
|
|
741
|
+
part of that other name and is left alone. A quoted symbol that is a token as a whole
|
|
742
|
+
(``|n<|``, the same symbol as ``n<``) is rewritten as a whole, and stays quoted. A string literal
|
|
743
|
+
and a comment are skipped the same way, so a ``|`` in one of them opens no quoted symbol.
|
|
744
|
+
"""
|
|
745
|
+
tokens = {token: original for token, original in reverse.items() if token != original}
|
|
746
|
+
if not tokens or not text:
|
|
747
|
+
return text
|
|
748
|
+
chars = "A-Za-z0-9" + "".join(re.escape(ch) for ch in _SMTLIB_SYMBOL_CHARS)
|
|
749
|
+
pattern = re.compile(
|
|
750
|
+
r"(?P<quoted>\|[^|\\]*\|)|(?P<string>\"(?:[^\"]|\"\")*\")|(?P<comment>;[^\n]*)"
|
|
751
|
+
f"|(?<![{chars}])(?P<token>" + "|".join(re.escape(token) for token in sorted(tokens, key=len, reverse=True))
|
|
752
|
+
+ f")(?![{chars}])")
|
|
753
|
+
|
|
754
|
+
def rewritten(match) -> str:
|
|
755
|
+
token = match.group("token")
|
|
756
|
+
if token is not None:
|
|
757
|
+
return tokens[token]
|
|
758
|
+
quoted = match.group("quoted")
|
|
759
|
+
if quoted is not None and quoted[1:-1] in tokens:
|
|
760
|
+
original = tokens[quoted[1:-1]]
|
|
761
|
+
# a name that holds ``|`` or a backslash cannot be written inside a quoted symbol
|
|
762
|
+
return original if any(ch in original for ch in "|\\") else f"|{original}|"
|
|
763
|
+
return match.group(0)
|
|
764
|
+
|
|
765
|
+
return pattern.sub(rewritten, text)
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
def _lower_counting_for_smtlib(nodes: Sequence[Node]) -> Tuple[List[Node], set]:
|
|
769
|
+
"""Lower every counting quantifier of ``nodes``, before the names of the problem are renamed.
|
|
770
|
+
|
|
771
|
+
The text of this module has ONE namespace: a bound variable, a predicate, a function and a
|
|
772
|
+
sort are one identifier when they are spelled alike (``(exists ((x0 S)) (x0 x0))``, which
|
|
773
|
+
cvc5 turns into a native crash). A counting quantifier is expanded into witnesses that the
|
|
774
|
+
translation to Z3 mints, after the sanitiser has chosen its tokens, so such a witness
|
|
775
|
+
was no symbol the sanitiser had seen. So the counting quantifiers are expanded first, by
|
|
776
|
+
:func:`~unicode_logic_kit.fol._msfl_nodes.lower_counting`, with every name of the whole
|
|
777
|
+
problem to avoid; the witnesses are then variables of the problem, which the sanitiser
|
|
778
|
+
keeps apart from every other symbol like any variable.
|
|
779
|
+
|
|
780
|
+
Returns the lowered nodes and the set of every name of the problem, the witnesses
|
|
781
|
+
included, for the caller to avoid in what else it mints (the sort axioms).
|
|
782
|
+
"""
|
|
783
|
+
avoid = set(symbol_names(*nodes))
|
|
784
|
+
return [lower_counting(node, avoid) for node in nodes], avoid
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
def _reverse_map_assignment(entries: Sequence[Tuple[str, int, str, str]],
|
|
788
|
+
reverse: Dict[str, str],
|
|
789
|
+
variables: frozenset = frozenset()) -> Dict[str, str]:
|
|
790
|
+
"""Translate a cvc5 ``sat`` model, one ``(declared_term_str, arity,
|
|
791
|
+
range, value_str)`` entry per declaration, back to original kit-level names
|
|
792
|
+
in ``{key: value}`` form.
|
|
793
|
+
|
|
794
|
+
Every name AND value is first unquoted (:func:`_unquote_smtlib`) — cvc5's
|
|
795
|
+
``str(term)``/``str(value)`` reproduce whatever quoting the term's own
|
|
796
|
+
declaration used, so a non-ASCII name that Z3 pipe-quoted on export (see
|
|
797
|
+
the module docstring — already correct, never renamed by
|
|
798
|
+
:func:`_sanitize_for_smtlib`) would otherwise reach the caller as
|
|
799
|
+
``"|świątek|"`` rather than the true original ``"świątek"``. After
|
|
800
|
+
unquoting, a name found in ``reverse`` (a name this module DID rename) is
|
|
801
|
+
translated back to its original; anything else — cvc5's own fresh
|
|
802
|
+
model-value tokens (``"(as @S_0 S)"``, ``"(lambda (...) ...)"``, ...)
|
|
803
|
+
included — passes through the unquoted form unchanged, since
|
|
804
|
+
``reverse.get(..., default)`` falls back to the unquoted string itself.
|
|
805
|
+
|
|
806
|
+
The key of a symbol is its original name when no other declaration of the
|
|
807
|
+
model has that name, and ``"name/arity"`` (``"name/arity:range"`` when that
|
|
808
|
+
still does not tell two apart) when one name is declared more than once — one
|
|
809
|
+
name at two arities, a function and a predicate of one name — as the Z3 route
|
|
810
|
+
reports them (:func:`~unicode_logic_kit.atp.z3_models.declaration_keys`), so
|
|
811
|
+
two symbols that this module renamed apart do not fold back into one entry.
|
|
812
|
+
|
|
813
|
+
``variables`` are the tokens that stand for a FREE variable (a declared symbol
|
|
814
|
+
of the problem that is no constant; :meth:`SmtNameMap.variable_tokens`). A
|
|
815
|
+
variable is reported under its own name, and as ``name!v`` when a constant of
|
|
816
|
+
that name is declared too (:func:`~unicode_logic_kit.atp.z3_models.separate_variables`),
|
|
817
|
+
so the constant keeps its plain name.
|
|
818
|
+
"""
|
|
819
|
+
def original(text: str) -> str:
|
|
820
|
+
return reverse.get(_unquote_smtlib(text), _unquote_smtlib(text))
|
|
821
|
+
|
|
822
|
+
named = [(original(term), arity, range_text) for term, arity, range_text, _ in entries]
|
|
823
|
+
is_variable = [_unquote_smtlib(term) in variables for term, _, _, _ in entries]
|
|
824
|
+
keys = declaration_keys(separate_variables(named, is_variable))
|
|
825
|
+
return {key: original(value) for key, (_, _, _, value) in zip(keys, entries)}
|
|
826
|
+
|
|
827
|
+
|
|
828
|
+
def _implication(formula: Node, premises: Sequence[Node]) -> Node:
|
|
829
|
+
"""Fold ``premises ⊨ φ`` into the single formula ``(∧ premises) → φ``.
|
|
830
|
+
|
|
831
|
+
Reimplemented locally (rather than imported from
|
|
832
|
+
:mod:`unicode_logic_kit.atp.protocol`) because the helper there is a
|
|
833
|
+
private, unexported symbol — this module only imports protocol's public
|
|
834
|
+
contract (:class:`ProverBackend`, :class:`Verdict`, the status
|
|
835
|
+
constants).
|
|
836
|
+
"""
|
|
837
|
+
premises = list(premises)
|
|
838
|
+
if not premises:
|
|
839
|
+
return formula
|
|
840
|
+
conj = premises[0]
|
|
841
|
+
for p in premises[1:]:
|
|
842
|
+
conj = And(conj, p)
|
|
843
|
+
return Implies(conj, formula)
|
|
844
|
+
|
|
845
|
+
|
|
846
|
+
def _timed(fn):
|
|
847
|
+
"""Run ``fn()`` returning ``(result, seconds)``."""
|
|
848
|
+
start = time.perf_counter()
|
|
849
|
+
result = fn()
|
|
850
|
+
return result, time.perf_counter() - start
|
|
851
|
+
|
|
852
|
+
|
|
853
|
+
# ---------------------------------------------------------------------------
|
|
854
|
+
# Solver-version provenance (K1). cvc5 is a pip binding (``external = False``
|
|
855
|
+
# — see the class attribute below), not a spawned binary, so there is no
|
|
856
|
+
# ``--version`` subprocess to run and memoize the way
|
|
857
|
+
# ``atp.protocol._binary_version`` does for Vampire/Prover9/E/Zipperposition;
|
|
858
|
+
# the installed package's own distribution metadata is the analogous
|
|
859
|
+
# provenance, and it is exactly as immutable for the life of THIS process
|
|
860
|
+
# (the interpreter would have to restart to pick up a different install), so
|
|
861
|
+
# a one-slot memo is the same "ask once" discipline, just backed by
|
|
862
|
+
# ``importlib.metadata`` instead of a subprocess.
|
|
863
|
+
# ---------------------------------------------------------------------------
|
|
864
|
+
|
|
865
|
+
_VERSION_CACHE: Dict[str, Optional[str]] = {}
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
def _cvc5_package_version() -> Optional[str]:
|
|
869
|
+
"""The installed ``cvc5`` PyPI package's version, memoized process-wide.
|
|
870
|
+
|
|
871
|
+
``None`` when the package is not importable (mirrors
|
|
872
|
+
:meth:`Cvc5Backend.available`) or its distribution metadata cannot be
|
|
873
|
+
read for any reason (e.g. an editable/vendored install with no METADATA
|
|
874
|
+
file) — best-effort provenance, never raises.
|
|
875
|
+
"""
|
|
876
|
+
if "cvc5" in _VERSION_CACHE:
|
|
877
|
+
return _VERSION_CACHE["cvc5"]
|
|
878
|
+
version: Optional[str] = None
|
|
879
|
+
if importlib.util.find_spec("cvc5") is not None:
|
|
880
|
+
try:
|
|
881
|
+
version = importlib.metadata.version("cvc5")
|
|
882
|
+
except importlib.metadata.PackageNotFoundError:
|
|
883
|
+
version = None
|
|
884
|
+
_VERSION_CACHE["cvc5"] = version
|
|
885
|
+
return version
|
|
886
|
+
|
|
887
|
+
|
|
888
|
+
# ---------------------------------------------------------------------------
|
|
889
|
+
# The Alethe proof text, in a child process.
|
|
890
|
+
#
|
|
891
|
+
# cvc5 1.3.4's proof PRINTER (``Solver.proofToString``) can end the calling
|
|
892
|
+
# process with a native access violation — e.g. ``∀x f(carl) = x ⊢ ∃w ∀x
|
|
893
|
+
# f(carl) = x`` — while ``checkSat``, ``getProof`` and ``getUnsatCore`` on the
|
|
894
|
+
# same solver are fine. A native crash is not a Python exception, so no
|
|
895
|
+
# ``try`` around the call can protect the caller. The verdict and the unsat core
|
|
896
|
+
# therefore never depend on it: the default path asks for neither a proof nor
|
|
897
|
+
# its text, and the text is produced only when the caller asks (``proof=True``),
|
|
898
|
+
# by a second solver run in a child interpreter with a time limit. A crash or a
|
|
899
|
+
# timeout there costs the text and nothing else, and the verdict says why.
|
|
900
|
+
# ---------------------------------------------------------------------------
|
|
901
|
+
|
|
902
|
+
#: Seconds the child interpreter may take on top of the solver's own time limit
|
|
903
|
+
#: (importing cvc5, printing the proof).
|
|
904
|
+
_PROOF_PROCESS_SLACK = 30.0
|
|
905
|
+
|
|
906
|
+
#: The child's time limit, in seconds, when the caller gave no time limit.
|
|
907
|
+
_PROOF_UNLIMITED_BUDGET = 60.0
|
|
908
|
+
|
|
909
|
+
#: What a PROVED verdict's proof says about its text when none was asked for.
|
|
910
|
+
_PROOF_NOT_REQUESTED = ("not requested: the Alethe text is produced only on request "
|
|
911
|
+
"(pass proof=True), in a child process")
|
|
912
|
+
|
|
913
|
+
#: The program the child interpreter runs. It reads one JSON object on stdin
|
|
914
|
+
#: (``smt2`` the problem, ``logic``, ``seed``, ``timeout`` in milliseconds) and
|
|
915
|
+
#: writes one JSON line on stdout. It imports nothing but ``cvc5``, so it runs the
|
|
916
|
+
#: same whatever the parent's ``sys.path`` is. The options are those of the
|
|
917
|
+
#: parent's solve plus the proofs; a solver that never reaches ``unsat`` here
|
|
918
|
+
#: (the time limit) reports that and no text.
|
|
919
|
+
_PROOF_CHILD_SOURCE = r"""
|
|
920
|
+
import json, sys
|
|
921
|
+
import cvc5
|
|
922
|
+
|
|
923
|
+
spec = json.loads(sys.stdin.read())
|
|
924
|
+
solver = cvc5.Solver()
|
|
925
|
+
solver.setLogic(spec["logic"])
|
|
926
|
+
solver.setOption("produce-models", "true")
|
|
927
|
+
solver.setOption("produce-proofs", "true")
|
|
928
|
+
solver.setOption("proof-format-mode", "alethe")
|
|
929
|
+
solver.setOption("produce-unsat-cores", "true")
|
|
930
|
+
solver.setOption("seed", str(spec["seed"]))
|
|
931
|
+
if spec["timeout"] and spec["timeout"] > 0:
|
|
932
|
+
solver.setOption("tlimit", str(spec["timeout"]))
|
|
933
|
+
solver.setOption("tlimit-per", str(spec["timeout"]))
|
|
934
|
+
parser = cvc5.InputParser(solver)
|
|
935
|
+
symbol_manager = parser.getSymbolManager()
|
|
936
|
+
parser.setStringInput(cvc5.InputLanguage.SMT_LIB_2_6, spec["smt2"], "cvc5_backend")
|
|
937
|
+
while True:
|
|
938
|
+
command = parser.nextCommand()
|
|
939
|
+
if command.isNull():
|
|
940
|
+
break
|
|
941
|
+
if command.getCommandName() == "check-sat":
|
|
942
|
+
continue
|
|
943
|
+
command.invoke(solver, symbol_manager)
|
|
944
|
+
result = solver.checkSat()
|
|
945
|
+
if not result.isUnsat():
|
|
946
|
+
print(json.dumps({"ok": False, "why": "the proof run answered %s instead of unsat" % (result,)}))
|
|
947
|
+
sys.exit(0)
|
|
948
|
+
raw = solver.proofToString(solver.getProof()[0])
|
|
949
|
+
print(json.dumps({"ok": True, "text": raw.decode("utf-8") if isinstance(raw, bytes) else raw}))
|
|
950
|
+
"""
|
|
951
|
+
|
|
952
|
+
|
|
953
|
+
def _alethe_text(smt2_text: str, logic: str, random_seed: int,
|
|
954
|
+
timeout: int) -> Tuple[Optional[str], Optional[str]]:
|
|
955
|
+
"""Ask a child interpreter for the Alethe proof of an unsatisfiable problem.
|
|
956
|
+
|
|
957
|
+
Returns ``(text, None)``, or ``(None, reason)`` when there is no text: the
|
|
958
|
+
child ended abnormally (cvc5's proof printer ending its process is the
|
|
959
|
+
known case), ran past its time limit, could not be started, or did not reach
|
|
960
|
+
``unsat``. Never raises and never takes the calling process down; the
|
|
961
|
+
caller's verdict and unsat core come from the main solve and are unaffected.
|
|
962
|
+
|
|
963
|
+
The limit is the solver's own ``timeout`` (milliseconds) plus
|
|
964
|
+
:data:`_PROOF_PROCESS_SLACK` seconds (:data:`_PROOF_UNLIMITED_BUDGET` plus
|
|
965
|
+
the slack when there is no ``timeout``).
|
|
966
|
+
"""
|
|
967
|
+
if not sys.executable:
|
|
968
|
+
return None, "there is no Python interpreter to run the proof printer in"
|
|
969
|
+
seconds = (timeout / 1000.0 if timeout and timeout > 0 else _PROOF_UNLIMITED_BUDGET)
|
|
970
|
+
limit = seconds + _PROOF_PROCESS_SLACK
|
|
971
|
+
spec = json.dumps({"smt2": smt2_text, "logic": logic, "seed": random_seed,
|
|
972
|
+
"timeout": timeout})
|
|
973
|
+
try:
|
|
974
|
+
done = subprocess.run([sys.executable, "-c", _PROOF_CHILD_SOURCE], input=spec,
|
|
975
|
+
capture_output=True, text=True, encoding="utf-8",
|
|
976
|
+
timeout=limit)
|
|
977
|
+
except subprocess.TimeoutExpired:
|
|
978
|
+
return None, (f"cvc5's proof run did not finish within {limit:g} s and was stopped; "
|
|
979
|
+
f"the verdict and the unsat core are from the main solve")
|
|
980
|
+
except OSError as exc:
|
|
981
|
+
return None, f"the proof run could not be started ({type(exc).__name__}: {exc})"
|
|
982
|
+
if done.returncode != 0:
|
|
983
|
+
return None, (f"cvc5's proof run ended abnormally (exit code {done.returncode}; its "
|
|
984
|
+
f"Alethe proof printer is known to end its process on some problems); "
|
|
985
|
+
f"the verdict and the unsat core are from the main solve")
|
|
986
|
+
try:
|
|
987
|
+
answer = json.loads(done.stdout.strip().splitlines()[-1])
|
|
988
|
+
except (IndexError, ValueError):
|
|
989
|
+
return None, "the proof run printed no answer"
|
|
990
|
+
if not answer.get("ok"):
|
|
991
|
+
return None, str(answer.get("why") or "the proof run gave no text")
|
|
992
|
+
return answer["text"], None
|
|
993
|
+
|
|
994
|
+
|
|
995
|
+
class Cvc5Backend(ProverBackend):
|
|
996
|
+
"""Classical FOL/MSFOL via cvc5 — tri-state, with a model on refutation.
|
|
997
|
+
|
|
998
|
+
Structurally the same contract as ``Z3Backend``: an entailment
|
|
999
|
+
``premises ⊨ formula`` is decided by asking whether the negated goal is
|
|
1000
|
+
UNSAT. ``proved`` and ``refuted`` are both fully trustworthy (cvc5's
|
|
1001
|
+
``unsat``/``sat`` are sound and, on the quantifier-free fragment,
|
|
1002
|
+
complete); ``unknown`` only ever means cvc5's own instantiation search
|
|
1003
|
+
did not close the goal — never a silent downgrade of a real answer.
|
|
1004
|
+
|
|
1005
|
+
Registered automatically: ``atp/protocol.py`` imports and registers this
|
|
1006
|
+
backend at the bottom of its own module, and its ``default_chain("fol")``
|
|
1007
|
+
inserts ``"cvc5"`` directly after ``"z3"`` whenever :meth:`available`
|
|
1008
|
+
is true — so on a machine with the optional ``cvc5`` extra installed, a
|
|
1009
|
+
plain ``prove()`` call runs cvc5 with zero caller action (see
|
|
1010
|
+
``default_chain``'s docstring for why that one member is
|
|
1011
|
+
availability-dependent). This module itself never touches the registry.
|
|
1012
|
+
|
|
1013
|
+
A PROVED verdict also carries a certificate in ``Verdict.proof``:
|
|
1014
|
+
``{"kind": "cvc5_alethe", "text": <Alethe proof text or None>,
|
|
1015
|
+
"unsat_core": [<original-name term text>, ...]}``. The core is cvc5's own
|
|
1016
|
+
``getUnsatCore`` on the per-call ``cvc5.Solver()`` this backend builds — no
|
|
1017
|
+
process-wide cvc5 setting is ever touched, mirroring ``Z3Backend``'s own
|
|
1018
|
+
per-``Solver`` discipline. ``unsat_core`` is SOUND (re-asserting just those
|
|
1019
|
+
terms is still unsat) but not necessarily MINIMAL — cvc5's core extraction
|
|
1020
|
+
is free to keep more than the smallest sufficient subset, exactly like
|
|
1021
|
+
``Z3Backend``'s ``z3_unsat_core``. It also never contains one of the
|
|
1022
|
+
synthetic many-sorted axioms (non-emptiness and membership) described
|
|
1023
|
+
below — see :meth:`_run`'s docstring for how that exclusion is done, since this
|
|
1024
|
+
backend's SMT-LIB2-replay route has no ``assert_and_track``-style
|
|
1025
|
+
tagged boolean to lean on the way ``Z3Backend``'s own core does.
|
|
1026
|
+
``unsat_core`` reading is best-effort (a format/version edge case degrades
|
|
1027
|
+
to ``[]`` rather than turning a sound PROVED verdict into an ERROR one), and
|
|
1028
|
+
it is reverse-mapped back to original kit-level symbol names before it
|
|
1029
|
+
reaches the caller — see :func:`_reverse_map_assignment` for the ``sat``
|
|
1030
|
+
branch's ``countermodel``.
|
|
1031
|
+
|
|
1032
|
+
**The Alethe text is opt-in.** cvc5's proof printer can end the process
|
|
1033
|
+
(see the comment above :func:`_alethe_text`), so the text is produced only
|
|
1034
|
+
when asked for, ``decide(..., proof=True)``, and then by a second solver run
|
|
1035
|
+
in a child process with a time limit; the time of that run is part of the
|
|
1036
|
+
verdict's ``wall_time``. Without ``proof=True``, ``text`` is
|
|
1037
|
+
``None`` and ``text_unavailable`` says so. With it, a crash or a timeout of
|
|
1038
|
+
the child leaves ``text`` ``None``, puts the reason in ``text_unavailable``
|
|
1039
|
+
and in the verdict's ``detail``, and leaves the PROVED verdict and the core
|
|
1040
|
+
exactly as they are.
|
|
1041
|
+
|
|
1042
|
+
**Names.** A symbol is ``(kind, name, arity)``: one name at two arities, as a
|
|
1043
|
+
predicate and as a function or constant, is several symbols, each declared
|
|
1044
|
+
under a name of its own (two declarations of one name end cvc5's process),
|
|
1045
|
+
and a countermodel reports each under ``"name/arity"``. A kit symbol named
|
|
1046
|
+
like a symbol of a standard SMT-LIB theory (``+``, ``select``, ``str.len``,
|
|
1047
|
+
…) is renamed under every logic — see :data:`_SMTLIB_THEORIES`. The witnesses
|
|
1048
|
+
of a counting quantifier and of a sort's non-emptiness axiom are names of the
|
|
1049
|
+
problem like any other, fresh against every name of it (see the module
|
|
1050
|
+
docstring), so a predicate, a function, a constant or a sort called ``x0``
|
|
1051
|
+
never meets a witness. The countermodel, the unsat core and the proof text
|
|
1052
|
+
carry the caller's names.
|
|
1053
|
+
|
|
1054
|
+
**Many-sorted (MSFOL) soundness.** A sorted quantifier/constant/count
|
|
1055
|
+
lowers to a plain unary predicate guard (the same relativisation
|
|
1056
|
+
:class:`Z3Backend` relies on), which by itself carries no guarantee that
|
|
1057
|
+
the guarded sort is non-empty or that a sorted constant ``c:S`` lies in
|
|
1058
|
+
``S`` — and MSFOL, by convention, never gives a sort an empty universe and
|
|
1059
|
+
puts ``c`` in ``S`` (see the classical-reasoning guide's many-sorted
|
|
1060
|
+
section). :meth:`decide` closes both gaps exactly like
|
|
1061
|
+
:class:`Z3Backend` does: it adds
|
|
1062
|
+
``unicode_logic_kit.fol._msfl_nodes.sort_axioms(*premises, formula)`` — one
|
|
1063
|
+
``∃x S(x)`` per sort, one ``S(c)`` per sorted constant — as their own
|
|
1064
|
+
extra, UNNEGATED top-level assertions (one more ``(assert ...)`` command
|
|
1065
|
+
each, mirroring how every premise already gets its own — see
|
|
1066
|
+
:meth:`_run`'s docstring), never folded inside ``to_z3()`` itself. They
|
|
1067
|
+
are built from the SANITISED nodes (the ones that are actually
|
|
1068
|
+
translated), so a sort or a constant whose name had to be renamed to be a
|
|
1069
|
+
legal SMT-LIB2 symbol carries the same token in the fact as in the
|
|
1070
|
+
premises; the sanitiser renames sorts, sorted constants and the plain
|
|
1071
|
+
constants of the same name alike (see :func:`_sanitize_node_for_smtlib`).
|
|
1072
|
+
Empty for an unsorted query, so behaviour there is unchanged.
|
|
1073
|
+
"""
|
|
1074
|
+
|
|
1075
|
+
name = "cvc5"
|
|
1076
|
+
logics = frozenset({"fol"})
|
|
1077
|
+
external = False # pip package (optional extra), not a spawned binary
|
|
1078
|
+
|
|
1079
|
+
def available(self) -> bool:
|
|
1080
|
+
"""Pure discovery: is the ``cvc5`` package importable? (No import.)"""
|
|
1081
|
+
return importlib.util.find_spec("cvc5") is not None
|
|
1082
|
+
|
|
1083
|
+
def solver_version(self) -> Optional[str]:
|
|
1084
|
+
"""The installed ``cvc5`` package's own distribution version
|
|
1085
|
+
(``importlib.metadata.version("cvc5")``), memoized process-wide —
|
|
1086
|
+
see :func:`_cvc5_package_version`. ``None`` when the package is not
|
|
1087
|
+
installed.
|
|
1088
|
+
"""
|
|
1089
|
+
return _cvc5_package_version()
|
|
1090
|
+
|
|
1091
|
+
def decide(self, formula: Node, premises: Sequence[Node] = (),
|
|
1092
|
+
timeout: int = 10000, **options) -> Verdict:
|
|
1093
|
+
"""Decide ``premises ⊨ formula`` and return a :class:`Verdict`.
|
|
1094
|
+
|
|
1095
|
+
Args:
|
|
1096
|
+
formula: the goal.
|
|
1097
|
+
premises: entailment premises (``⊨ formula`` when empty).
|
|
1098
|
+
timeout: milliseconds; forwarded to cvc5's ``tlimit-per`` (the
|
|
1099
|
+
limit of the one query this backend asks) AND ``tlimit``
|
|
1100
|
+
options -- ``tlimit`` alone was measured not to stop a
|
|
1101
|
+
non-terminating instantiation chain at all, so the per-query
|
|
1102
|
+
option is what makes the budget real (``0``/negative disables
|
|
1103
|
+
the limit, matching cvc5's own "unlimited" default).
|
|
1104
|
+
**options: ``logic`` overrides the SMT-LIB logic string handed
|
|
1105
|
+
to cvc5 (default ``"UF"`` — what any classical FOL/MSFOL
|
|
1106
|
+
fragment ``to_z3`` produces IS: a single uninterpreted sort
|
|
1107
|
+
with equality and uninterpreted functions/predicates, never
|
|
1108
|
+
arithmetic. It was ``"ALL"`` until 0.30.0, under which cvc5
|
|
1109
|
+
knows hundreds of theory symbols by name, and a predicate
|
|
1110
|
+
``<`` or a function ``+`` / ``select`` / ``sin`` of this
|
|
1111
|
+
kit — uninterpreted here — ended the PROCESS with a native
|
|
1112
|
+
access violation. A kit symbol named like a symbol of an
|
|
1113
|
+
SMT-LIB theory is renamed whatever the logic — see
|
|
1114
|
+
:data:`_SMTLIB_THEORIES`); ``random_seed``
|
|
1115
|
+
overrides cvc5's search seed (default ``42``, for
|
|
1116
|
+
reproducible verdicts across runs); ``proof`` (default
|
|
1117
|
+
``False``) asks for the Alethe proof text of a PROVED verdict,
|
|
1118
|
+
produced by a second solver run in a child process with a time
|
|
1119
|
+
limit (see the class docstring) — without it
|
|
1120
|
+
``Verdict.proof["text"]`` is ``None``, and with it the time of
|
|
1121
|
+
that second run is part of ``Verdict.wall_time``.
|
|
1122
|
+
|
|
1123
|
+
Returns:
|
|
1124
|
+
A :class:`Verdict` with ``status`` in
|
|
1125
|
+
``{"proved", "refuted", "unknown", "error"}``. Never raises for
|
|
1126
|
+
an in-contract ``Node`` — an unsupported fragment (``to_z3``
|
|
1127
|
+
raising ``NotImplementedError``, e.g. linear-logic/Lambek nodes,
|
|
1128
|
+
or a numeral and a constant of one text, which are one symbol, or a
|
|
1129
|
+
counting quantifier of a bound above 500, which the refusal names)
|
|
1130
|
+
comes back UNKNOWN/``"unsupported"``; a counting bound up to 500 is
|
|
1131
|
+
expanded and asked (nothing here recurses on its nesting); a formula
|
|
1132
|
+
nested deeper than the interpreter's recursion limit comes back
|
|
1133
|
+
ERROR/``"infra"`` with a detail that starts ``RecursionError``
|
|
1134
|
+
(``api.prove`` runs such a formula where it can be read, and names
|
|
1135
|
+
the depth when it cannot); any failure in the
|
|
1136
|
+
SMT-LIB2 round trip through cvc5 itself comes back
|
|
1137
|
+
ERROR/``"infra"`` rather than propagating. A REFUTED verdict's
|
|
1138
|
+
``countermodel["assignment"]`` names every symbol by its
|
|
1139
|
+
ORIGINAL kit-level name — see :func:`_reverse_map_assignment`
|
|
1140
|
+
and the module docstring's sanitisation section — never a
|
|
1141
|
+
digit-safe synthesised token, and never SMT-LIB2 ``|...|``
|
|
1142
|
+
quoting syntax wrapped around a non-ASCII one. A PROVED verdict
|
|
1143
|
+
whose proof text was asked for (``proof=True``) and could not be
|
|
1144
|
+
produced says why in ``detail`` and in ``proof["text_unavailable"]``.
|
|
1145
|
+
"""
|
|
1146
|
+
premises = list(premises)
|
|
1147
|
+
try:
|
|
1148
|
+
problem = premises + [formula]
|
|
1149
|
+
# The counting quantifiers are expanded, and the sort axioms made, BEFORE the
|
|
1150
|
+
# sanitiser runs, so that every name of the text -- the witnesses of an
|
|
1151
|
+
# expansion and of a non-emptiness axiom too -- is a symbol the sanitiser has
|
|
1152
|
+
# seen and keeps apart from every other (one namespace in SMT-LIB text; a
|
|
1153
|
+
# bound variable spelled like a predicate ends cvc5's process). The axioms are
|
|
1154
|
+
# built from the formulas as written: the sanitiser renames them together with
|
|
1155
|
+
# the premises and the goal, so the sort guard and the constant of a
|
|
1156
|
+
# membership atom carry the tokens the premises use (see the class docstring's
|
|
1157
|
+
# many-sorted-soundness paragraph).
|
|
1158
|
+
lowered, avoid = _lower_counting_for_smtlib(problem)
|
|
1159
|
+
sort_facts = list(sort_axioms(*problem, avoid_names=avoid))
|
|
1160
|
+
sanitised_nodes, name_map = _sanitize_many_for_smtlib(lowered + sort_facts)
|
|
1161
|
+
sanitised_premises = sanitised_nodes[:len(premises)]
|
|
1162
|
+
sanitised_formula = sanitised_nodes[len(premises)]
|
|
1163
|
+
sanitised_facts = sanitised_nodes[len(premises) + 1:]
|
|
1164
|
+
# The sanitiser has given every variable and every constant a token of its
|
|
1165
|
+
# own, so the environment names a variable as it is named: the SMT-LIB text
|
|
1166
|
+
# then holds the tokens, which is what the countermodel is read back through.
|
|
1167
|
+
env = Z3Env(variables_apart=False)
|
|
1168
|
+
z3_premises = [p.to_z3(env) for p in sanitised_premises]
|
|
1169
|
+
z3_formula = sanitised_formula.to_z3(env)
|
|
1170
|
+
z3_sort_facts = [axiom.to_z3(env) for axiom in sanitised_facts]
|
|
1171
|
+
except NotImplementedError as exc:
|
|
1172
|
+
return Verdict(UNKNOWN, self.name, reason="unsupported",
|
|
1173
|
+
solver_version=self.solver_version(), detail=str(exc))
|
|
1174
|
+
except RecursionError as exc:
|
|
1175
|
+
# a formula nested deeper than the interpreter's recursion limit: the dispatcher
|
|
1176
|
+
# (``api.prove``) names the depth for a verdict that starts like this one
|
|
1177
|
+
return Verdict(ERROR, self.name, reason="infra", solver_version=self.solver_version(),
|
|
1178
|
+
detail=f"{type(exc).__name__}: {exc}")
|
|
1179
|
+
|
|
1180
|
+
logic = options.pop("logic", "UF")
|
|
1181
|
+
random_seed = options.pop("random_seed", 42)
|
|
1182
|
+
want_proof = bool(options.pop("proof", False))
|
|
1183
|
+
# One importlib.metadata read, memoized process-wide (see
|
|
1184
|
+
# _cvc5_package_version) — cheap enough to call unconditionally on
|
|
1185
|
+
# every decide(), unlike the subprocess-spawning backends' own
|
|
1186
|
+
# solver_version() lookups.
|
|
1187
|
+
solver_version = self.solver_version()
|
|
1188
|
+
|
|
1189
|
+
try:
|
|
1190
|
+
(kind, payload), elapsed = _timed(
|
|
1191
|
+
lambda: self._run(z3_formula, z3_premises, z3_sort_facts,
|
|
1192
|
+
timeout, logic, random_seed))
|
|
1193
|
+
except Exception as exc: # noqa: BLE001 - cvc5/z3 raise plain RuntimeError/etc.
|
|
1194
|
+
return Verdict(ERROR, self.name, reason="infra",
|
|
1195
|
+
solver_version=solver_version,
|
|
1196
|
+
detail=f"{type(exc).__name__}: {exc}")
|
|
1197
|
+
|
|
1198
|
+
if kind == "unsat":
|
|
1199
|
+
reverse = name_map.reverse()
|
|
1200
|
+
text: Optional[str] = None
|
|
1201
|
+
why: Optional[str] = _PROOF_NOT_REQUESTED
|
|
1202
|
+
detail: Optional[str] = None
|
|
1203
|
+
if want_proof:
|
|
1204
|
+
(text, why), proof_seconds = _timed(
|
|
1205
|
+
lambda: _alethe_text(payload["smt2"], logic, random_seed, timeout))
|
|
1206
|
+
elapsed += proof_seconds
|
|
1207
|
+
if text is None:
|
|
1208
|
+
detail = f"no Alethe proof text: {why}"
|
|
1209
|
+
proof = {
|
|
1210
|
+
"kind": "cvc5_alethe",
|
|
1211
|
+
"text": _reverse_map_smtlib_text(text, reverse) if text is not None else None,
|
|
1212
|
+
# An unsat CORE, per cvc5's own getUnsatCore() — sound
|
|
1213
|
+
# (re-asserting just these terms is still unsat) but not
|
|
1214
|
+
# necessarily MINIMAL, exactly like Z3Backend's core; see
|
|
1215
|
+
# this backend's class docstring.
|
|
1216
|
+
"unsat_core": [_reverse_map_smtlib_text(term, reverse)
|
|
1217
|
+
for term in payload["unsat_core"]],
|
|
1218
|
+
}
|
|
1219
|
+
if text is None:
|
|
1220
|
+
proof["text_unavailable"] = why
|
|
1221
|
+
return Verdict(PROVED, self.name, wall_time=elapsed,
|
|
1222
|
+
solver_version=solver_version, proof=proof, detail=detail)
|
|
1223
|
+
if kind == "sat":
|
|
1224
|
+
assignment = _reverse_map_assignment(payload, name_map.reverse(),
|
|
1225
|
+
name_map.variable_tokens())
|
|
1226
|
+
return Verdict(REFUTED, self.name, wall_time=elapsed,
|
|
1227
|
+
solver_version=solver_version,
|
|
1228
|
+
countermodel={"kind": "cvc5_model", "assignment": assignment})
|
|
1229
|
+
# kind == "unknown"
|
|
1230
|
+
return Verdict(UNKNOWN, self.name, reason=payload["reason"], wall_time=elapsed,
|
|
1231
|
+
solver_version=solver_version, detail=payload["detail"])
|
|
1232
|
+
|
|
1233
|
+
@staticmethod
|
|
1234
|
+
def _run(z3_formula, z3_premises: Sequence, z3_sort_facts: Sequence, timeout: int,
|
|
1235
|
+
logic: str, random_seed: int):
|
|
1236
|
+
"""Serialise ``z3_premises``/``z3_sort_facts``/``¬z3_formula`` to
|
|
1237
|
+
SMT-LIB2 and decide with cvc5.
|
|
1238
|
+
|
|
1239
|
+
Each of ``z3_premises``, ``z3_sort_facts`` and ``Not(z3_formula)``
|
|
1240
|
+
becomes its OWN top-level Z3 ``.add()`` call, hence its OWN
|
|
1241
|
+
``(assert ...)`` line in ``Solver.to_smt2()`` and its OWN
|
|
1242
|
+
``assertFormula`` when replayed — see
|
|
1243
|
+
:func:`_sanitize_many_for_smtlib`'s docstring for why: cvc5's
|
|
1244
|
+
``getUnsatCore()`` reports relevance per top-level assertion, so
|
|
1245
|
+
this is what lets it exclude an irrelevant premise instead of
|
|
1246
|
+
always naming "the whole conjoined problem". Logically identical to
|
|
1247
|
+
asserting one folded ``(∧ premises ∧ sort facts) → φ`` implication (a
|
|
1248
|
+
solver conjoins every assertion regardless of how many commands they
|
|
1249
|
+
arrived in) — this changes only the unsat-core bookkeeping.
|
|
1250
|
+
``z3_premises`` are asserted FIRST, then ``z3_sort_facts`` (the
|
|
1251
|
+
non-emptiness of every sort AND the membership atom of every sorted
|
|
1252
|
+
constant, in one list), then the negated goal — :meth:`decide` keeps
|
|
1253
|
+
them as separate arguments (rather than one pre-concatenated list) so
|
|
1254
|
+
this method knows exactly which assertion INDICES are the synthetic
|
|
1255
|
+
sort facts once it needs to exclude them from the reported core below.
|
|
1256
|
+
The facts are asserted OUTSIDE the negated goal: a membership atom under
|
|
1257
|
+
the goal's negation would be one more thing to prove.
|
|
1258
|
+
|
|
1259
|
+
Returns ``("unsat", payload)``, ``("sat", entries)``, or
|
|
1260
|
+
``("unknown", {"reason": ..., "detail": ...})``. The ``unsat`` payload
|
|
1261
|
+
is ``{"unsat_core": [term_text, ...], "smt2": text}``: the core, and the
|
|
1262
|
+
SMT-LIB2 text of the problem, which is what the proof run of
|
|
1263
|
+
:func:`_alethe_text` needs. ``entries`` is one ``(declared_term_text,
|
|
1264
|
+
arity, range, value_text)`` per declared symbol, ``range`` being
|
|
1265
|
+
``"Bool"`` for a predicate and ``"S"`` for a function or constant (what
|
|
1266
|
+
:func:`~unicode_logic_kit.atp.z3_models.declaration_keys` reads).
|
|
1267
|
+
Every string is still in cvc5's OWN (possibly sanitised) symbol names —
|
|
1268
|
+
:meth:`decide` reverse-maps it to original kit-level names; this method
|
|
1269
|
+
stays a pure cvc5-API wrapper.
|
|
1270
|
+
|
|
1271
|
+
**No proof is built here.** ``produce-proofs`` is off: the verdict and
|
|
1272
|
+
the unsat core need none (``getUnsatCore`` works without it, measured),
|
|
1273
|
+
and the proof PRINTER can end the process (see :func:`_alethe_text`), so
|
|
1274
|
+
the Alethe text is produced elsewhere, in a child process, when asked
|
|
1275
|
+
for. Nothing this method calls is known to end the process.
|
|
1276
|
+
|
|
1277
|
+
``unsat_core`` NEVER contains one of ``z3_sort_facts`` — those are
|
|
1278
|
+
background MSFOL convention (every sort is non-empty, a sorted constant
|
|
1279
|
+
is in its sort), never one of the caller's own premises, mirroring how
|
|
1280
|
+
``atp.protocol._z3_track_and_check`` asserts the identical axioms
|
|
1281
|
+
UNTRACKED so :class:`~unicode_logic_kit.atp.protocol.Z3Backend`'s own
|
|
1282
|
+
``z3_unsat_core`` can never name them either (see that function's
|
|
1283
|
+
docstring). cvc5's SMT-LIB2-replay route here has no tagged-boolean
|
|
1284
|
+
``assert_and_track`` equivalent to lean on, so exclusion instead
|
|
1285
|
+
matches each ``getUnsatCore()`` term against the sort-fact slice
|
|
1286
|
+
of ``solver.getAssertions()`` (the SAME solver, in the SAME order
|
|
1287
|
+
just asserted above) by cvc5 ``Term`` equality — robust to cvc5's
|
|
1288
|
+
own core/assertion printers disagreeing on whitespace, and exact
|
|
1289
|
+
unless a CALLER-supplied premise is itself syntactically identical
|
|
1290
|
+
to one of the synthetic facts (a premise that reads ``∃x (Ghost(x))``
|
|
1291
|
+
for a sort literally named ``Ghost``, or ``Human(socrates)`` for a
|
|
1292
|
+
constant ``socrates:Human`` — the latter is common), in which case that
|
|
1293
|
+
coincidental duplicate is absorbed into the background fact instead of
|
|
1294
|
+
being listed — harmless, since a premise that equals a background fact
|
|
1295
|
+
adds nothing to it, so the reported core together with the background
|
|
1296
|
+
facts is still unsat (``unsat_core`` is already documented as
|
|
1297
|
+
sound-but-not-necessarily-minimal).
|
|
1298
|
+
|
|
1299
|
+
Core production is enabled unconditionally (per-``Solver``-instance
|
|
1300
|
+
only — this class never touches a process-wide cvc5 setting), but
|
|
1301
|
+
reading it back (:meth:`cvc5.Solver.getUnsatCore`/``getAssertions``) is
|
|
1302
|
+
best-effort: a failure there must not turn a genuinely sound ``unsat``
|
|
1303
|
+
result into anything but PROVED, so it degrades to an empty core
|
|
1304
|
+
rather than raising.
|
|
1305
|
+
"""
|
|
1306
|
+
import cvc5
|
|
1307
|
+
from z3 import Solver as Z3Solver, Not as _ZNot
|
|
1308
|
+
|
|
1309
|
+
z3_solver = Z3Solver()
|
|
1310
|
+
for z3_premise in z3_premises:
|
|
1311
|
+
z3_solver.add(z3_premise)
|
|
1312
|
+
for z3_axiom in z3_sort_facts:
|
|
1313
|
+
z3_solver.add(z3_axiom)
|
|
1314
|
+
z3_solver.add(_ZNot(z3_formula))
|
|
1315
|
+
smt2_text = z3_solver.to_smt2()
|
|
1316
|
+
|
|
1317
|
+
solver = cvc5.Solver()
|
|
1318
|
+
solver.setLogic(logic)
|
|
1319
|
+
solver.setOption("produce-models", "true")
|
|
1320
|
+
solver.setOption("produce-unsat-cores", "true")
|
|
1321
|
+
solver.setOption("seed", str(random_seed))
|
|
1322
|
+
if timeout and timeout > 0:
|
|
1323
|
+
# BOTH options, because they do not bound the same thing. ``tlimit``
|
|
1324
|
+
# is the cumulative limit of the solver instance and ``tlimit-per``
|
|
1325
|
+
# the limit of ONE query; this method asks exactly one, so the two
|
|
1326
|
+
# are the same budget. Measured (cvc5 1.3.4) on a one-GCI
|
|
1327
|
+
# description-logic image -- ``∀x (OwlThing(x) → ∃y (r(x,y) ∧
|
|
1328
|
+
# C(y)))`` with ``r`` typed into OwlThing, whose instantiation chain
|
|
1329
|
+
# never ends: ``tlimit=3000`` alone never returned (a hard cap of
|
|
1330
|
+
# 60 s was hit, and the default prover chain with it), while
|
|
1331
|
+
# ``tlimit-per=3000`` came back ``unknown (TIMEOUT)`` after 3.09 s.
|
|
1332
|
+
# So the PER-QUERY option is the one that enforces the budget.
|
|
1333
|
+
solver.setOption("tlimit", str(timeout))
|
|
1334
|
+
solver.setOption("tlimit-per", str(timeout))
|
|
1335
|
+
|
|
1336
|
+
parser = cvc5.InputParser(solver)
|
|
1337
|
+
symbol_manager = parser.getSymbolManager()
|
|
1338
|
+
parser.setStringInput(cvc5.InputLanguage.SMT_LIB_2_6, smt2_text, "cvc5_backend")
|
|
1339
|
+
|
|
1340
|
+
while True:
|
|
1341
|
+
command = parser.nextCommand()
|
|
1342
|
+
if command.isNull():
|
|
1343
|
+
break
|
|
1344
|
+
# The (check-sat) command in the replayed text is skipped so we
|
|
1345
|
+
# get a real cvc5.Result from checkSat() below, not its stringified
|
|
1346
|
+
# form from Command.invoke().
|
|
1347
|
+
if command.getCommandName() == "check-sat":
|
|
1348
|
+
continue
|
|
1349
|
+
command.invoke(solver, symbol_manager)
|
|
1350
|
+
|
|
1351
|
+
result = solver.checkSat()
|
|
1352
|
+
|
|
1353
|
+
if result.isUnsat():
|
|
1354
|
+
try:
|
|
1355
|
+
core = solver.getUnsatCore()
|
|
1356
|
+
# solver.getAssertions() replays in the SAME order the three
|
|
1357
|
+
# groups were asserted above: z3_premises, then z3_sort_facts,
|
|
1358
|
+
# then Not(z3_formula) — slice out exactly the sort-fact
|
|
1359
|
+
# group so it can be excluded from the reported core by
|
|
1360
|
+
# cvc5 Term equality (see this method's docstring).
|
|
1361
|
+
assertions = solver.getAssertions()
|
|
1362
|
+
n_premises = len(z3_premises)
|
|
1363
|
+
fact_terms = list(assertions[n_premises:n_premises + len(z3_sort_facts)])
|
|
1364
|
+
core_terms = [str(term) for term in core if term not in fact_terms]
|
|
1365
|
+
except Exception: # noqa: BLE001 - best-effort certificate, must not sink a sound PROVED verdict
|
|
1366
|
+
core_terms = []
|
|
1367
|
+
return "unsat", {"unsat_core": core_terms, "smt2": smt2_text}
|
|
1368
|
+
if result.isSat():
|
|
1369
|
+
entries = []
|
|
1370
|
+
for term in symbol_manager.getDeclaredTerms():
|
|
1371
|
+
try:
|
|
1372
|
+
value = str(solver.getValue(term))
|
|
1373
|
+
sort = term.getSort()
|
|
1374
|
+
if sort.isFunction():
|
|
1375
|
+
arity, codomain = sort.getFunctionArity(), sort.getFunctionCodomainSort()
|
|
1376
|
+
else:
|
|
1377
|
+
arity, codomain = 0, sort
|
|
1378
|
+
entries.append((str(term), arity, "Bool" if codomain.isBoolean() else "S", value))
|
|
1379
|
+
except Exception: # noqa: BLE001 - best-effort witness, one symbol must not blank out the rest
|
|
1380
|
+
continue
|
|
1381
|
+
return "sat", entries
|
|
1382
|
+
|
|
1383
|
+
explanation = result.getUnknownExplanation()
|
|
1384
|
+
reason = "timeout" if explanation == cvc5.UnknownExplanation.TIMEOUT else "incomplete"
|
|
1385
|
+
return "unknown", {"reason": reason, "detail": str(explanation)}
|