unicode-logic-kit 0.31.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- unicode_logic_kit/__init__.py +385 -0
- unicode_logic_kit/__main__.py +520 -0
- unicode_logic_kit/_deadline.py +219 -0
- unicode_logic_kit/ace/__init__.py +126 -0
- unicode_logic_kit/ace/_align.py +135 -0
- unicode_logic_kit/ace/chem_lexicon.py +128 -0
- unicode_logic_kit/ace/drs_reader.py +570 -0
- unicode_logic_kit/ace/mapping.py +666 -0
- unicode_logic_kit/ace/reverse_modal.py +138 -0
- unicode_logic_kit/ace/runner.py +551 -0
- unicode_logic_kit/ace/translate.py +452 -0
- unicode_logic_kit/ace/verbalize.py +1070 -0
- unicode_logic_kit/api.py +1284 -0
- unicode_logic_kit/atp/__init__.py +177 -0
- unicode_logic_kit/atp/_ascii_names.py +113 -0
- unicode_logic_kit/atp/_html.py +72 -0
- unicode_logic_kit/atp/_substructural_input.py +228 -0
- unicode_logic_kit/atp/_tff_problem.py +715 -0
- unicode_logic_kit/atp/_tptp_problem.py +1111 -0
- unicode_logic_kit/atp/_writer_support.py +289 -0
- unicode_logic_kit/atp/clingo_backend.py +1180 -0
- unicode_logic_kit/atp/cvc5_backend.py +1385 -0
- unicode_logic_kit/atp/eprover_backend.py +732 -0
- unicode_logic_kit/atp/finite_domain.py +1055 -0
- unicode_logic_kit/atp/fitch.py +1547 -0
- unicode_logic_kit/atp/fitch_search.py +551 -0
- unicode_logic_kit/atp/hets_backend.py +339 -0
- unicode_logic_kit/atp/hybrid_down.py +120 -0
- unicode_logic_kit/atp/incremental.py +250 -0
- unicode_logic_kit/atp/kripke_enum.py +741 -0
- unicode_logic_kit/atp/lambek.py +436 -0
- unicode_logic_kit/atp/leo3_backend.py +332 -0
- unicode_logic_kit/atp/linear.py +738 -0
- unicode_logic_kit/atp/lj.py +705 -0
- unicode_logic_kit/atp/logic_backends.py +566 -0
- unicode_logic_kit/atp/ltl_tableau.py +1084 -0
- unicode_logic_kit/atp/minizinc_backend.py +1402 -0
- unicode_logic_kit/atp/modal_tableau.py +1382 -0
- unicode_logic_kit/atp/nanocop_backend.py +410 -0
- unicode_logic_kit/atp/portfolio.py +489 -0
- unicode_logic_kit/atp/protocol.py +1803 -0
- unicode_logic_kit/atp/prover9_entailment.py +1153 -0
- unicode_logic_kit/atp/resolution.py +1376 -0
- unicode_logic_kit/atp/resolution_check.py +1114 -0
- unicode_logic_kit/atp/sequent.py +1050 -0
- unicode_logic_kit/atp/tableau.py +921 -0
- unicode_logic_kit/atp/tableau_check.py +543 -0
- unicode_logic_kit/atp/tptp_ncl.py +811 -0
- unicode_logic_kit/atp/tptp_tff.py +1546 -0
- unicode_logic_kit/atp/tstp.py +1333 -0
- unicode_logic_kit/atp/tstp_check.py +1096 -0
- unicode_logic_kit/atp/twee_backend.py +236 -0
- unicode_logic_kit/atp/twee_check.py +711 -0
- unicode_logic_kit/atp/twee_entailment.py +953 -0
- unicode_logic_kit/atp/vampire_entailment.py +540 -0
- unicode_logic_kit/atp/z3_arith.py +470 -0
- unicode_logic_kit/atp/z3_equivalence.py +36 -0
- unicode_logic_kit/atp/z3_fuzzy.py +362 -0
- unicode_logic_kit/atp/z3_input.py +500 -0
- unicode_logic_kit/atp/z3_models.py +208 -0
- unicode_logic_kit/chem/__init__.py +88 -0
- unicode_logic_kit/chem/_naming.py +284 -0
- unicode_logic_kit/chem/cache.py +185 -0
- unicode_logic_kit/chem/interop.py +244 -0
- unicode_logic_kit/chem/mol.py +525 -0
- unicode_logic_kit/chem/signature.py +112 -0
- unicode_logic_kit/comorphism.py +497 -0
- unicode_logic_kit/dl/__init__.py +384 -0
- unicode_logic_kit/dl/classification.py +227 -0
- unicode_logic_kit/dl/concepts.py +632 -0
- unicode_logic_kit/dl/datatypes.py +818 -0
- unicode_logic_kit/dl/owl_functional.py +2433 -0
- unicode_logic_kit/dl/owl_manchester.py +1637 -0
- unicode_logic_kit/dl/owl_reasoner.py +790 -0
- unicode_logic_kit/dl/parser.py +391 -0
- unicode_logic_kit/dl/tableau.py +4048 -0
- unicode_logic_kit/dl/translate.py +2704 -0
- unicode_logic_kit/drt/__init__.py +94 -0
- unicode_logic_kit/drt/export.py +179 -0
- unicode_logic_kit/drt/nodes.py +506 -0
- unicode_logic_kit/drt/parser.py +965 -0
- unicode_logic_kit/drt/resolve.py +195 -0
- unicode_logic_kit/drt/reverse.py +175 -0
- unicode_logic_kit/eval/__init__.py +106 -0
- unicode_logic_kit/eval/batch.py +382 -0
- unicode_logic_kit/eval/canonical.py +663 -0
- unicode_logic_kit/eval/chem_batch.py +606 -0
- unicode_logic_kit/eval/converses.py +200 -0
- unicode_logic_kit/eval/datasets/__init__.py +136 -0
- unicode_logic_kit/eval/datasets/_base.py +263 -0
- unicode_logic_kit/eval/datasets/_proofwriter_proof.py +422 -0
- unicode_logic_kit/eval/datasets/c3po.py +678 -0
- unicode_logic_kit/eval/datasets/folio.py +158 -0
- unicode_logic_kit/eval/datasets/fracas.py +418 -0
- unicode_logic_kit/eval/datasets/groves.py +191 -0
- unicode_logic_kit/eval/datasets/logicbench.py +467 -0
- unicode_logic_kit/eval/datasets/logicnli.py +303 -0
- unicode_logic_kit/eval/datasets/malls.py +133 -0
- unicode_logic_kit/eval/datasets/pfolio.py +594 -0
- unicode_logic_kit/eval/datasets/pmb.py +242 -0
- unicode_logic_kit/eval/datasets/prontoqa.py +611 -0
- unicode_logic_kit/eval/datasets/proofwriter.py +1431 -0
- unicode_logic_kit/eval/datasets/proverqa.py +674 -0
- unicode_logic_kit/eval/datasets/willow.py +478 -0
- unicode_logic_kit/eval/equivalence.py +466 -0
- unicode_logic_kit/eval/exercise_gen.py +533 -0
- unicode_logic_kit/eval/explain.py +791 -0
- unicode_logic_kit/eval/generality.py +750 -0
- unicode_logic_kit/eval/metric_hf.py +458 -0
- unicode_logic_kit/eval/predicate_match.py +343 -0
- unicode_logic_kit/eval/theory_check.py +1170 -0
- unicode_logic_kit/eval/validate.py +306 -0
- unicode_logic_kit/fol/__init__.py +177 -0
- unicode_logic_kit/fol/_atom_keys.py +510 -0
- unicode_logic_kit/fol/_fol_nodes.py +3586 -0
- unicode_logic_kit/fol/_free_parameters.py +105 -0
- unicode_logic_kit/fol/_ho_nodes.py +448 -0
- unicode_logic_kit/fol/_hybrid_nodes.py +308 -0
- unicode_logic_kit/fol/_identifiers.py +1091 -0
- unicode_logic_kit/fol/_lambek_nodes.py +112 -0
- unicode_logic_kit/fol/_linear_nodes.py +352 -0
- unicode_logic_kit/fol/_modal_nodes.py +1467 -0
- unicode_logic_kit/fol/_msfl_nodes.py +2196 -0
- unicode_logic_kit/fol/_numeral_symbols.py +231 -0
- unicode_logic_kit/fol/_so_nodes.py +200 -0
- unicode_logic_kit/fol/_symbol_names.py +81 -0
- unicode_logic_kit/fol/_team_nodes.py +181 -0
- unicode_logic_kit/fol/_tptp_symbols.py +551 -0
- unicode_logic_kit/fol/_truth_constants.py +117 -0
- unicode_logic_kit/fol/casl_export.py +1135 -0
- unicode_logic_kit/fol/casl_import.py +929 -0
- unicode_logic_kit/fol/derivation.py +367 -0
- unicode_logic_kit/fol/dialect_detect.py +70 -0
- unicode_logic_kit/fol/dialect_repair.py +537 -0
- unicode_logic_kit/fol/frames.py +637 -0
- unicode_logic_kit/fol/grammars/terminals.lark +31 -0
- unicode_logic_kit/fol/lambda_tools.py +297 -0
- unicode_logic_kit/fol/latex_input.py +429 -0
- unicode_logic_kit/fol/modal_translation.py +944 -0
- unicode_logic_kit/fol/msflparser.py +1033 -0
- unicode_logic_kit/fol/naming.py +422 -0
- unicode_logic_kit/fol/nodes.py +241 -0
- unicode_logic_kit/fol/normalforms.py +492 -0
- unicode_logic_kit/fol/pal.py +287 -0
- unicode_logic_kit/fol/prolog_export.py +566 -0
- unicode_logic_kit/fol/prolog_input.py +505 -0
- unicode_logic_kit/fol/prover9_input.py +1325 -0
- unicode_logic_kit/fol/qml.py +1760 -0
- unicode_logic_kit/fol/qmltp_input.py +525 -0
- unicode_logic_kit/fol/sanitize.py +221 -0
- unicode_logic_kit/fol/serialize.py +79 -0
- unicode_logic_kit/fol/signature.py +1290 -0
- unicode_logic_kit/fol/simplify_check.py +544 -0
- unicode_logic_kit/fol/spans.py +594 -0
- unicode_logic_kit/fol/tptp_input.py +1503 -0
- unicode_logic_kit/fol/tptp_repair.py +941 -0
- unicode_logic_kit/fol/unification.py +157 -0
- unicode_logic_kit/fol/verbalize.py +263 -0
- unicode_logic_kit/hets/__init__.py +163 -0
- unicode_logic_kit/hets/bridge.py +142 -0
- unicode_logic_kit/hets/client.py +748 -0
- unicode_logic_kit/hets/docker.py +420 -0
- unicode_logic_kit/hets/dol.py +712 -0
- unicode_logic_kit/hets/haskell_json.py +355 -0
- unicode_logic_kit/hets/owl_backend.py +794 -0
- unicode_logic_kit/hets/owl_cli.py +598 -0
- unicode_logic_kit/hets/symbols.py +512 -0
- unicode_logic_kit/hol/__init__.py +140 -0
- unicode_logic_kit/hol/_ho_common.py +323 -0
- unicode_logic_kit/hol/_isabelle_binders.py +125 -0
- unicode_logic_kit/hol/classical.py +812 -0
- unicode_logic_kit/hol/deepshallow/__init__.py +45 -0
- unicode_logic_kit/hol/deepshallow/_common.py +177 -0
- unicode_logic_kit/hol/deepshallow/conditional.py +225 -0
- unicode_logic_kit/hol/deepshallow/intuitionistic.py +181 -0
- unicode_logic_kit/hol/deepshallow/modal.py +217 -0
- unicode_logic_kit/hol/deepshallow/qml.py +406 -0
- unicode_logic_kit/hol/deepshallow/relevant.py +206 -0
- unicode_logic_kit/hol/free.py +753 -0
- unicode_logic_kit/hol/goedel.py +336 -0
- unicode_logic_kit/hol/ho_modal.py +1743 -0
- unicode_logic_kit/hol/intuitionistic.py +403 -0
- unicode_logic_kit/hol/isabelle_conditional.py +593 -0
- unicode_logic_kit/hol/isabelle_modal.py +1908 -0
- unicode_logic_kit/hol/isabelle_relevant.py +412 -0
- unicode_logic_kit/hol/isabelle_runner.py +1147 -0
- unicode_logic_kit/hol/isabelle_substructural.py +884 -0
- unicode_logic_kit/hol/lean.py +1018 -0
- unicode_logic_kit/hol/manyvalued.py +921 -0
- unicode_logic_kit/hol/secondorder.py +687 -0
- unicode_logic_kit/hol/thf_modal.py +941 -0
- unicode_logic_kit/hol/thirdorder.py +397 -0
- unicode_logic_kit/ilp/__init__.py +89 -0
- unicode_logic_kit/ilp/readback.py +389 -0
- unicode_logic_kit/ilp/separation.py +153 -0
- unicode_logic_kit/ilp/task.py +730 -0
- unicode_logic_kit/logic.py +163 -0
- unicode_logic_kit/mcp/__init__.py +28 -0
- unicode_logic_kit/mcp/__main__.py +5 -0
- unicode_logic_kit/mcp/chem_tools.py +1031 -0
- unicode_logic_kit/mcp/server.py +2453 -0
- unicode_logic_kit/mcp/syntax_spec.py +681 -0
- unicode_logic_kit/prob/__init__.py +53 -0
- unicode_logic_kit/prob/_bdd.py +225 -0
- unicode_logic_kit/prob/_column_gen.py +668 -0
- unicode_logic_kit/prob/distribution.py +686 -0
- unicode_logic_kit/prob/nilsson.py +470 -0
- unicode_logic_kit/py.typed +0 -0
- unicode_logic_kit/semantics/__init__.py +137 -0
- unicode_logic_kit/semantics/_modal_reject.py +156 -0
- unicode_logic_kit/semantics/action_models.py +466 -0
- unicode_logic_kit/semantics/asp_models.py +1200 -0
- unicode_logic_kit/semantics/conditional.py +580 -0
- unicode_logic_kit/semantics/dynamic_epistemic.py +95 -0
- unicode_logic_kit/semantics/free_logic.py +913 -0
- unicode_logic_kit/semantics/fuzzy.py +384 -0
- unicode_logic_kit/semantics/fuzzy_kripke.py +442 -0
- unicode_logic_kit/semantics/intuitionistic.py +581 -0
- unicode_logic_kit/semantics/kripke.py +1139 -0
- unicode_logic_kit/semantics/manyvalued.py +580 -0
- unicode_logic_kit/semantics/matrix.py +342 -0
- unicode_logic_kit/semantics/model_eval.py +1135 -0
- unicode_logic_kit/semantics/modelfinder.py +1036 -0
- unicode_logic_kit/semantics/nonmonotonic.py +372 -0
- unicode_logic_kit/semantics/relevant.py +331 -0
- unicode_logic_kit/semantics/secondorder.py +657 -0
- unicode_logic_kit/semantics/structures.py +352 -0
- unicode_logic_kit/semantics/tarski.py +975 -0
- unicode_logic_kit/semantics/team.py +315 -0
- unicode_logic_kit/semantics/team_translation.py +416 -0
- unicode_logic_kit/semantics/thirdorder.py +358 -0
- unicode_logic_kit/semantics/tnorm.py +85 -0
- unicode_logic_kit/semantics/truthtable.py +201 -0
- unicode_logic_kit-0.31.0.dist-info/METADATA +333 -0
- unicode_logic_kit-0.31.0.dist-info/RECORD +237 -0
- unicode_logic_kit-0.31.0.dist-info/WHEEL +4 -0
- unicode_logic_kit-0.31.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,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
|