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,410 @@
1
+ """nanoCoP-M as an opt-in modal backend with a MANDATORY cross-check.
2
+
3
+ nanoCoP-M (Jens Otten, http://leancop.de/nanocop-m/, GPL) is one of the
4
+ very few provers that decide FIRST-ORDER modal logic natively — with
5
+ explicit domain conditions (constant / cumulative / varying), which the
6
+ kit's own QML embedding can only approximate. That power comes with two
7
+ sharp edges this module manages instead of hiding:
8
+
9
+ 1. **It is a Prolog program the user configures.** The shipped
10
+ ``nanocopm.sh`` hard-codes LOGIC (d/t/s4/s5) and DOMAIN
11
+ (const/cumul/vary) as shell variables and needs an ECLiPSe or
12
+ SWI-Prolog install. This backend therefore never guesses an
13
+ installation: discovery is ``$UFK_NANOCOP_CMD`` (the user's configured
14
+ wrapper script; prefix ``wsl:`` for the WSL route) → ``nanocopm.sh`` on
15
+ PATH → the same name inside WSL. The script prints which configuration
16
+ actually ran (``… is a modal (s4/cumul) Theorem``) and this backend
17
+ PARSES that back: if the caller requested ``logic=``/``domain=``
18
+ options and the script's report disagrees, the verdict is an ERROR
19
+ telling the user to reconfigure their script — never a silently wrong
20
+ logic.
21
+ 2. **The roadmap's mandatory cross-check.** Connection provers earn their
22
+ keep exactly where other methods time out, so their claims must be
23
+ independently checked (the kit granted nanoCoP-M opt-in status ONLY
24
+ under this condition). Policy, asymmetric on purpose:
25
+
26
+ * ``Theorem`` → the kit re-runs ``modal-tableau`` on the same problem.
27
+ Agreement or an inconclusive tableau keeps PROVED (with the
28
+ cross-check outcome in ``detail``); a tableau REFUTATION raises the
29
+ soundness alarm as an ERROR verdict — two definitive answers may
30
+ never disagree silently.
31
+ * ``Non-Theorem`` → only a ``kripke-enum`` COUNTERMODEL turns it into
32
+ REFUTED (then the countermodel is the certificate, carried in the
33
+ verdict); without one the claim stays UNKNOWN/``incomplete`` with
34
+ the Non-Theorem noted in ``detail``. First-order modal validity is
35
+ undecidable, so a proof-search failure — even from the script's
36
+ COMP=y "complete" strategy — is not a refutation certificate this
37
+ kit will vouch for on its own.
38
+
39
+ Input syntax (from the shipped ReadMe, v2.0, verified): the problem file
40
+ contains ``f(<formula>).`` with ``~`` negation, ``#`` box, ``*`` diamond,
41
+ ``,`` conjunction, ``;`` disjunction, ``=>``, ``<=>``, ``all X:`` /
42
+ ``ex X:`` quantifiers; atoms are Prolog terms. Kit names map by
43
+ first-letter case flip (predicate ``P``→``p``, variable ``x``→``X``);
44
+ the mapping must be injective — ``P``/``p`` colliding on ``p`` refuses
45
+ loudly (the same discipline as the NXF exporter's collision check).
46
+ Fragment: Box/Diamond + classical connectives + ∀/∃ over
47
+ predicates/constants/variables. Everything else (Knows/temporal/deontic
48
+ operators, functions with clashing names, sorted nodes, …) is refused as
49
+ unsupported, never approximated.
50
+
51
+ Output contract (from ``nanocopm.sh``): exit 0 + ``… is a modal
52
+ (<logic>/<domain>) Theorem`` on proof; exit 1 + ``… Non-Theorem`` on
53
+ disproof (printed only by COMP=y strategy slots); exit 2 / ``Timeout`` on
54
+ budget exhaustion.
55
+ """
56
+
57
+ import os
58
+ import re
59
+ import shutil
60
+ import subprocess
61
+ import tempfile
62
+ import time
63
+ from typing import Dict, List, Optional, Sequence, Tuple
64
+
65
+ from ..fol.nodes import (
66
+ And, Atom, Box, Constant, Diamond, Function, Iff, Implies, Node, Not,
67
+ Or, Quantifier, Variable,
68
+ )
69
+ from ..fol._truth_constants import refuse_truth_constants
70
+ from .protocol import ProverBackend, Verdict, PROVED, REFUTED, UNKNOWN, ERROR
71
+
72
+ __all__ = ["NanocopBackend", "to_nanocop", "nanocop_available"]
73
+
74
+ _RESULT_RE = re.compile(
75
+ r"is (?:a )?modal \(([^)/]+)/([^)]+)\) (Theorem|Non-Theorem)")
76
+
77
+
78
+ class _NameMap:
79
+ """Injective kit→Prolog name mapping with loud collision refusal."""
80
+
81
+ def __init__(self):
82
+ self._forward: Dict[Tuple[str, str], str] = {}
83
+ self._taken: Dict[str, Tuple[str, str]] = {}
84
+
85
+ def get(self, namespace: str, kit_name: str, prolog_name: str) -> str:
86
+ key = (namespace, kit_name)
87
+ if key in self._forward:
88
+ return self._forward[key]
89
+ owner = self._taken.get(prolog_name)
90
+ if owner is not None and owner != key:
91
+ raise NotImplementedError(
92
+ f"nanocop: refusing to alias — kit {namespace} '{kit_name}' "
93
+ f"and {owner[0]} '{owner[1]}' would both render as Prolog "
94
+ f"name '{prolog_name}'.")
95
+ self._forward[key] = prolog_name
96
+ self._taken[prolog_name] = key
97
+ return prolog_name
98
+
99
+
100
+ def _term(node: Node, names: _NameMap, bound: Dict[str, str]) -> str:
101
+ cls = type(node).__name__
102
+ if cls == "Variable":
103
+ if node.name not in bound:
104
+ raise ValueError(
105
+ f"nanocop: free variable '{node.name}' — the formula must "
106
+ "be closed.")
107
+ return bound[node.name]
108
+ if cls == "Constant":
109
+ lowered = node.name[0].lower() + node.name[1:]
110
+ return names.get("constant", node.name, lowered)
111
+ if cls == "Function":
112
+ head = names.get("function", node.name,
113
+ node.name[0].lower() + node.name[1:])
114
+ args = ", ".join(_term(a, names, bound) for a in node.args)
115
+ return f"{head}({args})"
116
+ raise NotImplementedError(
117
+ f"nanocop: term node {cls!r} is outside the supported fragment.")
118
+
119
+
120
+ def _formula(node: Node, names: _NameMap, bound: Dict[str, str]) -> str:
121
+ cls = type(node).__name__
122
+ if cls == "Atom":
123
+ refuse_truth_constants(
124
+ [node], "nanocop",
125
+ "nanoCoP-M's f(...) input language has no truth-constant syntax, so "
126
+ "$true / $false would reach it as the name of an unrelated symbol")
127
+ if node.predicate == "=":
128
+ raise NotImplementedError(
129
+ "nanocop: equality is not in the supported fragment "
130
+ "(nanoCoP-M has no built-in equality handling).")
131
+ head = names.get("predicate", node.predicate,
132
+ node.predicate[0].lower() + node.predicate[1:])
133
+ if not node.args:
134
+ return head
135
+ args = ", ".join(_term(a, names, bound) for a in node.args)
136
+ return f"{head}({args})"
137
+ if cls == "Not":
138
+ return f"(~ {_formula(node.formula, names, bound)})"
139
+ if cls == "Box":
140
+ return f"(# {_formula(node.formula, names, bound)})"
141
+ if cls == "Diamond":
142
+ return f"(* {_formula(node.formula, names, bound)})"
143
+ if cls == "And":
144
+ return (f"({_formula(node.left, names, bound)} , "
145
+ f"{_formula(node.right, names, bound)})")
146
+ if cls == "Or":
147
+ return (f"({_formula(node.left, names, bound)} ; "
148
+ f"{_formula(node.right, names, bound)})")
149
+ if cls == "Implies":
150
+ return (f"({_formula(node.left, names, bound)} => "
151
+ f"{_formula(node.right, names, bound)})")
152
+ if cls == "Iff":
153
+ return (f"({_formula(node.left, names, bound)} <=> "
154
+ f"{_formula(node.right, names, bound)})")
155
+ if cls == "Quantifier":
156
+ # Variables go through the SAME injective map as every other name
157
+ # class (review-confirmed hole: kit 'x1' and 'X1' both case-flip to
158
+ # Prolog 'X1', which Prolog's reader would silently unify into ONE
159
+ # variable — corrupting the formula instead of refusing).
160
+ upper = names.get("variable", node.variable.name,
161
+ node.variable.name[0].upper() + node.variable.name[1:])
162
+ keyword = "all" if node.type in ("∀", "forall") else "ex"
163
+ new_bound = dict(bound)
164
+ new_bound[node.variable.name] = upper
165
+ return f"({keyword} {upper}: {_formula(node.formula, names, new_bound)})"
166
+ raise NotImplementedError(
167
+ f"nanocop: formula node {cls!r} is outside the supported fragment "
168
+ "(Box/Diamond + classical connectives + plain quantifiers only).")
169
+
170
+
171
+ def to_nanocop(formula: Node, premises: Sequence[Node] = ()) -> str:
172
+ """Render ``premises ⊨ formula`` as a nanoCoP-M problem ``f(...).``.
173
+
174
+ Premises fold into ``(∧ premises) => formula`` first (nanoCoP-M takes
175
+ one validity question, not an axioms/conjecture split).
176
+ """
177
+ goal = formula
178
+ premises = list(premises)
179
+ if premises:
180
+ conj = premises[0]
181
+ for p in premises[1:]:
182
+ conj = And(conj, p)
183
+ goal = Implies(conj, formula)
184
+ names = _NameMap()
185
+ return f"f( {_formula(goal, names, {})} ).\n"
186
+
187
+
188
+ _DISCOVERY_CACHE: dict = {}
189
+
190
+
191
+ def _discover() -> Optional[Tuple[str, bool]]:
192
+ """``(command, use_wsl)`` for the nanocopm wrapper, or ``None``.
193
+
194
+ The env override is read fresh on every call (never cached), so setting
195
+ ``$UFK_NANOCOP_CMD`` mid-process repoints immediately; only the
196
+ PATH/WSL binary probes are cached.
197
+ """
198
+ override = os.environ.get("UFK_NANOCOP_CMD")
199
+ if override:
200
+ return ((override[4:], True) if override.startswith("wsl:")
201
+ else (override, False))
202
+ if "cmd" in _DISCOVERY_CACHE:
203
+ return _DISCOVERY_CACHE["cmd"]
204
+ resolved: Optional[Tuple[str, bool]] = None
205
+ if shutil.which("nanocopm.sh"):
206
+ resolved = ("nanocopm.sh", False)
207
+ else:
208
+ try:
209
+ probe = subprocess.run(["wsl.exe", "which", "nanocopm.sh"],
210
+ capture_output=True, text=True, timeout=20)
211
+ path = probe.stdout.strip()
212
+ if probe.returncode == 0 and path:
213
+ resolved = (path, True)
214
+ except (OSError, subprocess.TimeoutExpired):
215
+ resolved = None
216
+ _DISCOVERY_CACHE["cmd"] = resolved
217
+ return resolved
218
+
219
+
220
+ def nanocop_available() -> bool:
221
+ """Pure discovery (env → PATH → WSL); see the module docstring."""
222
+ return _discover() is not None
223
+
224
+
225
+ def _run_nanocop(problem: str, command: str, use_wsl: bool,
226
+ timeout_s: int) -> Tuple[str, int]:
227
+ """Write the problem file, run the wrapper, return ``(output, code)``."""
228
+ from .eprover_backend import _to_wsl_path
229
+
230
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".ncp", delete=False,
231
+ encoding="utf-8") as tmp:
232
+ tmp.write(problem)
233
+ path = tmp.name
234
+ try:
235
+ target = _to_wsl_path(path) if use_wsl else path
236
+ cmd = (["wsl.exe", command, target, str(timeout_s)] if use_wsl
237
+ else [command, target, str(timeout_s)])
238
+ result = subprocess.run(cmd, capture_output=True, text=True,
239
+ timeout=timeout_s + 15)
240
+ return (result.stdout or "") + (result.stderr or ""), result.returncode
241
+ except subprocess.TimeoutExpired:
242
+ return "", 2
243
+ finally:
244
+ try:
245
+ os.unlink(path)
246
+ except OSError:
247
+ pass
248
+
249
+
250
+ class NanocopBackend(ProverBackend):
251
+ """nanoCoP-M, opt-in, cross-checked (see the module docstring)."""
252
+
253
+ name = "nanocop"
254
+ logics = frozenset({"modal"})
255
+ external = True
256
+
257
+ def available(self) -> bool:
258
+ return nanocop_available()
259
+
260
+ def decide(self, formula: Node, premises: Sequence[Node] = (),
261
+ timeout: int = 10000, **options) -> Verdict:
262
+ from .protocol import BackendUnavailable
263
+
264
+ logic = options.pop("logic", None) # d | t | s4 | s5
265
+ domain = options.pop("domain", None) # const | cumul | vary
266
+
267
+ found = _discover()
268
+ if found is None:
269
+ raise BackendUnavailable(
270
+ "nanocop: no nanocopm.sh found — install nanoCoP-M "
271
+ "(http://leancop.de/nanocop-m/, needs ECLiPSe or SWI-Prolog, "
272
+ "configure LOGIC/DOMAIN in nanocopm.sh) and set "
273
+ "$UFK_NANOCOP_CMD to the wrapper (prefix 'wsl:' for WSL).")
274
+ command, use_wsl = found
275
+
276
+ try:
277
+ problem = to_nanocop(formula, premises)
278
+ except (ValueError, NotImplementedError) as exc:
279
+ return Verdict(UNKNOWN, self.name, reason="unsupported",
280
+ detail=str(exc))
281
+
282
+ timeout_s = max(1, timeout // 1000)
283
+ start = time.perf_counter()
284
+ try:
285
+ output, code = _run_nanocop(problem, command, use_wsl, timeout_s)
286
+ except (OSError, RuntimeError) as exc:
287
+ return Verdict(ERROR, self.name, reason="infra",
288
+ detail=f"{type(exc).__name__}: {exc}")
289
+ elapsed = time.perf_counter() - start
290
+
291
+ match = _RESULT_RE.search(output)
292
+ if match is None:
293
+ if code == 2 or "Timeout" in output:
294
+ return Verdict(UNKNOWN, self.name, reason="timeout",
295
+ wall_time=elapsed)
296
+ return Verdict(ERROR, self.name, reason="infra", wall_time=elapsed,
297
+ detail="no recognisable nanoCoP-M result line: "
298
+ + output.strip()[-200:])
299
+ ran_logic, ran_domain, result = match.groups()
300
+
301
+ # The exit code is authoritative alongside the text (the script's
302
+ # documented contract: 0=Theorem, 1=Non-Theorem, 2=timeout). A
303
+ # result line left in the buffer of a run that ultimately exited 2
304
+ # is stale, not an answer (review-reproduced); a code/text mismatch
305
+ # is an infrastructure fault, never silently resolved either way.
306
+ if code == 2:
307
+ return Verdict(UNKNOWN, self.name, reason="timeout",
308
+ wall_time=elapsed,
309
+ detail="exit code 2 (timeout) despite a "
310
+ f"'{result}' line in the buffer — the run "
311
+ "did not complete; discarding the line.")
312
+ expected_code = 0 if result == "Theorem" else 1
313
+ if code != expected_code:
314
+ return Verdict(ERROR, self.name, reason="infra", wall_time=elapsed,
315
+ detail=f"nanocopm.sh printed '{result}' but "
316
+ f"exited {code} (contract: "
317
+ f"{expected_code}) — refusing the answer.")
318
+
319
+ # The script's own report is authoritative about what ran; a caller
320
+ # who requested a specific logic/domain must not get an answer from
321
+ # a differently-configured script.
322
+ if logic is not None and logic != ran_logic:
323
+ return Verdict(ERROR, self.name, reason="infra", wall_time=elapsed,
324
+ detail=f"requested logic={logic!r} but the "
325
+ f"configured script ran {ran_logic!r} — "
326
+ "edit LOGIC in your nanocopm.sh.")
327
+ if domain is not None and domain != ran_domain:
328
+ return Verdict(ERROR, self.name, reason="infra", wall_time=elapsed,
329
+ detail=f"requested domain={domain!r} but the "
330
+ f"configured script ran {ran_domain!r} — "
331
+ "edit DOMAIN in your nanocopm.sh.")
332
+
333
+ frame = {"d": "D", "t": "T", "s4": "S4", "s5": "S5"}.get(ran_logic)
334
+ provenance = f"nanoCoP-M ({ran_logic}/{ran_domain})"
335
+ # The propositional cross-checkers treat Quantifier nodes as opaque
336
+ # (modal-tableau) or unsupported (kripke-enum) — review-confirmed —
337
+ # so quantified problems route through the QML embedding instead.
338
+ quantified = any(type(n).__name__ in ("Quantifier", "SortedQuantifier")
339
+ for f in [formula, *premises] for n in f.walk())
340
+
341
+ if result == "Theorem":
342
+ if quantified:
343
+ # qml is the K-frame embedding: a qml PROOF is valid in
344
+ # every stronger frame (K ⊆ D/T/S4/S5 theorems), so it
345
+ # CONFIRMS; a qml refutation is a K-countermodel that says
346
+ # nothing against a D/T/S4/S5 theorem — never an alarm.
347
+ check = self._cross_check("qml", formula, premises, None,
348
+ timeout)
349
+ agreement = ("confirmed (qml, K-frame proof implies "
350
+ f"{ran_logic})" if check is not None
351
+ and check.status == PROVED
352
+ else "inconclusive (qml)")
353
+ return Verdict(PROVED, self.name, wall_time=elapsed,
354
+ detail=f"{provenance} Theorem; cross-check "
355
+ f"{agreement}")
356
+ check = self._cross_check("modal-tableau", formula, premises,
357
+ frame, timeout)
358
+ if check is not None and check.status == REFUTED:
359
+ return Verdict(
360
+ ERROR, self.name, reason="infra", wall_time=elapsed,
361
+ detail=f"SOUNDNESS ALARM: {provenance} says Theorem but "
362
+ f"modal-tableau (frame {frame}) refutes it — "
363
+ "refusing to answer.")
364
+ agreement = ("confirmed" if check is not None
365
+ and check.status == PROVED else "inconclusive")
366
+ return Verdict(PROVED, self.name, wall_time=elapsed,
367
+ detail=f"{provenance} Theorem; cross-check "
368
+ f"modal-tableau: {agreement}")
369
+
370
+ # Non-Theorem: only an independent countermodel certifies REFUTED.
371
+ if quantified:
372
+ # kripke-enum has no quantified route (unsupported by its own
373
+ # contract): the FO fragment has NO independent countermodel
374
+ # source, so a quantified Non-Theorem is never vouched for.
375
+ return Verdict(UNKNOWN, self.name, reason="incomplete",
376
+ wall_time=elapsed,
377
+ detail=f"{provenance} claims Non-Theorem; the "
378
+ "kit has no independent FIRST-ORDER modal "
379
+ "countermodel route to certify it — "
380
+ "recorded, not vouched for.")
381
+ check = self._cross_check("kripke-enum", formula, premises, frame,
382
+ timeout)
383
+ if check is not None and check.status == REFUTED:
384
+ return Verdict(REFUTED, self.name, wall_time=elapsed,
385
+ countermodel=check.countermodel,
386
+ detail=f"{provenance} Non-Theorem; kripke-enum "
387
+ "countermodel confirms")
388
+ return Verdict(UNKNOWN, self.name, reason="incomplete",
389
+ wall_time=elapsed,
390
+ detail=f"{provenance} claims Non-Theorem, but no "
391
+ "independent countermodel was found — the "
392
+ "claim is recorded, not vouched for.")
393
+
394
+ @staticmethod
395
+ def _cross_check(backend_name: str, formula: Node,
396
+ premises: Sequence[Node], frame: Optional[str],
397
+ timeout: int):
398
+ """Run the named kit backend on the same problem; None on any error.
399
+
400
+ The cross-check is best-effort infrastructure: ITS failure must not
401
+ mask nanoCoP-M's answer, only its definitive disagreement may.
402
+ """
403
+ from .protocol import get_backend
404
+
405
+ try:
406
+ kwargs = {"frame": frame} if frame else {}
407
+ return get_backend(backend_name).decide(
408
+ formula, list(premises), timeout=timeout, **kwargs)
409
+ except Exception: # noqa: BLE001
410
+ return None