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
unicode_logic_kit/api.py
ADDED
|
@@ -0,0 +1,1284 @@
|
|
|
1
|
+
"""The core facade: seven verbs for NL→logic pipelines.
|
|
2
|
+
|
|
3
|
+
``unicode_logic_kit.api`` bundles the whole toolkit behind a small, stable
|
|
4
|
+
vocabulary designed so that an LLM verification loop can consume every answer
|
|
5
|
+
without extra parsing:
|
|
6
|
+
|
|
7
|
+
from unicode_logic_kit import api
|
|
8
|
+
|
|
9
|
+
api.parse_any(text) # dialect detection + tolerant parsing
|
|
10
|
+
api.check(formula) # well-formedness + optional signature check
|
|
11
|
+
api.equivalent(f, g) # graded equivalence (structural → solver)
|
|
12
|
+
api.prove(f) # backend chain → one Verdict with provenance
|
|
13
|
+
api.countermodel(f) # refutation witness + plain-English gloss
|
|
14
|
+
api.repair(text, fixer=llm) # diagnose → suggest → (caller) fix → re-check
|
|
15
|
+
api.translate(t, "alc", "fol") # comorphism registry, composed via BFS
|
|
16
|
+
|
|
17
|
+
Every result object has ``to_dict()`` returning JSON-compatible data.
|
|
18
|
+
|
|
19
|
+
STABILITY POLICY (this module, :class:`~unicode_logic_kit.atp.protocol.Verdict`,
|
|
20
|
+
and the result dataclasses here): within a minor release line (0.N.x) nothing
|
|
21
|
+
is renamed or removed and dict keys only ever gain siblings; breaking changes
|
|
22
|
+
happen only at a minor bump, are listed in the CHANGELOG, and keep a
|
|
23
|
+
deprecation shim for one minor cycle where feasible. Downstream consumers
|
|
24
|
+
(e.g. FitchAsATP) should pin ``unicode-logic-kit>=0.N,<0.N+1`` per line.
|
|
25
|
+
|
|
26
|
+
Deliberately NOT re-exported at the package top level: ``prove`` would collide
|
|
27
|
+
with the resolution prover's ``prove`` that existing consumers already import
|
|
28
|
+
from ``unicode_logic_kit`` — the facade lives in this namespace, use
|
|
29
|
+
``api.prove``.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
import functools
|
|
33
|
+
import re
|
|
34
|
+
import sys
|
|
35
|
+
import threading
|
|
36
|
+
from dataclasses import dataclass
|
|
37
|
+
from typing import Callable, Iterator, Optional, Sequence, Tuple, TypeVar, Union
|
|
38
|
+
|
|
39
|
+
from .fol.nodes import Node, And
|
|
40
|
+
from .atp.protocol import (
|
|
41
|
+
Verdict, BackendUnavailable, PROVED, REFUTED, UNKNOWN, ERROR,
|
|
42
|
+
get_backend, default_chain, run_backend, _no_definitive_verdict,
|
|
43
|
+
plan_options,
|
|
44
|
+
)
|
|
45
|
+
from .eval.equivalence import EquivalenceResult
|
|
46
|
+
from .eval.equivalence import equivalent as _equivalent
|
|
47
|
+
from .comorphism import DEFAULT_REGISTRY, TranslationResult
|
|
48
|
+
|
|
49
|
+
__all__ = [
|
|
50
|
+
"ParseResult", "CheckResult", "RepairStep",
|
|
51
|
+
"parse_any", "check", "equivalent", "prove", "countermodel",
|
|
52
|
+
"repair", "translate",
|
|
53
|
+
"EquivalenceResult", "Verdict", "TranslationResult", "CountermodelResult",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
# parse_any — dialect detection + tolerant parsing
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
# The unicode-surface parser modes tried, in order, when the text is (or falls
|
|
62
|
+
# back to) the kit's own syntax. Broadest-first would mask mode-specific
|
|
63
|
+
# meanings (⊕ is Xor in fol but StrongDisjunction in fl), so classical first.
|
|
64
|
+
_UNICODE_MODES: Tuple[Tuple[str, dict], ...] = (
|
|
65
|
+
("fol", {}),
|
|
66
|
+
("modal", {"modal": True}),
|
|
67
|
+
("second_order", {"second_order": True}),
|
|
68
|
+
# Second-order syntax plus predicates in argument position, and NOTHING
|
|
69
|
+
# else: it is served by the same LALR table as the mode above it, so the
|
|
70
|
+
# only inputs it newly accepts are the ones with a predicate really standing
|
|
71
|
+
# in an argument slot -- which no mode above can express.
|
|
72
|
+
#
|
|
73
|
+
# Its MODAL sibling is deliberately NOT on the ladder. That one falls back
|
|
74
|
+
# to Earley (inherited from `modal`, which needs it), and Earley reaches
|
|
75
|
+
# readings LALR does not: with a second-order binder available, "∀ P(x)" parses
|
|
76
|
+
# as a quantifier over the propositional atom `x` instead of failing. Every
|
|
77
|
+
# other dialect reports that string as the malformed quantifier it is, and
|
|
78
|
+
# the repair / error-routing machinery depends on their agreeing. Reach the
|
|
79
|
+
# mode explicitly with MSFLParser(third_order=True, modal=True).
|
|
80
|
+
("third_order", {"third_order": True}),
|
|
81
|
+
("dependence", {"dependence": True}),
|
|
82
|
+
("msfol", {"many_sorted": True}),
|
|
83
|
+
("msfl", {"many_sorted": True, "fuzzy": True}),
|
|
84
|
+
("fl", {"fuzzy": True}),
|
|
85
|
+
("linear", {"linear": True}),
|
|
86
|
+
("lambek", {"lambek": True}),
|
|
87
|
+
)
|
|
88
|
+
_UNICODE_HINTS = {name: kwargs for name, kwargs in _UNICODE_MODES}
|
|
89
|
+
|
|
90
|
+
# The detection signals and their order live in fol.dialect_detect — one
|
|
91
|
+
# importable source of truth; parse_any consumes its candidate list.
|
|
92
|
+
from .fol.dialect_detect import detect_dialects
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass(frozen=True)
|
|
96
|
+
class ParseResult:
|
|
97
|
+
"""Outcome of :func:`parse_any`.
|
|
98
|
+
|
|
99
|
+
``dialect`` names what succeeded (a unicode mode name, or ``"tptp"`` /
|
|
100
|
+
``"tptp_bare"`` / ``"latex"`` / ``"prover9"`` / ``"smtlib"``); ``errors``
|
|
101
|
+
records every failed attempt as ``{"dialect": ..., "message": ...}`` so a
|
|
102
|
+
repair loop can see exactly what each parser objected to.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
ok: bool
|
|
106
|
+
formula: Optional[Node] = None
|
|
107
|
+
dialect: Optional[str] = None
|
|
108
|
+
errors: Tuple[dict, ...] = ()
|
|
109
|
+
|
|
110
|
+
def __bool__(self) -> bool:
|
|
111
|
+
return self.ok
|
|
112
|
+
|
|
113
|
+
def to_dict(self) -> dict:
|
|
114
|
+
return {
|
|
115
|
+
"ok": self.ok,
|
|
116
|
+
"dialect": self.dialect,
|
|
117
|
+
"formula": self.formula.to_dict() if self.formula is not None else None,
|
|
118
|
+
"errors": list(self.errors),
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _try(fn, dialect: str, errors: list) -> Optional[ParseResult]:
|
|
123
|
+
"""Run one parse attempt; success → ParseResult, failure → recorded."""
|
|
124
|
+
try:
|
|
125
|
+
node = fn()
|
|
126
|
+
except Exception as exc: # each parser has its own error types
|
|
127
|
+
errors.append({"dialect": dialect, "message": str(exc)})
|
|
128
|
+
return None
|
|
129
|
+
return ParseResult(ok=True, formula=node, dialect=dialect,
|
|
130
|
+
errors=tuple(errors))
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _parse_smtlib(text: str) -> Node:
|
|
134
|
+
from .atp.z3_input import parse_smtlib
|
|
135
|
+
asserts = parse_smtlib(text)
|
|
136
|
+
if not asserts:
|
|
137
|
+
raise ValueError("smtlib: no assertions found")
|
|
138
|
+
conj = asserts[0]
|
|
139
|
+
for a in asserts[1:]: # multiple asserts = their conjunction
|
|
140
|
+
conj = And(conj, a)
|
|
141
|
+
return conj
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _parse_tptp_annotated(text: str) -> Node:
|
|
145
|
+
from .fol.tptp_input import parse_tptp
|
|
146
|
+
records = parse_tptp(text)
|
|
147
|
+
if len(records) != 1:
|
|
148
|
+
raise ValueError(
|
|
149
|
+
f"tptp: {len(records)} annotated formulas — parse_any handles a single "
|
|
150
|
+
"formula; use parse_tptp / load_tptp_problem for whole problems")
|
|
151
|
+
return records[0].formula
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _unicode_ladder(text: str, errors: list) -> ParseResult:
|
|
155
|
+
"""Try every unicode-surface mode in order; first success wins."""
|
|
156
|
+
from .fol.msflparser import MSFLParser
|
|
157
|
+
|
|
158
|
+
for name, kwargs in _UNICODE_MODES:
|
|
159
|
+
result = _try(lambda k=kwargs: MSFLParser(**k).parse(text), name, errors)
|
|
160
|
+
if result is not None:
|
|
161
|
+
return result
|
|
162
|
+
return ParseResult(ok=False, errors=tuple(errors))
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def parse_any(text: str, *, hint: Optional[str] = None) -> ParseResult:
|
|
166
|
+
"""Parse ``text`` in whatever dialect it appears to be written.
|
|
167
|
+
|
|
168
|
+
Detection order: SMT-LIB (``(assert`` …) → annotated TPTP (``fof(...)``) →
|
|
169
|
+
LaTeX (``\\forall`` …) → the kit's own unicode surface syntax (mode ladder,
|
|
170
|
+
classical first) → for pure-ASCII leftovers, bare TPTP (``![X]: ...``) and
|
|
171
|
+
Prover9. Nothing raises: failure returns ``ok=False`` with every attempt's
|
|
172
|
+
error message, ready for a repair loop.
|
|
173
|
+
|
|
174
|
+
``hint`` pins the dialect instead of detecting: a unicode mode name
|
|
175
|
+
(``"fol"``, ``"modal"``, …), ``"unicode"`` (the whole mode ladder), or
|
|
176
|
+
``"tptp"`` / ``"tptp_bare"`` / ``"latex"`` / ``"prover9"`` / ``"smtlib"``.
|
|
177
|
+
Multiple SMT-LIB assertions fold into their conjunction; a multi-formula
|
|
178
|
+
TPTP problem is refused (use ``load_tptp_problem``).
|
|
179
|
+
"""
|
|
180
|
+
errors: list = []
|
|
181
|
+
|
|
182
|
+
if hint is not None:
|
|
183
|
+
if hint in _UNICODE_HINTS:
|
|
184
|
+
from .fol.msflparser import MSFLParser
|
|
185
|
+
kwargs = _UNICODE_HINTS[hint]
|
|
186
|
+
result = _try(lambda: MSFLParser(**kwargs).parse(text), hint, errors)
|
|
187
|
+
return result or ParseResult(ok=False, errors=tuple(errors))
|
|
188
|
+
if hint == "unicode":
|
|
189
|
+
return _unicode_ladder(text, errors)
|
|
190
|
+
if hint == "smtlib":
|
|
191
|
+
result = _try(lambda: _parse_smtlib(text), "smtlib", errors)
|
|
192
|
+
return result or ParseResult(ok=False, errors=tuple(errors))
|
|
193
|
+
if hint == "tptp":
|
|
194
|
+
result = _try(lambda: _parse_tptp_annotated(text), "tptp", errors)
|
|
195
|
+
return result or ParseResult(ok=False, errors=tuple(errors))
|
|
196
|
+
if hint == "tptp_bare":
|
|
197
|
+
from .fol.tptp_input import parse_tptp_formula
|
|
198
|
+
result = _try(lambda: parse_tptp_formula(text), "tptp_bare", errors)
|
|
199
|
+
return result or ParseResult(ok=False, errors=tuple(errors))
|
|
200
|
+
if hint == "latex":
|
|
201
|
+
from .fol.latex_input import parse_latex
|
|
202
|
+
result = _try(lambda: parse_latex(text), "latex", errors)
|
|
203
|
+
return result or ParseResult(ok=False, errors=tuple(errors))
|
|
204
|
+
if hint == "prover9":
|
|
205
|
+
from .fol.prover9_input import parse_prover9
|
|
206
|
+
result = _try(lambda: parse_prover9(text), "prover9", errors)
|
|
207
|
+
return result or ParseResult(ok=False, errors=tuple(errors))
|
|
208
|
+
raise ValueError(
|
|
209
|
+
f"parse_any: unknown hint {hint!r} (unicode modes "
|
|
210
|
+
f"{sorted(_UNICODE_HINTS)}, or 'unicode'/'tptp'/'tptp_bare'/"
|
|
211
|
+
f"'latex'/'prover9'/'smtlib')")
|
|
212
|
+
|
|
213
|
+
# detect_dialects nominates candidates in order (always ending in
|
|
214
|
+
# "unicode", the mode-ladder catch-all); each one is tried and a parse
|
|
215
|
+
# failure falls through to the next, accumulating its error.
|
|
216
|
+
for dialect in detect_dialects(text):
|
|
217
|
+
if dialect == "unicode":
|
|
218
|
+
return _unicode_ladder(text, errors)
|
|
219
|
+
if dialect == "smtlib":
|
|
220
|
+
result = _try(lambda: _parse_smtlib(text), "smtlib", errors)
|
|
221
|
+
elif dialect == "tptp":
|
|
222
|
+
result = _try(lambda: _parse_tptp_annotated(text), "tptp", errors)
|
|
223
|
+
elif dialect == "latex":
|
|
224
|
+
from .fol.latex_input import parse_latex
|
|
225
|
+
result = _try(lambda: parse_latex(text), "latex", errors)
|
|
226
|
+
elif dialect == "tptp_bare":
|
|
227
|
+
from .fol.tptp_input import parse_tptp_formula
|
|
228
|
+
result = _try(lambda: parse_tptp_formula(text), "tptp_bare", errors)
|
|
229
|
+
else: # "prover9"
|
|
230
|
+
from .fol.prover9_input import parse_prover9
|
|
231
|
+
result = _try(lambda: parse_prover9(text), "prover9", errors)
|
|
232
|
+
if result is not None:
|
|
233
|
+
return result
|
|
234
|
+
raise AssertionError("detect_dialects always ends in 'unicode'")
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
# ---------------------------------------------------------------------------
|
|
238
|
+
# Formulas nested deeper than the interpreter's recursion limit
|
|
239
|
+
#
|
|
240
|
+
# Most readers of a formula (a prover's translation, a validity check, a
|
|
241
|
+
# canonical form) recurse over it, and CPython's recursion limit (1000 by default)
|
|
242
|
+
# stops them at a nesting of a few hundred levels. A verb of this module does not
|
|
243
|
+
# let that surface as an exception: a formula nested at least ``_SHALLOW_DEPTH``
|
|
244
|
+
# levels deep is read on a worker thread whose stack and recursion limit are sized
|
|
245
|
+
# for it, and a reader that still runs out says so by the nesting depth.
|
|
246
|
+
# ---------------------------------------------------------------------------
|
|
247
|
+
|
|
248
|
+
_T = TypeVar("_T")
|
|
249
|
+
|
|
250
|
+
#: A formula nested fewer levels than this is handled on the calling thread under the
|
|
251
|
+
#: interpreter's own recursion limit, as every formula was before.
|
|
252
|
+
_SHALLOW_DEPTH = 100
|
|
253
|
+
|
|
254
|
+
#: What a deep run asks of the interpreter: the stack of its worker thread, the bytes
|
|
255
|
+
#: one Python frame may take of it (generous: a frame takes a few hundred), the frames
|
|
256
|
+
#: that one level of nesting costs a recursive reader (the backends that recurse most
|
|
257
|
+
#: take three on a chain of negations; eight leaves room for the other connectives) and
|
|
258
|
+
#: the frames the callers above the reader need.
|
|
259
|
+
_DEEP_STACK_BYTES = 128 * 1024 * 1024
|
|
260
|
+
_FRAME_BYTES = 2048
|
|
261
|
+
_FRAMES_PER_LEVEL = 8
|
|
262
|
+
_FRAME_HEADROOM = 1000
|
|
263
|
+
|
|
264
|
+
#: The deepest nesting a deep run takes on: the recursion limit it needs still fits the
|
|
265
|
+
#: stack, so a runaway recursion ends in a ``RecursionError``, never in a crash.
|
|
266
|
+
_DEEP_MAX_LEVELS = (_DEEP_STACK_BYTES // _FRAME_BYTES - _FRAME_HEADROOM) // _FRAMES_PER_LEVEL
|
|
267
|
+
|
|
268
|
+
#: The recursion limit and the stack size of new threads are process-wide settings; one
|
|
269
|
+
#: deep run at a time changes them.
|
|
270
|
+
_deep_run_lock = threading.Lock()
|
|
271
|
+
|
|
272
|
+
#: Set on the worker of a deep run: a call made from there is already where a deep formula
|
|
273
|
+
#: can be read, and waiting for the lock its own caller holds would wait for itself.
|
|
274
|
+
_on_deep_worker = threading.local()
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _nesting_depth(*formulas) -> int:
|
|
278
|
+
"""The most nodes on one path from a root down, over ``formulas`` (computed iteratively)."""
|
|
279
|
+
from .atp.tableau import nesting_depth
|
|
280
|
+
|
|
281
|
+
return nesting_depth(*(f for f in formulas if isinstance(f, Node)))
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _call_deep(depth: int, function: Callable[[], _T]) -> _T:
|
|
285
|
+
"""The result of ``function()``, run where a formula nested ``depth`` levels deep can be read.
|
|
286
|
+
|
|
287
|
+
A ``depth`` below ``_SHALLOW_DEPTH``, above what the worker's stack carries
|
|
288
|
+
(``_DEEP_MAX_LEVELS``), or within a recursion limit the caller has already raised
|
|
289
|
+
is called on the calling thread, unchanged. Otherwise the call runs on a worker
|
|
290
|
+
thread with a stack of ``_DEEP_STACK_BYTES`` and a recursion limit of
|
|
291
|
+
``_FRAMES_PER_LEVEL`` frames per level, both restored when it returns; what the
|
|
292
|
+
function raises is raised on the calling thread. A worker that cannot be started
|
|
293
|
+
leaves the call on the calling thread. Calls of this kind do not overlap: the
|
|
294
|
+
recursion limit belongs to the whole process.
|
|
295
|
+
"""
|
|
296
|
+
if not _SHALLOW_DEPTH <= depth <= _DEEP_MAX_LEVELS or getattr(_on_deep_worker, "on", False):
|
|
297
|
+
return function()
|
|
298
|
+
needed = depth * _FRAMES_PER_LEVEL + _FRAME_HEADROOM
|
|
299
|
+
if sys.getrecursionlimit() >= needed:
|
|
300
|
+
return function()
|
|
301
|
+
|
|
302
|
+
outcome: dict = {}
|
|
303
|
+
|
|
304
|
+
def work() -> None:
|
|
305
|
+
_on_deep_worker.on = True
|
|
306
|
+
try:
|
|
307
|
+
outcome["value"] = function()
|
|
308
|
+
except BaseException as exc: # re-raised on the calling thread
|
|
309
|
+
outcome["error"] = exc
|
|
310
|
+
|
|
311
|
+
with _deep_run_lock:
|
|
312
|
+
limit = sys.getrecursionlimit()
|
|
313
|
+
stack = threading.stack_size()
|
|
314
|
+
started = False
|
|
315
|
+
try:
|
|
316
|
+
sys.setrecursionlimit(max(limit, needed))
|
|
317
|
+
threading.stack_size(_DEEP_STACK_BYTES)
|
|
318
|
+
worker = threading.Thread(target=work, name="unicode-logic-kit-deep", daemon=True)
|
|
319
|
+
try:
|
|
320
|
+
worker.start()
|
|
321
|
+
started = True
|
|
322
|
+
finally:
|
|
323
|
+
threading.stack_size(stack)
|
|
324
|
+
worker.join()
|
|
325
|
+
except (ValueError, RuntimeError):
|
|
326
|
+
if started:
|
|
327
|
+
raise
|
|
328
|
+
finally:
|
|
329
|
+
sys.setrecursionlimit(limit)
|
|
330
|
+
if not started:
|
|
331
|
+
return function() # no worker could be started
|
|
332
|
+
if "error" in outcome:
|
|
333
|
+
raise outcome["error"]
|
|
334
|
+
return outcome["value"]
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _deep_detail(depth: int, who: str) -> str:
|
|
338
|
+
"""The sentence that names the nesting depth a reader could not follow."""
|
|
339
|
+
return (f"a formula nested {depth} levels deep is deeper than {who} can read within the "
|
|
340
|
+
f"interpreter's recursion limit ({sys.getrecursionlimit()}): nothing was decided")
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _name_nesting(verdict: Verdict, depth: int) -> Verdict:
|
|
344
|
+
"""``verdict`` with the nesting depth named, when the member failed because of it.
|
|
345
|
+
|
|
346
|
+
A backend that ran out of recursion on a deep formula answers ``error`` /
|
|
347
|
+
``infra`` with the bare ``RecursionError``; here it becomes ``unknown`` /
|
|
348
|
+
``bound_hit`` with the depth in its detail, the way the tableau reports a formula
|
|
349
|
+
nested deeper than it can read (the recursion limit is a bound of the search). Any
|
|
350
|
+
other member that mentions the recursion limit gets the depth appended to what it said.
|
|
351
|
+
"""
|
|
352
|
+
if depth < _SHALLOW_DEPTH or verdict.is_definitive:
|
|
353
|
+
return verdict
|
|
354
|
+
from dataclasses import replace as _replace
|
|
355
|
+
|
|
356
|
+
detail = verdict.detail or ""
|
|
357
|
+
if verdict.status == ERROR and verdict.reason == "infra" and detail.startswith("RecursionError"):
|
|
358
|
+
return _replace(verdict, status=UNKNOWN, reason="bound_hit", szs_status=None,
|
|
359
|
+
detail=_deep_detail(depth, f"the {verdict.backend} backend"))
|
|
360
|
+
said = detail.lower()
|
|
361
|
+
if ("recursion limit" in said or "recursion depth" in said) and "levels deep" not in said:
|
|
362
|
+
return _replace(verdict, detail=f"{detail} ({_deep_detail(depth, 'the backend')})")
|
|
363
|
+
return verdict
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
# ---------------------------------------------------------------------------
|
|
367
|
+
# check — well-formedness + optional signature conformance
|
|
368
|
+
# ---------------------------------------------------------------------------
|
|
369
|
+
|
|
370
|
+
@dataclass(frozen=True)
|
|
371
|
+
class CheckResult:
|
|
372
|
+
"""Outcome of :func:`check` — a ValidationReport plus signature errors.
|
|
373
|
+
|
|
374
|
+
``ok`` is the headline: parseable AND closed AND arity-consistent AND
|
|
375
|
+
lambda-free AND (when a signature was given) signature-conformant.
|
|
376
|
+
``signature_errors`` entries are dicts with ``kind`` ∈
|
|
377
|
+
``{"unknown_predicate", "unknown_function", "unknown_constant",
|
|
378
|
+
"wrong_arity"}`` plus ``symbol`` and, where applicable,
|
|
379
|
+
``expected``/``seen`` and a ``suggestion`` (closest signature symbol).
|
|
380
|
+
"""
|
|
381
|
+
|
|
382
|
+
ok: bool
|
|
383
|
+
parseable: bool
|
|
384
|
+
is_closed: bool
|
|
385
|
+
free_variables: Tuple[str, ...]
|
|
386
|
+
arity_consistent: bool
|
|
387
|
+
arity_conflicts: Tuple[dict, ...]
|
|
388
|
+
has_lambdas: bool
|
|
389
|
+
predicates: Tuple[str, ...]
|
|
390
|
+
functions: Tuple[str, ...]
|
|
391
|
+
constants: Tuple[str, ...]
|
|
392
|
+
signature_errors: Tuple[dict, ...] = ()
|
|
393
|
+
error: Optional[str] = None
|
|
394
|
+
|
|
395
|
+
def __bool__(self) -> bool:
|
|
396
|
+
return self.ok
|
|
397
|
+
|
|
398
|
+
def to_dict(self) -> dict:
|
|
399
|
+
return {
|
|
400
|
+
"ok": self.ok,
|
|
401
|
+
"parseable": self.parseable,
|
|
402
|
+
"is_closed": self.is_closed,
|
|
403
|
+
"free_variables": list(self.free_variables),
|
|
404
|
+
"arity_consistent": self.arity_consistent,
|
|
405
|
+
"arity_conflicts": list(self.arity_conflicts),
|
|
406
|
+
"has_lambdas": self.has_lambdas,
|
|
407
|
+
"predicates": list(self.predicates),
|
|
408
|
+
"functions": list(self.functions),
|
|
409
|
+
"constants": list(self.constants),
|
|
410
|
+
"signature_errors": list(self.signature_errors),
|
|
411
|
+
"error": self.error,
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def _closest(name: str, candidates) -> Optional[str]:
|
|
416
|
+
"""The lexically closest candidate for a did-you-mean suggestion."""
|
|
417
|
+
import difflib
|
|
418
|
+
matches = difflib.get_close_matches(name, list(candidates), n=1, cutoff=0.6)
|
|
419
|
+
return matches[0] if matches else None
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _signature_errors(report, signature: dict) -> Tuple[dict, ...]:
|
|
423
|
+
"""Compare a ValidationReport's inventories against a signature spec.
|
|
424
|
+
|
|
425
|
+
``signature`` keys (all optional): ``"predicates"`` / ``"functions"`` map
|
|
426
|
+
name → arity (or an iterable of allowed arities); ``"constants"`` is an
|
|
427
|
+
iterable of names. Symbols absent from a PROVIDED section are unknown;
|
|
428
|
+
a section left out entirely is unconstrained.
|
|
429
|
+
"""
|
|
430
|
+
def allowed_arities(spec_value):
|
|
431
|
+
if isinstance(spec_value, int):
|
|
432
|
+
return {spec_value}
|
|
433
|
+
return set(spec_value)
|
|
434
|
+
|
|
435
|
+
errors = []
|
|
436
|
+
for section, kind_unknown, inventory in (
|
|
437
|
+
("predicates", "unknown_predicate", report.predicates),
|
|
438
|
+
("functions", "unknown_function", report.functions),
|
|
439
|
+
):
|
|
440
|
+
if section not in signature:
|
|
441
|
+
continue
|
|
442
|
+
spec = signature[section]
|
|
443
|
+
for entry in inventory: # entries look like "Name/2"
|
|
444
|
+
name, _, arity_text = entry.rpartition("/")
|
|
445
|
+
arity = int(arity_text)
|
|
446
|
+
if name not in spec:
|
|
447
|
+
errors.append({
|
|
448
|
+
"kind": kind_unknown, "symbol": name, "arity": arity,
|
|
449
|
+
"suggestion": _closest(name, spec),
|
|
450
|
+
})
|
|
451
|
+
elif arity not in allowed_arities(spec[name]):
|
|
452
|
+
errors.append({
|
|
453
|
+
"kind": "wrong_arity", "symbol": name,
|
|
454
|
+
"expected": sorted(allowed_arities(spec[name])), "seen": arity,
|
|
455
|
+
"suggestion": None,
|
|
456
|
+
})
|
|
457
|
+
if "constants" in signature:
|
|
458
|
+
known = set(signature["constants"])
|
|
459
|
+
for name in report.constants:
|
|
460
|
+
if name not in known:
|
|
461
|
+
errors.append({
|
|
462
|
+
"kind": "unknown_constant", "symbol": name,
|
|
463
|
+
"suggestion": _closest(name, known),
|
|
464
|
+
})
|
|
465
|
+
return tuple(errors)
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def _declares_sorts(signature) -> bool:
|
|
469
|
+
"""Whether a signature dict is in the rich form :meth:`Signature.to_dict` emits.
|
|
470
|
+
|
|
471
|
+
That is the case when it has a ``"sorts"`` or a ``"subsorts"`` key, when an
|
|
472
|
+
entry of ``"predicates"`` / ``"functions"`` is itself a dict
|
|
473
|
+
(``{"arity": 1, "arg_sorts": None}``), or when ``"constants"`` is a dict that
|
|
474
|
+
gives some constant a sort. Everything else is the loose convention of
|
|
475
|
+
:func:`_signature_errors`.
|
|
476
|
+
"""
|
|
477
|
+
from collections.abc import Mapping
|
|
478
|
+
|
|
479
|
+
if "sorts" in signature or "subsorts" in signature:
|
|
480
|
+
return True
|
|
481
|
+
for section in ("predicates", "functions"):
|
|
482
|
+
spec = signature.get(section)
|
|
483
|
+
if isinstance(spec, Mapping) and any(isinstance(v, Mapping) for v in spec.values()):
|
|
484
|
+
return True
|
|
485
|
+
constants = signature.get("constants")
|
|
486
|
+
return isinstance(constants, Mapping) and any(v is not None for v in constants.values())
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def _loose_signature(signature) -> dict:
|
|
490
|
+
"""The loose spec of ``signature`` (a dict), after checking its shape.
|
|
491
|
+
|
|
492
|
+
``ValueError`` naming the offending key for what :func:`_signature_errors` could
|
|
493
|
+
not read: a key other than ``predicates`` / ``functions`` / ``constants``, a
|
|
494
|
+
section that is not a dict from a name to an arity (an ``int``, or a list of
|
|
495
|
+
allowed arities), a ``constants`` section that is not a list of names. It is a
|
|
496
|
+
``ValueError`` (not a ``TypeError``) because the dict is a document, often a
|
|
497
|
+
JSON file, whose content is invalid; the command line reports that one cleanly.
|
|
498
|
+
"""
|
|
499
|
+
from collections.abc import Mapping
|
|
500
|
+
|
|
501
|
+
allowed = ("constants", "functions", "predicates")
|
|
502
|
+
unknown = sorted(str(key) for key in signature if key not in allowed)
|
|
503
|
+
if unknown:
|
|
504
|
+
raise ValueError(
|
|
505
|
+
f"check: signature has the unknown key(s) {unknown}; the loose form has "
|
|
506
|
+
f"{list(allowed)} (the form Signature.to_dict() emits also has 'sorts' and "
|
|
507
|
+
"'subsorts')")
|
|
508
|
+
for section in ("predicates", "functions"):
|
|
509
|
+
if section not in signature:
|
|
510
|
+
continue
|
|
511
|
+
spec = signature[section]
|
|
512
|
+
if not isinstance(spec, Mapping):
|
|
513
|
+
raise ValueError(
|
|
514
|
+
f"check: signature[{section!r}] must be a dict from each name to its arity "
|
|
515
|
+
f"(an int, or a list of allowed arities), got {type(spec).__name__} {spec!r}")
|
|
516
|
+
for name, value in spec.items():
|
|
517
|
+
arities = value if isinstance(value, (list, tuple, set, frozenset)) else (value,)
|
|
518
|
+
if not all(isinstance(a, int) and not isinstance(a, bool) for a in arities):
|
|
519
|
+
raise ValueError(
|
|
520
|
+
f"check: signature[{section!r}][{name!r}] must be an arity (an int) or a "
|
|
521
|
+
f"list of allowed arities, got {value!r}")
|
|
522
|
+
if "constants" in signature:
|
|
523
|
+
constants = signature["constants"]
|
|
524
|
+
if (isinstance(constants, str) or not isinstance(constants, (list, tuple, set, frozenset, Mapping))
|
|
525
|
+
or not all(isinstance(c, str) for c in constants)):
|
|
526
|
+
raise ValueError(
|
|
527
|
+
f"check: signature['constants'] must be a list of constant names, got "
|
|
528
|
+
f"{type(constants).__name__} {constants!r}")
|
|
529
|
+
return dict(signature)
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def _read_signature(signature):
|
|
533
|
+
"""``(loose spec, Signature or None)`` for the ``signature=`` of :func:`check`.
|
|
534
|
+
|
|
535
|
+
A :class:`~unicode_logic_kit.fol.signature.Signature` is projected onto the loose
|
|
536
|
+
convention (name → arity per namespace, constant name list) and kept as the
|
|
537
|
+
object, because its sort declarations have no loose counterpart. A dict in the
|
|
538
|
+
rich form that :meth:`~unicode_logic_kit.fol.signature.Signature.to_dict` emits is
|
|
539
|
+
read as the ``Signature`` it describes (:meth:`~unicode_logic_kit.fol.signature.Signature.from_dict`,
|
|
540
|
+
whose own refusals name the entry), so what ``to_dict`` returns passes
|
|
541
|
+
:func:`check` unchanged. Any other dict is the loose convention, shape-checked.
|
|
542
|
+
``None`` is ``(None, None)``. ``TypeError`` for an argument that is neither a
|
|
543
|
+
``Signature`` nor a dict, ``ValueError`` (naming the entry) for a dict that is
|
|
544
|
+
malformed: a malformed ``signature=`` is a mistake of the caller's, never a
|
|
545
|
+
verdict about a formula.
|
|
546
|
+
"""
|
|
547
|
+
from collections.abc import Mapping
|
|
548
|
+
from .fol.signature import Signature as _Signature
|
|
549
|
+
|
|
550
|
+
if signature is None:
|
|
551
|
+
return None, None
|
|
552
|
+
if isinstance(signature, Mapping):
|
|
553
|
+
if not _declares_sorts(signature):
|
|
554
|
+
return _loose_signature(signature), None
|
|
555
|
+
try:
|
|
556
|
+
signature = _Signature.from_dict(signature)
|
|
557
|
+
except TypeError as exc: # from_dict says "wrong type" with a TypeError
|
|
558
|
+
raise ValueError(str(exc)) from exc
|
|
559
|
+
if not isinstance(signature, _Signature):
|
|
560
|
+
raise TypeError(
|
|
561
|
+
f"check: signature= must be a unicode_logic_kit.fol.signature.Signature or a dict, got "
|
|
562
|
+
f"{type(signature).__name__}")
|
|
563
|
+
# Project onto the loose convention for the classic diagnostics (unknown
|
|
564
|
+
# symbol / wrong arity, with did-you-mean suggestions), but KEEP the object:
|
|
565
|
+
# its sort declarations have no loose-dict counterpart and are checked
|
|
566
|
+
# additionally in check() (dropping them silently returned ok=True on
|
|
567
|
+
# sort-violating formulas the object itself rejects).
|
|
568
|
+
return {
|
|
569
|
+
"predicates": {d.name: d.arity for d in signature.predicates.values()},
|
|
570
|
+
"functions": {d.name: d.arity for d in signature.functions.values()},
|
|
571
|
+
"constants": sorted(signature.constants),
|
|
572
|
+
}, signature
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
def check(formula: Union[Node, str], *, signature=None,
|
|
576
|
+
dialect: Optional[str] = None) -> CheckResult:
|
|
577
|
+
"""Well-formedness report for a formula (or raw text) — never raises for a formula.
|
|
578
|
+
|
|
579
|
+
Accepts a parsed ``Node`` or a raw string (which goes through
|
|
580
|
+
:func:`parse_any` first, honouring ``dialect`` as its hint). The
|
|
581
|
+
structural checks are :func:`unicode_logic_kit.eval.validate`'s: closedness,
|
|
582
|
+
per-namespace arity consistency, lambda residue. ``signature`` adds
|
|
583
|
+
vocabulary conformance with did-you-mean suggestions (see
|
|
584
|
+
:func:`_signature_errors` for the loose-dict spec format) — and also
|
|
585
|
+
accepts a first-class :class:`unicode_logic_kit.fol.signature.Signature`,
|
|
586
|
+
which is projected onto that loose convention (name → arity per
|
|
587
|
+
namespace, constant name list) so the structured did-you-mean
|
|
588
|
+
diagnostics stay identical either way, and the dict that
|
|
589
|
+
:meth:`~unicode_logic_kit.fol.signature.Signature.to_dict` returns (what the
|
|
590
|
+
``get_signature`` tool hands out), which is read as the ``Signature`` it
|
|
591
|
+
describes. The two truth constants (``⊤`` / ``$true``, ``⊥`` / ``$false``) are
|
|
592
|
+
logical constants, not predicates: they are listed in no inventory and are
|
|
593
|
+
never an unknown predicate. A malformed ``signature`` is a mistake of the
|
|
594
|
+
caller's, not a property of the formula: ``TypeError`` for an argument that is
|
|
595
|
+
neither a ``Signature`` nor a dict, ``ValueError`` naming the entry for a dict
|
|
596
|
+
with a key that is none of the sections, a section of the wrong type, or a rich
|
|
597
|
+
entry ``Signature.from_dict`` refuses. A formula nested too deeply for the
|
|
598
|
+
checks to read it (see :func:`prove`) comes back as ``ok=False`` with ``error``
|
|
599
|
+
naming the nesting depth, never as a ``RecursionError``.
|
|
600
|
+
"""
|
|
601
|
+
from .eval.validate import validate
|
|
602
|
+
|
|
603
|
+
signature, signature_object = _read_signature(signature)
|
|
604
|
+
|
|
605
|
+
if isinstance(formula, str):
|
|
606
|
+
parsed = parse_any(formula, hint=dialect)
|
|
607
|
+
if not parsed.ok or parsed.formula is None: # an ok result always carries its formula
|
|
608
|
+
message = parsed.errors[-1]["message"] if parsed.errors else "unparseable"
|
|
609
|
+
return CheckResult(
|
|
610
|
+
ok=False, parseable=False, is_closed=False, free_variables=(),
|
|
611
|
+
arity_consistent=False, arity_conflicts=(), has_lambdas=False,
|
|
612
|
+
predicates=(), functions=(), constants=(), error=message)
|
|
613
|
+
formula = parsed.formula
|
|
614
|
+
|
|
615
|
+
def examine() -> CheckResult:
|
|
616
|
+
report = validate(formula)
|
|
617
|
+
conflicts = tuple(
|
|
618
|
+
{"namespace": ns, "symbol": name, "arities": list(arities)}
|
|
619
|
+
for (ns, name), arities in sorted(report.arity_conflicts.items()))
|
|
620
|
+
sig_errors = _signature_errors(report, signature) if signature else ()
|
|
621
|
+
if signature_object is not None:
|
|
622
|
+
# The Signature's own validate() covers what the projection cannot:
|
|
623
|
+
# declared argument/result/constant SORTS. Only its sort messages
|
|
624
|
+
# are added (the undeclared/arity classes are already reported
|
|
625
|
+
# above with richer, did-you-mean-carrying dicts).
|
|
626
|
+
sort_errors = tuple(
|
|
627
|
+
{"kind": "sort_mismatch", "message": message, "suggestion": None}
|
|
628
|
+
for message in signature_object.validate(formula)
|
|
629
|
+
if "sort" in message)
|
|
630
|
+
sig_errors = tuple(sig_errors) + sort_errors
|
|
631
|
+
ok = (report.is_closed and report.arity_consistent
|
|
632
|
+
and not report.has_lambdas and not sig_errors)
|
|
633
|
+
return CheckResult(
|
|
634
|
+
ok=ok, parseable=True,
|
|
635
|
+
is_closed=report.is_closed, free_variables=report.free_variable_names,
|
|
636
|
+
arity_consistent=report.arity_consistent, arity_conflicts=conflicts,
|
|
637
|
+
has_lambdas=report.has_lambdas,
|
|
638
|
+
predicates=report.predicates, functions=report.functions,
|
|
639
|
+
constants=report.constants,
|
|
640
|
+
signature_errors=sig_errors)
|
|
641
|
+
|
|
642
|
+
depth = _nesting_depth(formula)
|
|
643
|
+
try:
|
|
644
|
+
return _call_deep(depth, examine)
|
|
645
|
+
except RecursionError:
|
|
646
|
+
# nested deeper than a recursive reader can follow, even on the deep worker: the
|
|
647
|
+
# report is not made up, the result says why there is none
|
|
648
|
+
return CheckResult(
|
|
649
|
+
ok=False, parseable=True, is_closed=False, free_variables=(),
|
|
650
|
+
arity_consistent=False, arity_conflicts=(), has_lambdas=False,
|
|
651
|
+
predicates=(), functions=(), constants=(),
|
|
652
|
+
error=_deep_detail(depth, "the checks"))
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
@functools.wraps(_equivalent)
|
|
656
|
+
def equivalent(prediction: Node, reference: Node, **options) -> EquivalenceResult:
|
|
657
|
+
depth = _nesting_depth(prediction, reference)
|
|
658
|
+
try:
|
|
659
|
+
return _call_deep(depth, lambda: _equivalent(prediction, reference, **options))
|
|
660
|
+
except RecursionError:
|
|
661
|
+
# nested deeper than a recursive level can follow, even on the deep worker: the
|
|
662
|
+
# question stays undecided, and the result says why
|
|
663
|
+
method = options.get("method", "auto")
|
|
664
|
+
return EquivalenceResult(
|
|
665
|
+
equivalent=None,
|
|
666
|
+
method_used="solver" if method == "auto" else method,
|
|
667
|
+
reason=_deep_detail(depth, "the equivalence levels"))
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
# ---------------------------------------------------------------------------
|
|
671
|
+
# prove / countermodel — the backend chain
|
|
672
|
+
# ---------------------------------------------------------------------------
|
|
673
|
+
|
|
674
|
+
#: Node types that are the SOLE occupant of their own parser mode — a formula
|
|
675
|
+
#: containing one of these can only have come from that mode's grammar, so
|
|
676
|
+
#: routing on it is sound (see logic_backends' module docstring). Order
|
|
677
|
+
#: matters: hybrid is checked before the general modal test because
|
|
678
|
+
#: `atp.modal_tableau.has_modal` itself also counts Nominal/At as "modal"
|
|
679
|
+
#: (so the classical tableau gets a clean hybrid-specific rejection) — here
|
|
680
|
+
#: we want the MORE SPECIFIC "hybrid" logic label, not "modal", for exactly
|
|
681
|
+
#: those formulas. Intuitionistic and relevant logic reuse the plain
|
|
682
|
+
#: classical AST with no marker of their own, so they are deliberately
|
|
683
|
+
#: ABSENT from this table — `logic=` must be given explicitly for those two.
|
|
684
|
+
def _detect_logic(formula: Node, premises: Sequence[Node]) -> str:
|
|
685
|
+
from .atp.modal_tableau import has_modal
|
|
686
|
+
from .fol.nodes import Nominal, At, Product, Under, Over
|
|
687
|
+
from .fol._linear_nodes import (
|
|
688
|
+
Tensor, With, OPlus, LinearImplies, OfCourse, One, Top, Zero,
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
formulas = (formula, *premises)
|
|
692
|
+
|
|
693
|
+
def _contains(*node_types) -> bool:
|
|
694
|
+
return any(isinstance(node, node_types)
|
|
695
|
+
for f in formulas for node in f.walk())
|
|
696
|
+
|
|
697
|
+
if _contains(Nominal, At):
|
|
698
|
+
return "hybrid"
|
|
699
|
+
if _contains(Product, Under, Over):
|
|
700
|
+
return "lambek"
|
|
701
|
+
if _contains(Tensor, With, OPlus, LinearImplies, OfCourse, One, Top, Zero):
|
|
702
|
+
return "ill"
|
|
703
|
+
if any(has_modal(f) for f in formulas):
|
|
704
|
+
return "modal"
|
|
705
|
+
return "fol"
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
#: Which backend names this facade knows how to ask for premise relevance,
|
|
709
|
+
#: and the free function that answers it — see :func:`_attach_relevant_premises`.
|
|
710
|
+
#: Deliberately NOT every backend :func:`prove` can route to: a route with no
|
|
711
|
+
#: entry here leaves ``Verdict.relevant_premises`` at its default ``None``
|
|
712
|
+
#: rather than guessing. Vampire has no entry either, for the opposite reason:
|
|
713
|
+
#: its own verdict already carries the caller's premise indices (the backend asks
|
|
714
|
+
#: Vampire for the names of the axioms of its proof), so there is nothing to
|
|
715
|
+
#: re-ask.
|
|
716
|
+
def _relevant_premises_for(backend_name: str, formula: Node, premises: Sequence[Node],
|
|
717
|
+
timeout: int) -> Optional[Tuple[int, ...]]:
|
|
718
|
+
if backend_name == "z3":
|
|
719
|
+
from .atp.protocol import z3_relevant_premises
|
|
720
|
+
return z3_relevant_premises(formula, premises, timeout=timeout)
|
|
721
|
+
if backend_name == "eprover":
|
|
722
|
+
from .atp.eprover_backend import eprover_relevant_premises
|
|
723
|
+
return eprover_relevant_premises(list(premises), formula, timeout=max(1, timeout // 1000))
|
|
724
|
+
return None
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
def _attach_relevant_premises(verdict: Verdict, formula: Node, premises: Sequence[Node],
|
|
728
|
+
timeout: int) -> Verdict:
|
|
729
|
+
"""Best-effort: fill ``relevant_premises`` on a PROVED verdict, using
|
|
730
|
+
whichever route :func:`_relevant_premises_for` supports for the backend
|
|
731
|
+
that actually produced ``verdict`` — re-running that SAME query, not a
|
|
732
|
+
different one, so the reported premises are relevant to the ANSWER the
|
|
733
|
+
caller got, not to some other backend's independent proof of the same
|
|
734
|
+
entailment. Leaves ``verdict`` untouched (``relevant_premises`` stays its
|
|
735
|
+
default ``None``) when the winning backend has no route, or that route
|
|
736
|
+
itself could not produce a trustworthy answer.
|
|
737
|
+
"""
|
|
738
|
+
if verdict.status != PROVED or verdict.relevant_premises is not None:
|
|
739
|
+
return verdict # a backend that already reports them (Vampire) is not asked again
|
|
740
|
+
try:
|
|
741
|
+
indices = _relevant_premises_for(verdict.backend, formula, premises, timeout)
|
|
742
|
+
except RecursionError:
|
|
743
|
+
return verdict # a formula nested deeper than the re-run can read: no breakdown
|
|
744
|
+
if indices is None:
|
|
745
|
+
return verdict
|
|
746
|
+
from dataclasses import replace as _replace
|
|
747
|
+
return _replace(verdict, relevant_premises=indices)
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
def _unwrap_sentences(formula, premises, route: str):
|
|
751
|
+
"""Accept :class:`unicode_logic_kit.logic.Sentence` values next to bare nodes.
|
|
752
|
+
|
|
753
|
+
A Sentence carries the side axioms of the translation that produced it (the
|
|
754
|
+
non-emptiness of every sort AND the membership atom ``S(c)`` of every sorted
|
|
755
|
+
constant of a many-sorted formula, frame conditions, a domain regime). Those are exactly the
|
|
756
|
+
premises the question needs, so a Sentence handed to a decision route is
|
|
757
|
+
unwrapped WITH them instead of having them silently dropped — dropping them
|
|
758
|
+
is how a valid formula comes back REFUTED. A Sentence in a logic other than
|
|
759
|
+
classical FOL is refused by name: convert it first.
|
|
760
|
+
"""
|
|
761
|
+
from .logic import Sentence # local: logic imports api-free
|
|
762
|
+
extra = []
|
|
763
|
+
def one(value, what):
|
|
764
|
+
if not isinstance(value, Sentence):
|
|
765
|
+
return value
|
|
766
|
+
if value.logic not in ("fol", "msfol"):
|
|
767
|
+
raise ValueError(
|
|
768
|
+
f"{route}: {what} is a Sentence in logic {value.logic!r}, which "
|
|
769
|
+
f"these routes do not decide — convert it first, e.g. "
|
|
770
|
+
f"FOL(sentence) (unicode_logic_kit.logic), and pass that")
|
|
771
|
+
extra.extend(value.axioms)
|
|
772
|
+
return value.term
|
|
773
|
+
formula = one(formula, "the goal")
|
|
774
|
+
premises = [one(p, f"premise {i}") for i, p in enumerate(premises)]
|
|
775
|
+
return formula, premises + extra
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
def _signature_premises(caller: str, signature, logic: str) -> Tuple[Node, ...]:
|
|
779
|
+
"""The premises that ``signature=`` adds to a call: :func:`~unicode_logic_kit.fol.signature_axioms`.
|
|
780
|
+
|
|
781
|
+
``TypeError`` for anything but a :class:`~unicode_logic_kit.fol.signature.Signature`
|
|
782
|
+
(a dict is turned into one with ``Signature.from_dict``), and ``ValueError`` for a
|
|
783
|
+
logic other than classical first-order logic: the sentences are plain first-order
|
|
784
|
+
assumptions, and a modal, hybrid, intuitionistic or substructural route would read
|
|
785
|
+
them at one world or one resource only, where a sort guard is world-relative and a
|
|
786
|
+
constant is a rigid designator.
|
|
787
|
+
"""
|
|
788
|
+
from .fol.signature import Signature
|
|
789
|
+
from .fol._msfl_nodes import signature_axioms
|
|
790
|
+
|
|
791
|
+
if not isinstance(signature, Signature):
|
|
792
|
+
raise TypeError(
|
|
793
|
+
f"{caller}: signature= must be a unicode_logic_kit.fol.signature.Signature, got "
|
|
794
|
+
f"{type(signature).__name__}; build one from a dict with Signature.from_dict(d)")
|
|
795
|
+
if logic != "fol":
|
|
796
|
+
raise ValueError(
|
|
797
|
+
f"{caller}: signature= is read by the classical first-order routes only, and "
|
|
798
|
+
f"the logic of this call is {logic!r}: what a signature declares is added as "
|
|
799
|
+
"plain first-order assumptions, and a route for another logic would read them "
|
|
800
|
+
"at one world (or one resource) only -- a sort guard is world-relative and a "
|
|
801
|
+
"constant is a rigid designator there. Leave signature= out, or state the "
|
|
802
|
+
"declarations in the formula.")
|
|
803
|
+
return signature_axioms(signature)
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
def _name_background(options: dict, passed: int, total: int, caller: str) -> dict:
|
|
807
|
+
"""``options`` with ``premise_names=`` extended by names for the premises that were
|
|
808
|
+
appended to the ``passed`` the caller gave (the signature's sentences, the side axioms of
|
|
809
|
+
a Sentence): ``total`` premises are asked, the caller named only its own.
|
|
810
|
+
|
|
811
|
+
The names are the caller's premises', so what a prover reports about its proof is read as
|
|
812
|
+
the caller's own premises; the appended ones are background and are named by the writer
|
|
813
|
+
(:func:`~unicode_logic_kit.atp._writer_support.name_background_premises`). Without
|
|
814
|
+
``premise_names=``, or when nothing was appended, ``options`` is returned as it is."""
|
|
815
|
+
names = options.get("premise_names")
|
|
816
|
+
if names is None or total == passed:
|
|
817
|
+
return options
|
|
818
|
+
from .atp._writer_support import name_background_premises
|
|
819
|
+
return {**options, "premise_names": name_background_premises(names, passed, total,
|
|
820
|
+
where=caller)}
|
|
821
|
+
|
|
822
|
+
|
|
823
|
+
def _without_background(verdict: Verdict, given: int) -> Verdict:
|
|
824
|
+
"""``verdict`` with the premises that ``signature=`` appended removed from what it indexes.
|
|
825
|
+
|
|
826
|
+
A backend that tracks its premises (Z3's unsat core, a premise-relevance query)
|
|
827
|
+
names them by position, and the signature's sentences stand after the caller's
|
|
828
|
+
``given`` premises. Only the caller's own premises are reported back, as indices
|
|
829
|
+
into the caller's own list.
|
|
830
|
+
"""
|
|
831
|
+
from dataclasses import replace as _replace
|
|
832
|
+
|
|
833
|
+
changes: dict = {}
|
|
834
|
+
if verdict.relevant_premises is not None:
|
|
835
|
+
kept = tuple(i for i in verdict.relevant_premises if i < given)
|
|
836
|
+
if kept != tuple(verdict.relevant_premises):
|
|
837
|
+
changes["relevant_premises"] = kept
|
|
838
|
+
proof = verdict.proof
|
|
839
|
+
if isinstance(proof, dict) and proof.get("kind") == "z3_unsat_core":
|
|
840
|
+
core = list(proof.get("core", ()))
|
|
841
|
+
kept_core = [tag for tag in core
|
|
842
|
+
if not (tag.startswith("p") and tag[1:].isdigit() and int(tag[1:]) >= given)]
|
|
843
|
+
if kept_core != core:
|
|
844
|
+
changes["proof"] = {**proof, "core": kept_core}
|
|
845
|
+
return _replace(verdict, **changes) if changes else verdict
|
|
846
|
+
|
|
847
|
+
|
|
848
|
+
def prove(formula: Node, premises: Sequence[Node] = (), *,
|
|
849
|
+
logic: str = "auto", backends: Optional[Sequence[str]] = None,
|
|
850
|
+
timeout: int = 10000, require_agreement: int = 1,
|
|
851
|
+
relevant_premises: bool = False, signature=None,
|
|
852
|
+
**options) -> Verdict:
|
|
853
|
+
"""Decide ``premises ⊨ formula`` over a chain of backends.
|
|
854
|
+
|
|
855
|
+
``logic="auto"`` routes by syntax (modal operators → the modal chain).
|
|
856
|
+
``backends=None`` uses :func:`~unicode_logic_kit.atp.protocol.default_chain`
|
|
857
|
+
— fast, deterministic, and never silently expensive (the minutes-per-call
|
|
858
|
+
``isabelle`` backend runs only when named explicitly). An explicitly named
|
|
859
|
+
backend that is missing raises
|
|
860
|
+
:class:`~unicode_logic_kit.atp.protocol.BackendUnavailable`; one that does
|
|
861
|
+
not support the detected logic raises ``ValueError``.
|
|
862
|
+
|
|
863
|
+
The chain runs in order and returns the first DEFINITIVE verdict (proved /
|
|
864
|
+
refuted). With ``require_agreement=n`` > 1 it keeps going until ``n``
|
|
865
|
+
backends report the same definitive status — the returned verdict's
|
|
866
|
+
``agreement`` then lists them all. If nothing definitive emerges, the
|
|
867
|
+
result is an UNKNOWN verdict from the pseudo-backend ``"chain"`` whose
|
|
868
|
+
``detail`` summarises every member's answer: for each member that ran, its
|
|
869
|
+
status, its reason, and its own account of them when it gave one (the
|
|
870
|
+
refusal of a prover that would not read its input, the bound a search hit).
|
|
871
|
+
When EVERY member failed the result is an ERROR verdict
|
|
872
|
+
rather than an UNKNOWN one: nothing was asked and answered, and "unknown"
|
|
873
|
+
would read like a question that ran out of time.
|
|
874
|
+
|
|
875
|
+
``relevant_premises=True`` additionally fills the returned verdict's
|
|
876
|
+
``relevant_premises`` field on a PROVED result, by re-asking the SAME
|
|
877
|
+
winning backend which premises it actually needed (see
|
|
878
|
+
:mod:`unicode_logic_kit.atp.protocol`'s ``z3_relevant_premises`` and
|
|
879
|
+
:mod:`unicode_logic_kit.atp.eprover_backend`'s
|
|
880
|
+
``eprover_relevant_premises``) — currently supported only when that
|
|
881
|
+
backend is ``"z3"`` or ``"eprover"``; any other winning backend (or a
|
|
882
|
+
query that route itself could not answer) leaves the field at its
|
|
883
|
+
default ``None`` rather than guessing. Off by default: it re-runs the
|
|
884
|
+
winning backend's decision procedure a second time, so only pay for it
|
|
885
|
+
when the caller actually wants the breakdown. The ``"vampire"`` backend
|
|
886
|
+
needs no second run: its verdict carries the caller's premise indices
|
|
887
|
+
whenever its proof names its axioms (see
|
|
888
|
+
:class:`~unicode_logic_kit.atp.protocol.VampireBackend`), with or without
|
|
889
|
+
this flag, and ``premise_names=`` names the premises it reads them by.
|
|
890
|
+
The indices are indices into ``premises`` as the caller gave them: the side
|
|
891
|
+
axioms that a :class:`~unicode_logic_kit.logic.Sentence` brings along (the
|
|
892
|
+
non-emptiness of its sorts, the membership atoms of its sorted constants) and
|
|
893
|
+
the sentences of ``signature=`` are background, and are never reported as
|
|
894
|
+
premises.
|
|
895
|
+
|
|
896
|
+
``signature=`` (a :class:`~unicode_logic_kit.fol.signature.Signature`; a dict
|
|
897
|
+
is a ``TypeError`` that points at ``Signature.from_dict``) states what the
|
|
898
|
+
caller declared: the sorts, the sort of each constant, the argument and result
|
|
899
|
+
sorts of each function, the subsort edges. What it declares is added to the
|
|
900
|
+
premises of every backend of the chain as the sentences
|
|
901
|
+
:func:`~unicode_logic_kit.fol.signature_axioms` returns, so ``f: A → B`` makes
|
|
902
|
+
``∃x:B x = f(carl:A)`` valid, a constant declared in a sort is in it, and a
|
|
903
|
+
subsort edge ``A < B`` is the inclusion ``A ⊆ B``. A predicate's declared
|
|
904
|
+
argument sorts add nothing: a predicate is a relation over the whole universe.
|
|
905
|
+
Indices that come back (``relevant_premises``, the Z3 core) stay indices into
|
|
906
|
+
``premises``. The input is NOT checked against the signature -- a formula
|
|
907
|
+
that uses an undeclared symbol is decided as written; that check is
|
|
908
|
+
``api.check(formula, signature=...)``. Only the classical first-order routes
|
|
909
|
+
read a signature (``ValueError`` for another logic).
|
|
910
|
+
|
|
911
|
+
Extra keyword ``options`` go to the backends that read them, each backend
|
|
912
|
+
being handed only the options it declares
|
|
913
|
+
(:meth:`~unicode_logic_kit.atp.protocol.ProverBackend.accepted_options`). An
|
|
914
|
+
option that NO backend of the chain reads is a ``ValueError`` naming the
|
|
915
|
+
option and the chain, raised before anything runs: it would otherwise be
|
|
916
|
+
ignored, and the answer given to a question the caller did not ask. A backend
|
|
917
|
+
that does not read an option that changes the question (a modal ``frame=``,
|
|
918
|
+
``bridges=``, a ``subsorts=`` edge) while another backend of the chain does is
|
|
919
|
+
not run: it is listed in the chain's ``detail`` as ``unknown/unsupported``,
|
|
920
|
+
with the option named. An option that only bounds a search or says where a
|
|
921
|
+
binary lives (``max_steps=``, ``use_wsl=``) is simply not handed to a backend
|
|
922
|
+
that has no use for it.
|
|
923
|
+
|
|
924
|
+
A formula nested a hundred levels deep or more is decided on a worker thread
|
|
925
|
+
whose stack and recursion limit are sized for it (up to a nesting of about
|
|
926
|
+
eight thousand levels), so the interpreter's recursion limit does not decide
|
|
927
|
+
which backend can read it. A backend that still cannot is ``unknown`` /
|
|
928
|
+
``bound_hit`` and its detail names the nesting depth; nothing in this
|
|
929
|
+
function raises ``RecursionError``.
|
|
930
|
+
"""
|
|
931
|
+
premises = list(premises)
|
|
932
|
+
passed_premises = len(premises)
|
|
933
|
+
formula, premises = _unwrap_sentences(formula, premises, "prove")
|
|
934
|
+
if logic == "auto":
|
|
935
|
+
logic = _detect_logic(formula, premises)
|
|
936
|
+
chain = tuple(backends) if backends is not None else default_chain(logic)
|
|
937
|
+
|
|
938
|
+
for name in chain:
|
|
939
|
+
backend = get_backend(name) # ValueError on unknown names
|
|
940
|
+
if logic not in backend.logics:
|
|
941
|
+
raise ValueError(
|
|
942
|
+
f"prove: backend {name!r} does not support logic {logic!r} "
|
|
943
|
+
f"(it handles {sorted(backend.logics)})")
|
|
944
|
+
|
|
945
|
+
if signature is not None:
|
|
946
|
+
premises = [*premises, *_signature_premises("prove", signature, logic)]
|
|
947
|
+
options = _name_background(options, passed_premises, len(premises), "prove")
|
|
948
|
+
plan = plan_options("prove", chain, logic, options)
|
|
949
|
+
depth = _nesting_depth(formula, *premises)
|
|
950
|
+
|
|
951
|
+
def run_chain() -> Verdict:
|
|
952
|
+
verdicts = []
|
|
953
|
+
agreeing: dict = {}
|
|
954
|
+
for name in chain:
|
|
955
|
+
passed, refusal = plan[name]
|
|
956
|
+
if refusal is not None:
|
|
957
|
+
verdicts.append(Verdict(UNKNOWN, name, logic=logic, reason="unsupported",
|
|
958
|
+
detail=refusal))
|
|
959
|
+
continue
|
|
960
|
+
extra = dict(passed)
|
|
961
|
+
if name == "isabelle":
|
|
962
|
+
extra["logic"] = logic # the dual-logic backend routes on it
|
|
963
|
+
verdict = _name_nesting(
|
|
964
|
+
run_backend(name, formula, premises, timeout=timeout, **extra), depth)
|
|
965
|
+
verdicts.append(verdict)
|
|
966
|
+
if verdict.is_definitive:
|
|
967
|
+
group = agreeing.setdefault(verdict.status, [])
|
|
968
|
+
group.append(verdict)
|
|
969
|
+
if len(group) >= require_agreement:
|
|
970
|
+
first = group[0]
|
|
971
|
+
if len(group) == 1:
|
|
972
|
+
result = first
|
|
973
|
+
else:
|
|
974
|
+
from dataclasses import replace as _replace
|
|
975
|
+
result = _replace(first, agreement=tuple(v.backend for v in group))
|
|
976
|
+
if relevant_premises:
|
|
977
|
+
result = _attach_relevant_premises(result, formula, premises, timeout)
|
|
978
|
+
# what the signature and the side axioms of a Sentence added is
|
|
979
|
+
# background: only the caller's own premises are indexed back
|
|
980
|
+
return (_without_background(result, passed_premises)
|
|
981
|
+
if len(premises) > passed_premises else result)
|
|
982
|
+
|
|
983
|
+
return _no_definitive_verdict("chain", logic, verdicts, "empty backend chain")
|
|
984
|
+
|
|
985
|
+
return _call_deep(depth, run_chain)
|
|
986
|
+
|
|
987
|
+
|
|
988
|
+
@dataclass(frozen=True)
|
|
989
|
+
class CountermodelResult:
|
|
990
|
+
"""Outcome of :func:`countermodel`.
|
|
991
|
+
|
|
992
|
+
``found`` says whether a witness exists; ``model`` is the JSON-able
|
|
993
|
+
witness dict (same shapes as ``Verdict.countermodel``), ``backend`` names
|
|
994
|
+
the route that found it, and ``explanation_nl`` is a short plain-English
|
|
995
|
+
rendering from :func:`unicode_logic_kit.eval.explain.explain_countermodel`
|
|
996
|
+
(a structured Kripke witness is rebuilt and narrated world by world; a
|
|
997
|
+
Z3 assignment is read out; an opaque witness falls back to a one-line
|
|
998
|
+
gloss of its kind). ``reason`` is ``None`` unless a backend could not read
|
|
999
|
+
the formula it was given: a formula nested deeper than a recursive reader
|
|
1000
|
+
can follow is named there by its nesting depth, so that ``found=False`` is
|
|
1001
|
+
not mistaken for a finished search.
|
|
1002
|
+
"""
|
|
1003
|
+
|
|
1004
|
+
found: bool
|
|
1005
|
+
model: Optional[dict] = None
|
|
1006
|
+
backend: Optional[str] = None
|
|
1007
|
+
explanation_nl: Optional[str] = None
|
|
1008
|
+
reason: Optional[str] = None
|
|
1009
|
+
|
|
1010
|
+
def __bool__(self) -> bool:
|
|
1011
|
+
return self.found
|
|
1012
|
+
|
|
1013
|
+
def to_dict(self) -> dict:
|
|
1014
|
+
return {"found": self.found, "model": self.model,
|
|
1015
|
+
"backend": self.backend, "explanation_nl": self.explanation_nl,
|
|
1016
|
+
"reason": self.reason}
|
|
1017
|
+
|
|
1018
|
+
|
|
1019
|
+
_WITNESS_GLOSS = {
|
|
1020
|
+
"finite_structure": "A finite first-order structure satisfies the premises "
|
|
1021
|
+
"but falsifies the conclusion.",
|
|
1022
|
+
"z3_model": "An assignment of the symbols (found by Z3) makes the premises "
|
|
1023
|
+
"true and the conclusion false.",
|
|
1024
|
+
"kripke": "A Kripke model falsifies the formula at its root world.",
|
|
1025
|
+
"nitpick": "Isabelle's nitpick found a finite counter-interpretation.",
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
|
|
1029
|
+
def _explain_witness(witness: dict, formula: Node,
|
|
1030
|
+
premises: Sequence[Node]) -> Optional[str]:
|
|
1031
|
+
"""Best-effort plain-English rendering of a Verdict witness dict.
|
|
1032
|
+
|
|
1033
|
+
A ``"kripke"`` witness carrying the structured ``"data"`` payload is
|
|
1034
|
+
rebuilt into a real KripkeModel so
|
|
1035
|
+
:func:`~unicode_logic_kit.eval.explain.explain_countermodel` can narrate
|
|
1036
|
+
its worlds (the formula handed along for the world-0 check is the folded
|
|
1037
|
+
goal ``(∧ premises) → formula`` — that is what the model falsifies, not
|
|
1038
|
+
the bare conclusion). Every other shape goes to ``explain_countermodel``
|
|
1039
|
+
directly. This field is presentational: if the rendering fails for an
|
|
1040
|
+
unforeseen witness payload, the one-line kind gloss is the fallback —
|
|
1041
|
+
never an exception out of :func:`countermodel`.
|
|
1042
|
+
"""
|
|
1043
|
+
from .eval.explain import explain_countermodel
|
|
1044
|
+
|
|
1045
|
+
try:
|
|
1046
|
+
data = witness.get("data")
|
|
1047
|
+
if witness.get("kind") == "kripke" and isinstance(data, dict):
|
|
1048
|
+
from .atp.kripke_enum import kripke_model_from_dict
|
|
1049
|
+
from .fol.nodes import Implies
|
|
1050
|
+
goal = formula
|
|
1051
|
+
if premises:
|
|
1052
|
+
conj = premises[0]
|
|
1053
|
+
for p in premises[1:]:
|
|
1054
|
+
conj = And(conj, p)
|
|
1055
|
+
goal = Implies(conj, goal)
|
|
1056
|
+
return explain_countermodel(kripke_model_from_dict(data), goal)
|
|
1057
|
+
return explain_countermodel(witness)
|
|
1058
|
+
except Exception:
|
|
1059
|
+
kind = witness.get("kind")
|
|
1060
|
+
return _WITNESS_GLOSS.get(kind) if kind is not None else None
|
|
1061
|
+
|
|
1062
|
+
|
|
1063
|
+
def countermodel(formula: Node, premises: Sequence[Node] = (), *,
|
|
1064
|
+
logic: str = "auto", backends: Optional[Sequence[str]] = None,
|
|
1065
|
+
timeout: int = 10000, signature=None,
|
|
1066
|
+
**options) -> CountermodelResult:
|
|
1067
|
+
"""Search for a countermodel to ``premises ⊨ formula``.
|
|
1068
|
+
|
|
1069
|
+
Runs the refutation-capable backends for the logic (FOL: the finite model
|
|
1070
|
+
finder first — its structures are the most readable — then Z3; modal: the
|
|
1071
|
+
labelled tableau, then the bounded Kripke-model enumeration — the only
|
|
1072
|
+
route that can refute a temporal-closure formula) and returns the first
|
|
1073
|
+
witness. ``found=False`` means "no countermodel within the budgets",
|
|
1074
|
+
never a validity claim.
|
|
1075
|
+
|
|
1076
|
+
``signature=`` and the extra ``options`` mean here what they mean in
|
|
1077
|
+
:func:`prove`: what a :class:`~unicode_logic_kit.fol.signature.Signature`
|
|
1078
|
+
declares is added to the premises of every backend, so the witness is a
|
|
1079
|
+
structure in which the declarations hold; an option that no backend of the
|
|
1080
|
+
chain reads is a ``ValueError``, and a backend is handed only the options it
|
|
1081
|
+
reads.
|
|
1082
|
+
"""
|
|
1083
|
+
premises = list(premises)
|
|
1084
|
+
if logic == "auto":
|
|
1085
|
+
logic = _detect_logic(formula, premises)
|
|
1086
|
+
chain = tuple(backends) if backends is not None else (
|
|
1087
|
+
("modelfinder", "z3") if logic == "fol"
|
|
1088
|
+
else ("modal-tableau", "kripke-enum"))
|
|
1089
|
+
|
|
1090
|
+
asked = premises
|
|
1091
|
+
if signature is not None:
|
|
1092
|
+
asked = premises + list(_signature_premises("countermodel", signature, logic))
|
|
1093
|
+
options = _name_background(options, len(premises), len(asked), "countermodel")
|
|
1094
|
+
plan = plan_options("countermodel", chain, logic, options)
|
|
1095
|
+
depth = _nesting_depth(formula, *asked)
|
|
1096
|
+
|
|
1097
|
+
def search() -> CountermodelResult:
|
|
1098
|
+
unread = None
|
|
1099
|
+
for name in chain:
|
|
1100
|
+
passed, refusal = plan[name]
|
|
1101
|
+
if refusal is not None:
|
|
1102
|
+
continue # it would answer a different question
|
|
1103
|
+
verdict = _name_nesting(
|
|
1104
|
+
run_backend(name, formula, asked, timeout=timeout, **passed), depth)
|
|
1105
|
+
if verdict.status == REFUTED and verdict.countermodel is not None:
|
|
1106
|
+
return CountermodelResult(
|
|
1107
|
+
found=True, model=verdict.countermodel, backend=verdict.backend,
|
|
1108
|
+
explanation_nl=_explain_witness(verdict.countermodel,
|
|
1109
|
+
formula, premises))
|
|
1110
|
+
if unread is None and "levels deep" in (verdict.detail or ""):
|
|
1111
|
+
unread = f"{verdict.backend}: {verdict.detail}"
|
|
1112
|
+
return CountermodelResult(found=False, reason=unread)
|
|
1113
|
+
|
|
1114
|
+
return _call_deep(depth, search)
|
|
1115
|
+
|
|
1116
|
+
|
|
1117
|
+
# ---------------------------------------------------------------------------
|
|
1118
|
+
# repair — the diagnose→suggest→fix loop (the caller's LLM supplies the fix)
|
|
1119
|
+
# ---------------------------------------------------------------------------
|
|
1120
|
+
|
|
1121
|
+
@dataclass(frozen=True)
|
|
1122
|
+
class RepairStep:
|
|
1123
|
+
"""One round of the repair loop.
|
|
1124
|
+
|
|
1125
|
+
``diagnostics`` carries the machine-readable evidence (parse errors or a
|
|
1126
|
+
``CheckResult`` dict), ``suggestion`` a one-line human/LLM-readable
|
|
1127
|
+
instruction, ``converged`` whether this text finally passed.
|
|
1128
|
+
"""
|
|
1129
|
+
|
|
1130
|
+
attempt: int
|
|
1131
|
+
text: str
|
|
1132
|
+
ok: bool
|
|
1133
|
+
diagnostics: dict
|
|
1134
|
+
suggestion: Optional[str]
|
|
1135
|
+
converged: bool
|
|
1136
|
+
|
|
1137
|
+
def to_dict(self) -> dict:
|
|
1138
|
+
return {"attempt": self.attempt, "text": self.text, "ok": self.ok,
|
|
1139
|
+
"diagnostics": self.diagnostics, "suggestion": self.suggestion,
|
|
1140
|
+
"converged": self.converged}
|
|
1141
|
+
|
|
1142
|
+
|
|
1143
|
+
_POSITION_RE = re.compile(r"at position (\d+)")
|
|
1144
|
+
|
|
1145
|
+
|
|
1146
|
+
def _message_progress(message: str) -> float:
|
|
1147
|
+
"""How far into the input the dialect that produced ``message`` got.
|
|
1148
|
+
|
|
1149
|
+
A message reporting that the input "ended unexpectedly" consumed all of
|
|
1150
|
+
it — the farthest any attempt can get; everything else is located by the
|
|
1151
|
+
position the parser names. Used to pick the most informative of several
|
|
1152
|
+
competing diagnoses (:func:`_suggest`, and the MCP server's spec-topic
|
|
1153
|
+
routing, which imports this rather than reimplementing it).
|
|
1154
|
+
"""
|
|
1155
|
+
if "ended unexpectedly" in message.lower():
|
|
1156
|
+
return float("inf")
|
|
1157
|
+
return max((int(p) for p in _POSITION_RE.findall(message)), default=0)
|
|
1158
|
+
|
|
1159
|
+
|
|
1160
|
+
def _suggest(parse_result: Optional[ParseResult],
|
|
1161
|
+
check_result: Optional[CheckResult]) -> Optional[str]:
|
|
1162
|
+
"""One actionable sentence out of the strongest diagnostic available."""
|
|
1163
|
+
if parse_result is not None and not parse_result.ok:
|
|
1164
|
+
if parse_result.errors:
|
|
1165
|
+
# The FARTHEST failure, not the last one listed. Errors arrive one
|
|
1166
|
+
# per candidate dialect in detection order, and the dialects at
|
|
1167
|
+
# the end of that order are the specialised ones that give up
|
|
1168
|
+
# earliest on ordinary input: for 'A ∧ B ∨ C' the last entry is
|
|
1169
|
+
# lambek's "Invalid predicate 'A'" at position 3, while seven
|
|
1170
|
+
# other dialects read to the ∨ and name the real cause (mixed
|
|
1171
|
+
# connectives). Handing back the last one sends the reader off
|
|
1172
|
+
# renaming a predicate that is already well formed.
|
|
1173
|
+
best = max(parse_result.errors,
|
|
1174
|
+
key=lambda e: _message_progress(e.get("message", "")))
|
|
1175
|
+
return f"Fix the syntax: {best['message']}"
|
|
1176
|
+
return "Fix the syntax (no parser accepted the text)."
|
|
1177
|
+
if check_result is None or check_result.ok:
|
|
1178
|
+
return None
|
|
1179
|
+
if not check_result.is_closed:
|
|
1180
|
+
free = ", ".join(check_result.free_variables)
|
|
1181
|
+
return (f"Bind or replace the free variable(s) {free}: quantify them "
|
|
1182
|
+
f"(∀/∃) or use constant names (multi-letter lowercase, e.g. "
|
|
1183
|
+
f"'alice' — single lowercase letters are variables).")
|
|
1184
|
+
if not check_result.arity_consistent:
|
|
1185
|
+
c = check_result.arity_conflicts[0]
|
|
1186
|
+
arities = "/".join(str(a) for a in c["arities"])
|
|
1187
|
+
return (f"Use {c['symbol']} with ONE arity — it appears with "
|
|
1188
|
+
f"arities {arities}.")
|
|
1189
|
+
if check_result.has_lambdas:
|
|
1190
|
+
return "Eliminate the lambda residue (beta-reduce before finalising)."
|
|
1191
|
+
if check_result.signature_errors:
|
|
1192
|
+
e = check_result.signature_errors[0]
|
|
1193
|
+
base = f"{e['kind'].replace('_', ' ')}: {e['symbol']}"
|
|
1194
|
+
if e.get("suggestion"):
|
|
1195
|
+
return f"{base} — did you mean {e['suggestion']!r}?"
|
|
1196
|
+
return f"{base} is not in the signature."
|
|
1197
|
+
return None
|
|
1198
|
+
|
|
1199
|
+
|
|
1200
|
+
def repair(text: str, *, dialect: Optional[str] = None,
|
|
1201
|
+
signature: Optional[dict] = None,
|
|
1202
|
+
fixer: Optional[Callable[[str, dict], str]] = None,
|
|
1203
|
+
max_attempts: int = 5) -> Iterator[RepairStep]:
|
|
1204
|
+
"""Generator driving a diagnose→suggest→fix loop over raw formula text.
|
|
1205
|
+
|
|
1206
|
+
Each round parses (``parse_any``), checks (``check``), and yields a
|
|
1207
|
+
:class:`RepairStep` with machine-readable diagnostics and a one-line
|
|
1208
|
+
suggestion. The kit deliberately does NOT rewrite the text itself — the
|
|
1209
|
+
``fixer`` callback (typically the caller's LLM, prompted with
|
|
1210
|
+
``step.diagnostics`` / ``step.suggestion``) returns the next candidate
|
|
1211
|
+
text; without a fixer the generator yields the diagnosis for the input
|
|
1212
|
+
and stops. The loop ends on convergence (``converged=True``), on fixer
|
|
1213
|
+
absence, or after ``max_attempts`` rounds.
|
|
1214
|
+
"""
|
|
1215
|
+
if max_attempts < 1:
|
|
1216
|
+
raise ValueError("repair: max_attempts must be >= 1")
|
|
1217
|
+
_read_signature(signature) # a malformed signature is refused before any round, also for text that does not parse
|
|
1218
|
+
current = text
|
|
1219
|
+
for attempt in range(1, max_attempts + 1):
|
|
1220
|
+
parsed = parse_any(current, hint=dialect)
|
|
1221
|
+
diagnostics: dict
|
|
1222
|
+
if parsed.ok and parsed.formula is not None: # an ok result always carries its formula
|
|
1223
|
+
checked = check(parsed.formula, signature=signature)
|
|
1224
|
+
ok = checked.ok
|
|
1225
|
+
diagnostics = {"parse": None, "check": checked.to_dict()}
|
|
1226
|
+
suggestion = _suggest(None, checked)
|
|
1227
|
+
else:
|
|
1228
|
+
ok = False
|
|
1229
|
+
checked = None
|
|
1230
|
+
diagnostics = {"parse": list(parsed.errors), "check": None}
|
|
1231
|
+
suggestion = _suggest(parsed, None)
|
|
1232
|
+
yield RepairStep(attempt=attempt, text=current, ok=ok,
|
|
1233
|
+
diagnostics=diagnostics, suggestion=suggestion,
|
|
1234
|
+
converged=ok)
|
|
1235
|
+
if ok or fixer is None:
|
|
1236
|
+
return
|
|
1237
|
+
current = fixer(current, diagnostics)
|
|
1238
|
+
if not isinstance(current, str):
|
|
1239
|
+
raise TypeError("repair: fixer must return the next candidate text (str)")
|
|
1240
|
+
|
|
1241
|
+
|
|
1242
|
+
# ---------------------------------------------------------------------------
|
|
1243
|
+
# translate — the comorphism registry
|
|
1244
|
+
# ---------------------------------------------------------------------------
|
|
1245
|
+
|
|
1246
|
+
def translate(term, from_logic: str, to_logic: str,
|
|
1247
|
+
**options) -> TranslationResult:
|
|
1248
|
+
"""Translate ``term`` between logics via the comorphism registry.
|
|
1249
|
+
|
|
1250
|
+
Composes registered edges by BFS when there is no direct one (e.g.
|
|
1251
|
+
``alc → modal → fol`` if the direct ``alc → fol`` edge were absent). See
|
|
1252
|
+
:mod:`unicode_logic_kit.comorphism` for the default edges, their term types
|
|
1253
|
+
and conventions (free anchors, fragment limits), and how to register your
|
|
1254
|
+
own.
|
|
1255
|
+
|
|
1256
|
+
``options`` are forwarded to the edges on the path that declare them
|
|
1257
|
+
(``frame=``/``systems=``/``temporal_closure=`` for the modal edge,
|
|
1258
|
+
``signature=`` for the sorted one, ``mode=``/``bridges=`` for the
|
|
1259
|
+
quantified-modal one); one that no edge on the path accepts raises
|
|
1260
|
+
``ValueError`` naming what is accepted, so an option can never be
|
|
1261
|
+
ignored into a different question.
|
|
1262
|
+
|
|
1263
|
+
The result's ``axioms`` are the translation's SIDE CONDITIONS, already
|
|
1264
|
+
in the target logic, and belong in the premises::
|
|
1265
|
+
|
|
1266
|
+
t = api.translate(f, "msfol", "fol")
|
|
1267
|
+
api.prove(t.result, [*premises, *t.axioms])
|
|
1268
|
+
|
|
1269
|
+
or carry the pair around as a :class:`unicode_logic_kit.logic.Sentence`,
|
|
1270
|
+
which :func:`prove` unwraps together with its axioms. Dropping them is
|
|
1271
|
+
how a valid formula comes back REFUTED.
|
|
1272
|
+
|
|
1273
|
+
A formula nested a hundred levels deep or more is translated on a worker
|
|
1274
|
+
thread sized for it (see :func:`prove`). One that is still too deep for a
|
|
1275
|
+
recursive translation is a ``ValueError`` that names the nesting depth.
|
|
1276
|
+
"""
|
|
1277
|
+
depth = _nesting_depth(term)
|
|
1278
|
+
try:
|
|
1279
|
+
return _call_deep(
|
|
1280
|
+
depth, lambda: DEFAULT_REGISTRY.translate(term, from_logic, to_logic, **options))
|
|
1281
|
+
except RecursionError:
|
|
1282
|
+
if depth < _SHALLOW_DEPTH:
|
|
1283
|
+
raise
|
|
1284
|
+
raise ValueError(f"translate: {_deep_detail(depth, 'the translation')}") from None
|