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.
Files changed (237) hide show
  1. unicode_logic_kit/__init__.py +385 -0
  2. unicode_logic_kit/__main__.py +520 -0
  3. unicode_logic_kit/_deadline.py +219 -0
  4. unicode_logic_kit/ace/__init__.py +126 -0
  5. unicode_logic_kit/ace/_align.py +135 -0
  6. unicode_logic_kit/ace/chem_lexicon.py +128 -0
  7. unicode_logic_kit/ace/drs_reader.py +570 -0
  8. unicode_logic_kit/ace/mapping.py +666 -0
  9. unicode_logic_kit/ace/reverse_modal.py +138 -0
  10. unicode_logic_kit/ace/runner.py +551 -0
  11. unicode_logic_kit/ace/translate.py +452 -0
  12. unicode_logic_kit/ace/verbalize.py +1070 -0
  13. unicode_logic_kit/api.py +1284 -0
  14. unicode_logic_kit/atp/__init__.py +177 -0
  15. unicode_logic_kit/atp/_ascii_names.py +113 -0
  16. unicode_logic_kit/atp/_html.py +72 -0
  17. unicode_logic_kit/atp/_substructural_input.py +228 -0
  18. unicode_logic_kit/atp/_tff_problem.py +715 -0
  19. unicode_logic_kit/atp/_tptp_problem.py +1111 -0
  20. unicode_logic_kit/atp/_writer_support.py +289 -0
  21. unicode_logic_kit/atp/clingo_backend.py +1180 -0
  22. unicode_logic_kit/atp/cvc5_backend.py +1385 -0
  23. unicode_logic_kit/atp/eprover_backend.py +732 -0
  24. unicode_logic_kit/atp/finite_domain.py +1055 -0
  25. unicode_logic_kit/atp/fitch.py +1547 -0
  26. unicode_logic_kit/atp/fitch_search.py +551 -0
  27. unicode_logic_kit/atp/hets_backend.py +339 -0
  28. unicode_logic_kit/atp/hybrid_down.py +120 -0
  29. unicode_logic_kit/atp/incremental.py +250 -0
  30. unicode_logic_kit/atp/kripke_enum.py +741 -0
  31. unicode_logic_kit/atp/lambek.py +436 -0
  32. unicode_logic_kit/atp/leo3_backend.py +332 -0
  33. unicode_logic_kit/atp/linear.py +738 -0
  34. unicode_logic_kit/atp/lj.py +705 -0
  35. unicode_logic_kit/atp/logic_backends.py +566 -0
  36. unicode_logic_kit/atp/ltl_tableau.py +1084 -0
  37. unicode_logic_kit/atp/minizinc_backend.py +1402 -0
  38. unicode_logic_kit/atp/modal_tableau.py +1382 -0
  39. unicode_logic_kit/atp/nanocop_backend.py +410 -0
  40. unicode_logic_kit/atp/portfolio.py +489 -0
  41. unicode_logic_kit/atp/protocol.py +1803 -0
  42. unicode_logic_kit/atp/prover9_entailment.py +1153 -0
  43. unicode_logic_kit/atp/resolution.py +1376 -0
  44. unicode_logic_kit/atp/resolution_check.py +1114 -0
  45. unicode_logic_kit/atp/sequent.py +1050 -0
  46. unicode_logic_kit/atp/tableau.py +921 -0
  47. unicode_logic_kit/atp/tableau_check.py +543 -0
  48. unicode_logic_kit/atp/tptp_ncl.py +811 -0
  49. unicode_logic_kit/atp/tptp_tff.py +1546 -0
  50. unicode_logic_kit/atp/tstp.py +1333 -0
  51. unicode_logic_kit/atp/tstp_check.py +1096 -0
  52. unicode_logic_kit/atp/twee_backend.py +236 -0
  53. unicode_logic_kit/atp/twee_check.py +711 -0
  54. unicode_logic_kit/atp/twee_entailment.py +953 -0
  55. unicode_logic_kit/atp/vampire_entailment.py +540 -0
  56. unicode_logic_kit/atp/z3_arith.py +470 -0
  57. unicode_logic_kit/atp/z3_equivalence.py +36 -0
  58. unicode_logic_kit/atp/z3_fuzzy.py +362 -0
  59. unicode_logic_kit/atp/z3_input.py +500 -0
  60. unicode_logic_kit/atp/z3_models.py +208 -0
  61. unicode_logic_kit/chem/__init__.py +88 -0
  62. unicode_logic_kit/chem/_naming.py +284 -0
  63. unicode_logic_kit/chem/cache.py +185 -0
  64. unicode_logic_kit/chem/interop.py +244 -0
  65. unicode_logic_kit/chem/mol.py +525 -0
  66. unicode_logic_kit/chem/signature.py +112 -0
  67. unicode_logic_kit/comorphism.py +497 -0
  68. unicode_logic_kit/dl/__init__.py +384 -0
  69. unicode_logic_kit/dl/classification.py +227 -0
  70. unicode_logic_kit/dl/concepts.py +632 -0
  71. unicode_logic_kit/dl/datatypes.py +818 -0
  72. unicode_logic_kit/dl/owl_functional.py +2433 -0
  73. unicode_logic_kit/dl/owl_manchester.py +1637 -0
  74. unicode_logic_kit/dl/owl_reasoner.py +790 -0
  75. unicode_logic_kit/dl/parser.py +391 -0
  76. unicode_logic_kit/dl/tableau.py +4048 -0
  77. unicode_logic_kit/dl/translate.py +2704 -0
  78. unicode_logic_kit/drt/__init__.py +94 -0
  79. unicode_logic_kit/drt/export.py +179 -0
  80. unicode_logic_kit/drt/nodes.py +506 -0
  81. unicode_logic_kit/drt/parser.py +965 -0
  82. unicode_logic_kit/drt/resolve.py +195 -0
  83. unicode_logic_kit/drt/reverse.py +175 -0
  84. unicode_logic_kit/eval/__init__.py +106 -0
  85. unicode_logic_kit/eval/batch.py +382 -0
  86. unicode_logic_kit/eval/canonical.py +663 -0
  87. unicode_logic_kit/eval/chem_batch.py +606 -0
  88. unicode_logic_kit/eval/converses.py +200 -0
  89. unicode_logic_kit/eval/datasets/__init__.py +136 -0
  90. unicode_logic_kit/eval/datasets/_base.py +263 -0
  91. unicode_logic_kit/eval/datasets/_proofwriter_proof.py +422 -0
  92. unicode_logic_kit/eval/datasets/c3po.py +678 -0
  93. unicode_logic_kit/eval/datasets/folio.py +158 -0
  94. unicode_logic_kit/eval/datasets/fracas.py +418 -0
  95. unicode_logic_kit/eval/datasets/groves.py +191 -0
  96. unicode_logic_kit/eval/datasets/logicbench.py +467 -0
  97. unicode_logic_kit/eval/datasets/logicnli.py +303 -0
  98. unicode_logic_kit/eval/datasets/malls.py +133 -0
  99. unicode_logic_kit/eval/datasets/pfolio.py +594 -0
  100. unicode_logic_kit/eval/datasets/pmb.py +242 -0
  101. unicode_logic_kit/eval/datasets/prontoqa.py +611 -0
  102. unicode_logic_kit/eval/datasets/proofwriter.py +1431 -0
  103. unicode_logic_kit/eval/datasets/proverqa.py +674 -0
  104. unicode_logic_kit/eval/datasets/willow.py +478 -0
  105. unicode_logic_kit/eval/equivalence.py +466 -0
  106. unicode_logic_kit/eval/exercise_gen.py +533 -0
  107. unicode_logic_kit/eval/explain.py +791 -0
  108. unicode_logic_kit/eval/generality.py +750 -0
  109. unicode_logic_kit/eval/metric_hf.py +458 -0
  110. unicode_logic_kit/eval/predicate_match.py +343 -0
  111. unicode_logic_kit/eval/theory_check.py +1170 -0
  112. unicode_logic_kit/eval/validate.py +306 -0
  113. unicode_logic_kit/fol/__init__.py +177 -0
  114. unicode_logic_kit/fol/_atom_keys.py +510 -0
  115. unicode_logic_kit/fol/_fol_nodes.py +3586 -0
  116. unicode_logic_kit/fol/_free_parameters.py +105 -0
  117. unicode_logic_kit/fol/_ho_nodes.py +448 -0
  118. unicode_logic_kit/fol/_hybrid_nodes.py +308 -0
  119. unicode_logic_kit/fol/_identifiers.py +1091 -0
  120. unicode_logic_kit/fol/_lambek_nodes.py +112 -0
  121. unicode_logic_kit/fol/_linear_nodes.py +352 -0
  122. unicode_logic_kit/fol/_modal_nodes.py +1467 -0
  123. unicode_logic_kit/fol/_msfl_nodes.py +2196 -0
  124. unicode_logic_kit/fol/_numeral_symbols.py +231 -0
  125. unicode_logic_kit/fol/_so_nodes.py +200 -0
  126. unicode_logic_kit/fol/_symbol_names.py +81 -0
  127. unicode_logic_kit/fol/_team_nodes.py +181 -0
  128. unicode_logic_kit/fol/_tptp_symbols.py +551 -0
  129. unicode_logic_kit/fol/_truth_constants.py +117 -0
  130. unicode_logic_kit/fol/casl_export.py +1135 -0
  131. unicode_logic_kit/fol/casl_import.py +929 -0
  132. unicode_logic_kit/fol/derivation.py +367 -0
  133. unicode_logic_kit/fol/dialect_detect.py +70 -0
  134. unicode_logic_kit/fol/dialect_repair.py +537 -0
  135. unicode_logic_kit/fol/frames.py +637 -0
  136. unicode_logic_kit/fol/grammars/terminals.lark +31 -0
  137. unicode_logic_kit/fol/lambda_tools.py +297 -0
  138. unicode_logic_kit/fol/latex_input.py +429 -0
  139. unicode_logic_kit/fol/modal_translation.py +944 -0
  140. unicode_logic_kit/fol/msflparser.py +1033 -0
  141. unicode_logic_kit/fol/naming.py +422 -0
  142. unicode_logic_kit/fol/nodes.py +241 -0
  143. unicode_logic_kit/fol/normalforms.py +492 -0
  144. unicode_logic_kit/fol/pal.py +287 -0
  145. unicode_logic_kit/fol/prolog_export.py +566 -0
  146. unicode_logic_kit/fol/prolog_input.py +505 -0
  147. unicode_logic_kit/fol/prover9_input.py +1325 -0
  148. unicode_logic_kit/fol/qml.py +1760 -0
  149. unicode_logic_kit/fol/qmltp_input.py +525 -0
  150. unicode_logic_kit/fol/sanitize.py +221 -0
  151. unicode_logic_kit/fol/serialize.py +79 -0
  152. unicode_logic_kit/fol/signature.py +1290 -0
  153. unicode_logic_kit/fol/simplify_check.py +544 -0
  154. unicode_logic_kit/fol/spans.py +594 -0
  155. unicode_logic_kit/fol/tptp_input.py +1503 -0
  156. unicode_logic_kit/fol/tptp_repair.py +941 -0
  157. unicode_logic_kit/fol/unification.py +157 -0
  158. unicode_logic_kit/fol/verbalize.py +263 -0
  159. unicode_logic_kit/hets/__init__.py +163 -0
  160. unicode_logic_kit/hets/bridge.py +142 -0
  161. unicode_logic_kit/hets/client.py +748 -0
  162. unicode_logic_kit/hets/docker.py +420 -0
  163. unicode_logic_kit/hets/dol.py +712 -0
  164. unicode_logic_kit/hets/haskell_json.py +355 -0
  165. unicode_logic_kit/hets/owl_backend.py +794 -0
  166. unicode_logic_kit/hets/owl_cli.py +598 -0
  167. unicode_logic_kit/hets/symbols.py +512 -0
  168. unicode_logic_kit/hol/__init__.py +140 -0
  169. unicode_logic_kit/hol/_ho_common.py +323 -0
  170. unicode_logic_kit/hol/_isabelle_binders.py +125 -0
  171. unicode_logic_kit/hol/classical.py +812 -0
  172. unicode_logic_kit/hol/deepshallow/__init__.py +45 -0
  173. unicode_logic_kit/hol/deepshallow/_common.py +177 -0
  174. unicode_logic_kit/hol/deepshallow/conditional.py +225 -0
  175. unicode_logic_kit/hol/deepshallow/intuitionistic.py +181 -0
  176. unicode_logic_kit/hol/deepshallow/modal.py +217 -0
  177. unicode_logic_kit/hol/deepshallow/qml.py +406 -0
  178. unicode_logic_kit/hol/deepshallow/relevant.py +206 -0
  179. unicode_logic_kit/hol/free.py +753 -0
  180. unicode_logic_kit/hol/goedel.py +336 -0
  181. unicode_logic_kit/hol/ho_modal.py +1743 -0
  182. unicode_logic_kit/hol/intuitionistic.py +403 -0
  183. unicode_logic_kit/hol/isabelle_conditional.py +593 -0
  184. unicode_logic_kit/hol/isabelle_modal.py +1908 -0
  185. unicode_logic_kit/hol/isabelle_relevant.py +412 -0
  186. unicode_logic_kit/hol/isabelle_runner.py +1147 -0
  187. unicode_logic_kit/hol/isabelle_substructural.py +884 -0
  188. unicode_logic_kit/hol/lean.py +1018 -0
  189. unicode_logic_kit/hol/manyvalued.py +921 -0
  190. unicode_logic_kit/hol/secondorder.py +687 -0
  191. unicode_logic_kit/hol/thf_modal.py +941 -0
  192. unicode_logic_kit/hol/thirdorder.py +397 -0
  193. unicode_logic_kit/ilp/__init__.py +89 -0
  194. unicode_logic_kit/ilp/readback.py +389 -0
  195. unicode_logic_kit/ilp/separation.py +153 -0
  196. unicode_logic_kit/ilp/task.py +730 -0
  197. unicode_logic_kit/logic.py +163 -0
  198. unicode_logic_kit/mcp/__init__.py +28 -0
  199. unicode_logic_kit/mcp/__main__.py +5 -0
  200. unicode_logic_kit/mcp/chem_tools.py +1031 -0
  201. unicode_logic_kit/mcp/server.py +2453 -0
  202. unicode_logic_kit/mcp/syntax_spec.py +681 -0
  203. unicode_logic_kit/prob/__init__.py +53 -0
  204. unicode_logic_kit/prob/_bdd.py +225 -0
  205. unicode_logic_kit/prob/_column_gen.py +668 -0
  206. unicode_logic_kit/prob/distribution.py +686 -0
  207. unicode_logic_kit/prob/nilsson.py +470 -0
  208. unicode_logic_kit/py.typed +0 -0
  209. unicode_logic_kit/semantics/__init__.py +137 -0
  210. unicode_logic_kit/semantics/_modal_reject.py +156 -0
  211. unicode_logic_kit/semantics/action_models.py +466 -0
  212. unicode_logic_kit/semantics/asp_models.py +1200 -0
  213. unicode_logic_kit/semantics/conditional.py +580 -0
  214. unicode_logic_kit/semantics/dynamic_epistemic.py +95 -0
  215. unicode_logic_kit/semantics/free_logic.py +913 -0
  216. unicode_logic_kit/semantics/fuzzy.py +384 -0
  217. unicode_logic_kit/semantics/fuzzy_kripke.py +442 -0
  218. unicode_logic_kit/semantics/intuitionistic.py +581 -0
  219. unicode_logic_kit/semantics/kripke.py +1139 -0
  220. unicode_logic_kit/semantics/manyvalued.py +580 -0
  221. unicode_logic_kit/semantics/matrix.py +342 -0
  222. unicode_logic_kit/semantics/model_eval.py +1135 -0
  223. unicode_logic_kit/semantics/modelfinder.py +1036 -0
  224. unicode_logic_kit/semantics/nonmonotonic.py +372 -0
  225. unicode_logic_kit/semantics/relevant.py +331 -0
  226. unicode_logic_kit/semantics/secondorder.py +657 -0
  227. unicode_logic_kit/semantics/structures.py +352 -0
  228. unicode_logic_kit/semantics/tarski.py +975 -0
  229. unicode_logic_kit/semantics/team.py +315 -0
  230. unicode_logic_kit/semantics/team_translation.py +416 -0
  231. unicode_logic_kit/semantics/thirdorder.py +358 -0
  232. unicode_logic_kit/semantics/tnorm.py +85 -0
  233. unicode_logic_kit/semantics/truthtable.py +201 -0
  234. unicode_logic_kit-0.31.0.dist-info/METADATA +333 -0
  235. unicode_logic_kit-0.31.0.dist-info/RECORD +237 -0
  236. unicode_logic_kit-0.31.0.dist-info/WHEEL +4 -0
  237. 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)}