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,2196 @@
|
|
|
1
|
+
"""MSFL node classes (sorted quantifiers/constants, Łukasiewicz operators) and to_fol reduction."""
|
|
2
|
+
|
|
3
|
+
import contextvars
|
|
4
|
+
import logging
|
|
5
|
+
from dataclasses import dataclass, is_dataclass, replace
|
|
6
|
+
from typing import List, Optional, Tuple, TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from ._fol_nodes import (
|
|
9
|
+
Node, Z3Env, Variable, Constant, Number, Function,
|
|
10
|
+
Atom, Not, And, Or, Xor, Implies, Iff, Quantifier,
|
|
11
|
+
Count, Cardinality, Contrast, _COUNT_OPS, _COUNT_TOKEN_TO_OP, _count_bound,
|
|
12
|
+
NODE_CLASSES, OPERATORS, register_operator,
|
|
13
|
+
register_parser_op, _fold_binary, _number_text, _prover9_outermost,
|
|
14
|
+
)
|
|
15
|
+
from ._identifiers import (
|
|
16
|
+
constant_text, fresh_like, fresh_variable_like, fresh_variables, symbol_names,
|
|
17
|
+
variable_names,
|
|
18
|
+
)
|
|
19
|
+
from ._team_nodes import SlashedExists
|
|
20
|
+
# PredicateTerm is a term-level leaf like Variable/Constant, so the shared
|
|
21
|
+
# renderers and the lambda-scope resolver here have to know it by name.
|
|
22
|
+
# Safe to import at module scope: _ho_nodes reaches only _fol_nodes and
|
|
23
|
+
# _so_nodes at import time, and takes its own Lambda dependency lazily.
|
|
24
|
+
from ._ho_nodes import PredicateTerm
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
# Type-hint only: a real (runtime) import here would be circular
|
|
28
|
+
# (signature.py imports from .nodes, which re-exports the classes this
|
|
29
|
+
# module defines) — see subsort_axioms's own docstring for why a Signature
|
|
30
|
+
# is instead duck-typed at runtime (only its .subsorts mapping is read).
|
|
31
|
+
from .signature import Signature
|
|
32
|
+
|
|
33
|
+
_logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
# Why the Łukasiewicz nodes REFUSE the classical exports below: their to_msfol
|
|
36
|
+
# collapse (min/max/t-norm -> And/Or/...) changes the LOGIC, not just the syntax
|
|
37
|
+
# — fuzzy weak-disjunction excluded middle P ∨ ¬P is not Łukasiewicz-valid but
|
|
38
|
+
# its classical image is, so a silent collapse inside is_valid /
|
|
39
|
+
# is_valid_resolution returned wrong verdicts. The collapse stays available, but
|
|
40
|
+
# only explicitly: to_fol(node) first.
|
|
41
|
+
_LUK_NO_CLASSICAL_EXPORT = (
|
|
42
|
+
"Łukasiewicz connectives are not classical — a silent classical collapse "
|
|
43
|
+
"would decide the WRONG logic (fuzzy P ∨ ¬P is not Łukasiewicz-valid, its "
|
|
44
|
+
"classical image is). Evaluate with semantics.fuzzy.evaluate or decide with "
|
|
45
|
+
"atp.z3_fuzzy.fuzzy_is_valid / fuzzy_is_satisfiable; if the classical "
|
|
46
|
+
"skeleton is really wanted, collapse explicitly with to_fol(node) first."
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# =========================
|
|
51
|
+
# MSFL Nodes
|
|
52
|
+
# =========================
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True)
|
|
55
|
+
class SortedQuantifier(Node):
|
|
56
|
+
"""A sort-restricted quantifier binding a variable to a named sort.
|
|
57
|
+
|
|
58
|
+
type is "∀" (universal) or "∃" (existential).
|
|
59
|
+
Semantics: ∀x:S φ asserts φ holds for all elements of sort S;
|
|
60
|
+
∃x:S φ asserts φ holds for some element of sort S.
|
|
61
|
+
Reduction to plain FOL is a later step.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
type: str
|
|
65
|
+
variable: Variable
|
|
66
|
+
sort: str
|
|
67
|
+
formula: Node
|
|
68
|
+
|
|
69
|
+
def _tree_parts(self):
|
|
70
|
+
return f"{self.type} {self.variable.name}:{self.sort}", [self.formula]
|
|
71
|
+
|
|
72
|
+
def to_dict(self):
|
|
73
|
+
return {
|
|
74
|
+
"_type": "SortedQuantifier",
|
|
75
|
+
"type": self.type,
|
|
76
|
+
"variable": self.variable.to_dict(),
|
|
77
|
+
"sort": self.sort,
|
|
78
|
+
"formula": self.formula.to_dict(),
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@staticmethod
|
|
82
|
+
def from_dict(d):
|
|
83
|
+
return SortedQuantifier(
|
|
84
|
+
d["type"],
|
|
85
|
+
Node.from_dict(d["variable"]),
|
|
86
|
+
d["sort"],
|
|
87
|
+
Node.from_dict(d["formula"]),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def to_msfol(self) -> "Node":
|
|
91
|
+
return SortedQuantifier(self.type, self.variable, self.sort, self.formula.to_msfol())
|
|
92
|
+
|
|
93
|
+
def _relativize(self, facts: list) -> "Node":
|
|
94
|
+
body = self.formula._relativize(facts)
|
|
95
|
+
sort_atom = Atom(self.sort, (self.variable,))
|
|
96
|
+
if self.type == "∀":
|
|
97
|
+
return Quantifier("∀", self.variable, Implies(sort_atom, body))
|
|
98
|
+
elif self.type == "∃":
|
|
99
|
+
return Quantifier("∃", self.variable, And(sort_atom, body))
|
|
100
|
+
raise ValueError(f"Unknown quantifier type: {self.type}")
|
|
101
|
+
|
|
102
|
+
def to_z3(self, env: Optional[Z3Env] = None):
|
|
103
|
+
_logger.info("Auto-reducing %s to FOL for Z3 export.", type(self).__name__)
|
|
104
|
+
return to_fol(self).to_z3(env)
|
|
105
|
+
|
|
106
|
+
@_prover9_outermost
|
|
107
|
+
def to_prover9(self) -> str:
|
|
108
|
+
_logger.info("Auto-reducing %s to FOL for Prover9 export.", type(self).__name__)
|
|
109
|
+
return to_fol(self).to_prover9()
|
|
110
|
+
|
|
111
|
+
def to_tptp(self) -> str:
|
|
112
|
+
_logger.info("Auto-reducing %s to FOL for TPTP export.", type(self).__name__)
|
|
113
|
+
return to_fol(self).to_tptp()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass(frozen=True)
|
|
117
|
+
class SortedConstant(Node):
|
|
118
|
+
"""A constant symbol annotated with a sort name, e.g. alice:Human.
|
|
119
|
+
|
|
120
|
+
Semantics: a ground term that belongs to the named sort.
|
|
121
|
+
Reduction to plain FOL is a later step.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
name: str
|
|
125
|
+
sort: str
|
|
126
|
+
|
|
127
|
+
def _tree_parts(self):
|
|
128
|
+
return f"{self.name}:{self.sort}", []
|
|
129
|
+
|
|
130
|
+
def to_dict(self):
|
|
131
|
+
return {"_type": "SortedConstant", "name": self.name, "sort": self.sort}
|
|
132
|
+
|
|
133
|
+
@staticmethod
|
|
134
|
+
def from_dict(d):
|
|
135
|
+
return SortedConstant(d["name"], d["sort"])
|
|
136
|
+
|
|
137
|
+
def to_msfol(self) -> "Node":
|
|
138
|
+
return SortedConstant(self.name, self.sort)
|
|
139
|
+
|
|
140
|
+
def _relativize(self, facts: list) -> "Node":
|
|
141
|
+
facts.append(Atom(self.sort, (Constant(self.name),)))
|
|
142
|
+
return Constant(self.name)
|
|
143
|
+
|
|
144
|
+
def to_z3(self, env: Optional[Z3Env] = None):
|
|
145
|
+
_logger.info("Auto-reducing %s to FOL for Z3 export.", type(self).__name__)
|
|
146
|
+
return to_fol(self).to_z3(env)
|
|
147
|
+
|
|
148
|
+
def to_prover9(self) -> str:
|
|
149
|
+
_logger.info("Auto-reducing %s to FOL for Prover9 export.", type(self).__name__)
|
|
150
|
+
return to_fol(self).to_prover9()
|
|
151
|
+
|
|
152
|
+
def to_tptp(self) -> str:
|
|
153
|
+
_logger.info("Auto-reducing %s to FOL for TPTP export.", type(self).__name__)
|
|
154
|
+
return to_fol(self).to_tptp()
|
|
155
|
+
|
|
156
|
+
def _tptp_symbol(self):
|
|
157
|
+
"""The constant word this node writes: it renders as the plain ``Constant`` of the same name."""
|
|
158
|
+
return Constant(self.name)._tptp_symbol()
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@dataclass(frozen=True)
|
|
162
|
+
class SortedCount(Node):
|
|
163
|
+
"""A sort-restricted counting quantifier ∃≥n / ∃≤n / ∃=n x:S over one sorted variable.
|
|
164
|
+
|
|
165
|
+
The many-sorted counterpart of :class:`Count`: ``∃≥n x:S φ`` is true iff at least
|
|
166
|
+
``n`` DISTINCT elements OF SORT ``S`` satisfy ``φ`` (``∃≤n`` at most, ``∃=n`` exactly).
|
|
167
|
+
``op`` is ``"ge"`` / ``"le"`` / ``"eq"``; ``n`` is a non-negative-integer :class:`Number`
|
|
168
|
+
kept SYMBOLIC (never expanded); ``variable`` is the bound counting variable; ``sort`` the
|
|
169
|
+
sort name it ranges over; ``formula`` its matrix. Reduction to plain FOL relativises the
|
|
170
|
+
matrix with the sort guard and reuses the distinct-witnesses encoding of :class:`Count`
|
|
171
|
+
(see :meth:`_relativize`); :meth:`to_z3` / :meth:`to_prover9` / :meth:`to_tptp` auto-reduce.
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
op: str
|
|
175
|
+
n: Number
|
|
176
|
+
variable: Variable
|
|
177
|
+
sort: str
|
|
178
|
+
formula: Node
|
|
179
|
+
|
|
180
|
+
def __post_init__(self):
|
|
181
|
+
"""Validate the op code and that n is a non-negative integer Number."""
|
|
182
|
+
if self.op not in _COUNT_OPS:
|
|
183
|
+
raise ValueError(
|
|
184
|
+
f"SortedCount: unknown op {self.op!r}; expected one of 'ge', 'le', 'eq'.")
|
|
185
|
+
if not (isinstance(self.n, Number) and isinstance(self.n.value, int)
|
|
186
|
+
and self.n.value >= 0):
|
|
187
|
+
raise ValueError(
|
|
188
|
+
"SortedCount: n must be a Number wrapping a non-negative integer.")
|
|
189
|
+
|
|
190
|
+
def _tree_parts(self):
|
|
191
|
+
"""Return the ∃≥n x:S label (with the sorted bound variable) and the matrix."""
|
|
192
|
+
return (f"{_COUNT_OPS[self.op]}{self.n.value} {self.variable.name}:{self.sort}",
|
|
193
|
+
[self.formula])
|
|
194
|
+
|
|
195
|
+
def to_dict(self):
|
|
196
|
+
"""Serialise to dict with op, n, sorted bound variable, and serialised matrix."""
|
|
197
|
+
return {"_type": "SortedCount", "op": self.op, "n": self.n.to_dict(),
|
|
198
|
+
"variable": self.variable.to_dict(), "sort": self.sort,
|
|
199
|
+
"formula": self.formula.to_dict()}
|
|
200
|
+
|
|
201
|
+
@staticmethod
|
|
202
|
+
def from_dict(d):
|
|
203
|
+
"""Deserialise a SortedCount from a dict produced by to_dict."""
|
|
204
|
+
return SortedCount(d["op"], Node.from_dict(d["n"]),
|
|
205
|
+
Node.from_dict(d["variable"]), d["sort"],
|
|
206
|
+
Node.from_dict(d["formula"]))
|
|
207
|
+
|
|
208
|
+
def to_msfol(self) -> "Node":
|
|
209
|
+
"""Reduce the matrix to classical FOL; keep the sorted counting binder."""
|
|
210
|
+
return SortedCount(self.op, self.n, self.variable, self.sort,
|
|
211
|
+
self.formula.to_msfol())
|
|
212
|
+
|
|
213
|
+
def _relativize(self, facts: list) -> "Node":
|
|
214
|
+
"""Guard the matrix with the sort predicate, then delegate to unsorted Count.
|
|
215
|
+
|
|
216
|
+
``∃≥n x:S φ`` ≡ '≥ n distinct x with (S(x) ∧ φ)'. Guarding the matrix and reusing
|
|
217
|
+
:class:`Count`'s encoding is correct for EVERY op, because Count's ≤/= readings are
|
|
218
|
+
themselves defined via 'at least' over the (now sort-guarded) matrix.
|
|
219
|
+
"""
|
|
220
|
+
guarded = And(Atom(self.sort, (self.variable,)), self.formula._relativize(facts))
|
|
221
|
+
return Count(self.op, self.n, self.variable, guarded)
|
|
222
|
+
|
|
223
|
+
def to_z3(self, env: Optional[Z3Env] = None):
|
|
224
|
+
"""Auto-reduce to FOL (sort-guarded Count), then translate to Z3."""
|
|
225
|
+
_logger.info("Auto-reducing %s to FOL for Z3 export.", type(self).__name__)
|
|
226
|
+
return to_fol(self).to_z3(env)
|
|
227
|
+
|
|
228
|
+
@_prover9_outermost
|
|
229
|
+
def to_prover9(self) -> str:
|
|
230
|
+
"""Auto-reduce to FOL (sort-guarded Count), then render Prover9 syntax."""
|
|
231
|
+
_logger.info("Auto-reducing %s to FOL for Prover9 export.", type(self).__name__)
|
|
232
|
+
return to_fol(self).to_prover9()
|
|
233
|
+
|
|
234
|
+
def to_tptp(self) -> str:
|
|
235
|
+
"""Auto-reduce to FOL (sort-guarded Count), then render TPTP syntax."""
|
|
236
|
+
_logger.info("Auto-reducing %s to FOL for TPTP export.", type(self).__name__)
|
|
237
|
+
return to_fol(self).to_tptp()
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
# Shared rejection message: a sorted set-cardinality term is not first-order definable.
|
|
241
|
+
_NO_SORTED_CARDINALITY_EXPORT = (
|
|
242
|
+
"SortedCardinality terms (|{v:S : φ}|) denote set cardinality, a second-order notion "
|
|
243
|
+
"with no first-order counterpart. Keep the term at the AST level, or express a "
|
|
244
|
+
"fixed-bound sorted count with the SortedCount quantifier (∃≥n / ∃≤n / ∃=n x:S)."
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
@dataclass(frozen=True)
|
|
249
|
+
class SortedCardinality(Node):
|
|
250
|
+
"""A sort-restricted set-cardinality term ``|{v:S : φ}|``: how many
|
|
251
|
+
elements of sort ``S`` satisfy ``φ``.
|
|
252
|
+
|
|
253
|
+
The many-sorted counterpart of :class:`Cardinality`. It BINDS ``variable`` (of sort
|
|
254
|
+
``sort``) over the matrix ``formula``. Set cardinality is genuinely second-order, so it
|
|
255
|
+
has no first-order export: :meth:`to_z3` / :meth:`to_prover9` / :meth:`to_tptp` reject.
|
|
256
|
+
"""
|
|
257
|
+
|
|
258
|
+
variable: Variable
|
|
259
|
+
sort: str
|
|
260
|
+
formula: Node
|
|
261
|
+
|
|
262
|
+
def _tree_parts(self):
|
|
263
|
+
"""Return the |v:S| cardinality label (with the sorted bound variable) and the matrix."""
|
|
264
|
+
return f"|{self.variable.name}:{self.sort}|", [self.formula]
|
|
265
|
+
|
|
266
|
+
def to_dict(self):
|
|
267
|
+
"""Serialise to dict with the sorted bound variable and serialised matrix."""
|
|
268
|
+
return {"_type": "SortedCardinality", "variable": self.variable.to_dict(),
|
|
269
|
+
"sort": self.sort, "formula": self.formula.to_dict()}
|
|
270
|
+
|
|
271
|
+
@staticmethod
|
|
272
|
+
def from_dict(d):
|
|
273
|
+
"""Deserialise a SortedCardinality from a dict produced by to_dict."""
|
|
274
|
+
return SortedCardinality(Node.from_dict(d["variable"]), d["sort"],
|
|
275
|
+
Node.from_dict(d["formula"]))
|
|
276
|
+
|
|
277
|
+
def to_msfol(self) -> "Node":
|
|
278
|
+
"""Reduce the matrix to classical FOL; keep the sorted cardinality binder."""
|
|
279
|
+
return SortedCardinality(self.variable, self.sort, self.formula.to_msfol())
|
|
280
|
+
|
|
281
|
+
def _relativize(self, facts: list) -> "Node":
|
|
282
|
+
"""Guard the matrix with the sort predicate and fall back to unsorted Cardinality.
|
|
283
|
+
|
|
284
|
+
``|{v:S : φ}|`` = ``|{v : S(v) ∧ φ}|``. The result is still export-free (Cardinality
|
|
285
|
+
rejects), but the reduction keeps the term semantically faithful under to_fol.
|
|
286
|
+
"""
|
|
287
|
+
guarded = And(Atom(self.sort, (self.variable,)), self.formula._relativize(facts))
|
|
288
|
+
return Cardinality(self.variable, guarded)
|
|
289
|
+
|
|
290
|
+
def to_z3(self, env: Optional[Z3Env] = None):
|
|
291
|
+
"""Reject Z3 export: sorted set cardinality has no first-order counterpart."""
|
|
292
|
+
raise NotImplementedError(_NO_SORTED_CARDINALITY_EXPORT)
|
|
293
|
+
|
|
294
|
+
def to_prover9(self) -> str:
|
|
295
|
+
"""Reject Prover9 export: sorted set cardinality has no first-order counterpart."""
|
|
296
|
+
raise NotImplementedError(_NO_SORTED_CARDINALITY_EXPORT)
|
|
297
|
+
|
|
298
|
+
def to_tptp(self) -> str:
|
|
299
|
+
"""Reject TPTP export: sorted set cardinality has no first-order counterpart."""
|
|
300
|
+
raise NotImplementedError(_NO_SORTED_CARDINALITY_EXPORT)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
@dataclass(frozen=True)
|
|
304
|
+
class WeakConjunction(Node):
|
|
305
|
+
"""Łukasiewicz weak conjunction (fuzzy min): min{x, y}.
|
|
306
|
+
|
|
307
|
+
Uses the same glyph ∧ as classical And; distinct by class identity.
|
|
308
|
+
"""
|
|
309
|
+
|
|
310
|
+
left: Node
|
|
311
|
+
right: Node
|
|
312
|
+
|
|
313
|
+
def _tree_parts(self):
|
|
314
|
+
return "∧", [self.left, self.right]
|
|
315
|
+
|
|
316
|
+
def to_dict(self):
|
|
317
|
+
return {"_type": "WeakConjunction", "left": self.left.to_dict(), "right": self.right.to_dict()}
|
|
318
|
+
|
|
319
|
+
@staticmethod
|
|
320
|
+
def from_dict(d):
|
|
321
|
+
return WeakConjunction(Node.from_dict(d["left"]), Node.from_dict(d["right"]))
|
|
322
|
+
|
|
323
|
+
def to_msfol(self) -> "Node":
|
|
324
|
+
return And(self.left.to_msfol(), self.right.to_msfol())
|
|
325
|
+
|
|
326
|
+
def _relativize(self, facts: list) -> "Node":
|
|
327
|
+
raise RuntimeError("WeakConjunction._relativize called; call to_msfol() before _relativize.")
|
|
328
|
+
|
|
329
|
+
def to_z3(self, env: Optional[Z3Env] = None):
|
|
330
|
+
"""Reject Z3 export: the classical collapse must be explicit (to_fol first)."""
|
|
331
|
+
raise NotImplementedError(f"to_z3: {type(self).__name__} — " + _LUK_NO_CLASSICAL_EXPORT)
|
|
332
|
+
|
|
333
|
+
def to_prover9(self) -> str:
|
|
334
|
+
"""Reject Prover9 export: the classical collapse must be explicit (to_fol first)."""
|
|
335
|
+
raise NotImplementedError(f"to_prover9: {type(self).__name__} — " + _LUK_NO_CLASSICAL_EXPORT)
|
|
336
|
+
|
|
337
|
+
def to_tptp(self) -> str:
|
|
338
|
+
"""Reject TPTP export: the classical collapse must be explicit (to_fol first)."""
|
|
339
|
+
raise NotImplementedError(f"to_tptp: {type(self).__name__} — " + _LUK_NO_CLASSICAL_EXPORT)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
@dataclass(frozen=True)
|
|
343
|
+
class WeakDisjunction(Node):
|
|
344
|
+
"""Łukasiewicz weak disjunction (fuzzy max): max{x, y}.
|
|
345
|
+
|
|
346
|
+
Uses the same glyph ∨ as classical Or; distinct by class identity.
|
|
347
|
+
"""
|
|
348
|
+
|
|
349
|
+
left: Node
|
|
350
|
+
right: Node
|
|
351
|
+
|
|
352
|
+
def _tree_parts(self):
|
|
353
|
+
return "∨", [self.left, self.right]
|
|
354
|
+
|
|
355
|
+
def to_dict(self):
|
|
356
|
+
return {"_type": "WeakDisjunction", "left": self.left.to_dict(), "right": self.right.to_dict()}
|
|
357
|
+
|
|
358
|
+
@staticmethod
|
|
359
|
+
def from_dict(d):
|
|
360
|
+
return WeakDisjunction(Node.from_dict(d["left"]), Node.from_dict(d["right"]))
|
|
361
|
+
|
|
362
|
+
def to_msfol(self) -> "Node":
|
|
363
|
+
return Or(self.left.to_msfol(), self.right.to_msfol())
|
|
364
|
+
|
|
365
|
+
def _relativize(self, facts: list) -> "Node":
|
|
366
|
+
raise RuntimeError("WeakDisjunction._relativize called; call to_msfol() before _relativize.")
|
|
367
|
+
|
|
368
|
+
def to_z3(self, env: Optional[Z3Env] = None):
|
|
369
|
+
"""Reject Z3 export: the classical collapse must be explicit (to_fol first)."""
|
|
370
|
+
raise NotImplementedError(f"to_z3: {type(self).__name__} — " + _LUK_NO_CLASSICAL_EXPORT)
|
|
371
|
+
|
|
372
|
+
def to_prover9(self) -> str:
|
|
373
|
+
"""Reject Prover9 export: the classical collapse must be explicit (to_fol first)."""
|
|
374
|
+
raise NotImplementedError(f"to_prover9: {type(self).__name__} — " + _LUK_NO_CLASSICAL_EXPORT)
|
|
375
|
+
|
|
376
|
+
def to_tptp(self) -> str:
|
|
377
|
+
"""Reject TPTP export: the classical collapse must be explicit (to_fol first)."""
|
|
378
|
+
raise NotImplementedError(f"to_tptp: {type(self).__name__} — " + _LUK_NO_CLASSICAL_EXPORT)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
@dataclass(frozen=True)
|
|
382
|
+
class StrongConjunction(Node):
|
|
383
|
+
"""Łukasiewicz strong conjunction (t-norm): max{0, x+y−1}."""
|
|
384
|
+
|
|
385
|
+
left: Node
|
|
386
|
+
right: Node
|
|
387
|
+
|
|
388
|
+
def _tree_parts(self):
|
|
389
|
+
return "⊗", [self.left, self.right]
|
|
390
|
+
|
|
391
|
+
def to_dict(self):
|
|
392
|
+
return {"_type": "StrongConjunction", "left": self.left.to_dict(), "right": self.right.to_dict()}
|
|
393
|
+
|
|
394
|
+
@staticmethod
|
|
395
|
+
def from_dict(d):
|
|
396
|
+
return StrongConjunction(Node.from_dict(d["left"]), Node.from_dict(d["right"]))
|
|
397
|
+
|
|
398
|
+
def to_msfol(self) -> "Node":
|
|
399
|
+
return And(self.left.to_msfol(), self.right.to_msfol())
|
|
400
|
+
|
|
401
|
+
def _relativize(self, facts: list) -> "Node":
|
|
402
|
+
raise RuntimeError("StrongConjunction._relativize called; call to_msfol() before _relativize.")
|
|
403
|
+
|
|
404
|
+
def to_z3(self, env: Optional[Z3Env] = None):
|
|
405
|
+
"""Reject Z3 export: the classical collapse must be explicit (to_fol first)."""
|
|
406
|
+
raise NotImplementedError(f"to_z3: {type(self).__name__} — " + _LUK_NO_CLASSICAL_EXPORT)
|
|
407
|
+
|
|
408
|
+
def to_prover9(self) -> str:
|
|
409
|
+
"""Reject Prover9 export: the classical collapse must be explicit (to_fol first)."""
|
|
410
|
+
raise NotImplementedError(f"to_prover9: {type(self).__name__} — " + _LUK_NO_CLASSICAL_EXPORT)
|
|
411
|
+
|
|
412
|
+
def to_tptp(self) -> str:
|
|
413
|
+
"""Reject TPTP export: the classical collapse must be explicit (to_fol first)."""
|
|
414
|
+
raise NotImplementedError(f"to_tptp: {type(self).__name__} — " + _LUK_NO_CLASSICAL_EXPORT)
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
@dataclass(frozen=True)
|
|
418
|
+
class StrongDisjunction(Node):
|
|
419
|
+
"""Łukasiewicz strong disjunction (t-conorm): min{1, x+y}."""
|
|
420
|
+
|
|
421
|
+
left: Node
|
|
422
|
+
right: Node
|
|
423
|
+
|
|
424
|
+
def _tree_parts(self):
|
|
425
|
+
return "⊕", [self.left, self.right]
|
|
426
|
+
|
|
427
|
+
def to_dict(self):
|
|
428
|
+
return {"_type": "StrongDisjunction", "left": self.left.to_dict(), "right": self.right.to_dict()}
|
|
429
|
+
|
|
430
|
+
@staticmethod
|
|
431
|
+
def from_dict(d):
|
|
432
|
+
return StrongDisjunction(Node.from_dict(d["left"]), Node.from_dict(d["right"]))
|
|
433
|
+
|
|
434
|
+
def to_msfol(self) -> "Node":
|
|
435
|
+
return Or(self.left.to_msfol(), self.right.to_msfol())
|
|
436
|
+
|
|
437
|
+
def _relativize(self, facts: list) -> "Node":
|
|
438
|
+
raise RuntimeError("StrongDisjunction._relativize called; call to_msfol() before _relativize.")
|
|
439
|
+
|
|
440
|
+
def to_z3(self, env: Optional[Z3Env] = None):
|
|
441
|
+
"""Reject Z3 export: the classical collapse must be explicit (to_fol first)."""
|
|
442
|
+
raise NotImplementedError(f"to_z3: {type(self).__name__} — " + _LUK_NO_CLASSICAL_EXPORT)
|
|
443
|
+
|
|
444
|
+
def to_prover9(self) -> str:
|
|
445
|
+
"""Reject Prover9 export: the classical collapse must be explicit (to_fol first)."""
|
|
446
|
+
raise NotImplementedError(f"to_prover9: {type(self).__name__} — " + _LUK_NO_CLASSICAL_EXPORT)
|
|
447
|
+
|
|
448
|
+
def to_tptp(self) -> str:
|
|
449
|
+
"""Reject TPTP export: the classical collapse must be explicit (to_fol first)."""
|
|
450
|
+
raise NotImplementedError(f"to_tptp: {type(self).__name__} — " + _LUK_NO_CLASSICAL_EXPORT)
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
@dataclass(frozen=True)
|
|
454
|
+
class LukNegation(Node):
|
|
455
|
+
"""Łukasiewicz negation: 1−x.
|
|
456
|
+
|
|
457
|
+
Uses the same glyph ¬ as classical Not; distinct by class identity.
|
|
458
|
+
"""
|
|
459
|
+
|
|
460
|
+
formula: Node
|
|
461
|
+
|
|
462
|
+
def _tree_parts(self):
|
|
463
|
+
return "¬", [self.formula]
|
|
464
|
+
|
|
465
|
+
def to_dict(self):
|
|
466
|
+
return {"_type": "LukNegation", "formula": self.formula.to_dict()}
|
|
467
|
+
|
|
468
|
+
@staticmethod
|
|
469
|
+
def from_dict(d):
|
|
470
|
+
return LukNegation(Node.from_dict(d["formula"]))
|
|
471
|
+
|
|
472
|
+
def to_msfol(self) -> "Node":
|
|
473
|
+
return Not(self.formula.to_msfol())
|
|
474
|
+
|
|
475
|
+
def _relativize(self, facts: list) -> "Node":
|
|
476
|
+
raise RuntimeError("LukNegation._relativize called; call to_msfol() before _relativize.")
|
|
477
|
+
|
|
478
|
+
def to_z3(self, env: Optional[Z3Env] = None):
|
|
479
|
+
"""Reject Z3 export: the classical collapse must be explicit (to_fol first)."""
|
|
480
|
+
raise NotImplementedError(f"to_z3: {type(self).__name__} — " + _LUK_NO_CLASSICAL_EXPORT)
|
|
481
|
+
|
|
482
|
+
def to_prover9(self) -> str:
|
|
483
|
+
"""Reject Prover9 export: the classical collapse must be explicit (to_fol first)."""
|
|
484
|
+
raise NotImplementedError(f"to_prover9: {type(self).__name__} — " + _LUK_NO_CLASSICAL_EXPORT)
|
|
485
|
+
|
|
486
|
+
def to_tptp(self) -> str:
|
|
487
|
+
"""Reject TPTP export: the classical collapse must be explicit (to_fol first)."""
|
|
488
|
+
raise NotImplementedError(f"to_tptp: {type(self).__name__} — " + _LUK_NO_CLASSICAL_EXPORT)
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
@dataclass(frozen=True)
|
|
492
|
+
class LukImplication(Node):
|
|
493
|
+
"""Łukasiewicz implication: min{1, 1−x+y}.
|
|
494
|
+
|
|
495
|
+
Uses the same glyph → as classical Implies; distinct by class identity.
|
|
496
|
+
"""
|
|
497
|
+
|
|
498
|
+
left: Node
|
|
499
|
+
right: Node
|
|
500
|
+
|
|
501
|
+
def _tree_parts(self):
|
|
502
|
+
return "→", [self.left, self.right]
|
|
503
|
+
|
|
504
|
+
def to_dict(self):
|
|
505
|
+
return {"_type": "LukImplication", "left": self.left.to_dict(), "right": self.right.to_dict()}
|
|
506
|
+
|
|
507
|
+
@staticmethod
|
|
508
|
+
def from_dict(d):
|
|
509
|
+
return LukImplication(Node.from_dict(d["left"]), Node.from_dict(d["right"]))
|
|
510
|
+
|
|
511
|
+
def to_msfol(self) -> "Node":
|
|
512
|
+
return Implies(self.left.to_msfol(), self.right.to_msfol())
|
|
513
|
+
|
|
514
|
+
def _relativize(self, facts: list) -> "Node":
|
|
515
|
+
raise RuntimeError("LukImplication._relativize called; call to_msfol() before _relativize.")
|
|
516
|
+
|
|
517
|
+
def to_z3(self, env: Optional[Z3Env] = None):
|
|
518
|
+
"""Reject Z3 export: the classical collapse must be explicit (to_fol first)."""
|
|
519
|
+
raise NotImplementedError(f"to_z3: {type(self).__name__} — " + _LUK_NO_CLASSICAL_EXPORT)
|
|
520
|
+
|
|
521
|
+
def to_prover9(self) -> str:
|
|
522
|
+
"""Reject Prover9 export: the classical collapse must be explicit (to_fol first)."""
|
|
523
|
+
raise NotImplementedError(f"to_prover9: {type(self).__name__} — " + _LUK_NO_CLASSICAL_EXPORT)
|
|
524
|
+
|
|
525
|
+
def to_tptp(self) -> str:
|
|
526
|
+
"""Reject TPTP export: the classical collapse must be explicit (to_fol first)."""
|
|
527
|
+
raise NotImplementedError(f"to_tptp: {type(self).__name__} — " + _LUK_NO_CLASSICAL_EXPORT)
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
@dataclass(frozen=True)
|
|
531
|
+
class LukEquivalence(Node):
|
|
532
|
+
"""Łukasiewicz equivalence: 1−|x−y|.
|
|
533
|
+
|
|
534
|
+
Uses the same glyph ↔ as classical Iff; distinct by class identity.
|
|
535
|
+
"""
|
|
536
|
+
|
|
537
|
+
left: Node
|
|
538
|
+
right: Node
|
|
539
|
+
|
|
540
|
+
def _tree_parts(self):
|
|
541
|
+
return "↔", [self.left, self.right]
|
|
542
|
+
|
|
543
|
+
def to_dict(self):
|
|
544
|
+
return {"_type": "LukEquivalence", "left": self.left.to_dict(), "right": self.right.to_dict()}
|
|
545
|
+
|
|
546
|
+
@staticmethod
|
|
547
|
+
def from_dict(d):
|
|
548
|
+
return LukEquivalence(Node.from_dict(d["left"]), Node.from_dict(d["right"]))
|
|
549
|
+
|
|
550
|
+
def to_msfol(self) -> "Node":
|
|
551
|
+
return Iff(self.left.to_msfol(), self.right.to_msfol())
|
|
552
|
+
|
|
553
|
+
def _relativize(self, facts: list) -> "Node":
|
|
554
|
+
raise RuntimeError("LukEquivalence._relativize called; call to_msfol() before _relativize.")
|
|
555
|
+
|
|
556
|
+
def to_z3(self, env: Optional[Z3Env] = None):
|
|
557
|
+
"""Reject Z3 export: the classical collapse must be explicit (to_fol first)."""
|
|
558
|
+
raise NotImplementedError(f"to_z3: {type(self).__name__} — " + _LUK_NO_CLASSICAL_EXPORT)
|
|
559
|
+
|
|
560
|
+
def to_prover9(self) -> str:
|
|
561
|
+
"""Reject Prover9 export: the classical collapse must be explicit (to_fol first)."""
|
|
562
|
+
raise NotImplementedError(f"to_prover9: {type(self).__name__} — " + _LUK_NO_CLASSICAL_EXPORT)
|
|
563
|
+
|
|
564
|
+
def to_tptp(self) -> str:
|
|
565
|
+
"""Reject TPTP export: the classical collapse must be explicit (to_fol first)."""
|
|
566
|
+
raise NotImplementedError(f"to_tptp: {type(self).__name__} — " + _LUK_NO_CLASSICAL_EXPORT)
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
# =========================
|
|
570
|
+
# Operator registration
|
|
571
|
+
# =========================
|
|
572
|
+
#
|
|
573
|
+
# Self-register the Łukasiewicz operators with the central renderers. Each uses
|
|
574
|
+
# the same glyph/markup as its classical counterpart but is distinct by class
|
|
575
|
+
# identity (e.g. LukNegation renders ¬ like Not but lowers differently).
|
|
576
|
+
|
|
577
|
+
register_operator(LukNegation, "prefix", "¬", "\\lnot ", 4)
|
|
578
|
+
register_operator(WeakConjunction, "level2", "∧", "\\land", 3)
|
|
579
|
+
register_operator(WeakDisjunction, "level2", "∨", "\\lor", 3)
|
|
580
|
+
register_operator(StrongConjunction, "level2", "⊗", "\\otimes", 3)
|
|
581
|
+
register_operator(StrongDisjunction, "level2", "⊕", "\\oplus", 3)
|
|
582
|
+
register_operator(LukImplication, "binary_implies", "→", "\\rightarrow", 2)
|
|
583
|
+
register_operator(LukEquivalence, "binary_iff", "↔", "\\leftrightarrow", 1)
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
# =========================
|
|
587
|
+
# Parser registration (MSFL / FL Łukasiewicz + sorted quantifier/constant)
|
|
588
|
+
# =========================
|
|
589
|
+
#
|
|
590
|
+
# The Łukasiewicz connectives appear in both MSFL (sorted) and FL (unsorted)
|
|
591
|
+
# modes with identical grammar fragments; they register once per mode. The
|
|
592
|
+
# transforms mirror LukConnectivesMixin exactly. The sorted quantifier registers
|
|
593
|
+
# for MSFOL and MSFL (both sorted), the unsorted Łukasiewicz quantifier for FL,
|
|
594
|
+
# and sorted-constant handling rides on the SORTED term-layer flag (selected by
|
|
595
|
+
# build_grammar for msfol/msfl) plus the sorted_const_ transform here.
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def _sorted_quantifier_transform(items):
|
|
599
|
+
"""Build a SortedQuantifier from [FORALL/EXISTS, Variable, SORT, body]."""
|
|
600
|
+
quant_tok, var, sort_tok, formula = items
|
|
601
|
+
sort = str(sort_tok)[1:] # strip leading ':'
|
|
602
|
+
return SortedQuantifier(str(quant_tok), var, sort, formula)
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def _sorted_const_transform(items):
|
|
606
|
+
"""Build a SortedConstant from [NAME/CONSTANT/QUOTED_NAME, SORT].
|
|
607
|
+
|
|
608
|
+
NAME and QUOTED_NAME are converted to a Constant by their token handlers (the
|
|
609
|
+
quoted one with its escapes undone), CONSTANT arrives as a raw token; either way
|
|
610
|
+
the name is the constant's own.
|
|
611
|
+
"""
|
|
612
|
+
first, sort_tok = items
|
|
613
|
+
name = first.name if isinstance(first, Constant) else str(first)
|
|
614
|
+
sort = str(sort_tok)[1:] # strip leading ':'
|
|
615
|
+
return SortedConstant(name, sort)
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
def _luk_quantifier_transform(items):
|
|
619
|
+
"""Build an unsorted Quantifier (FL mode) from [FORALL/EXISTS, Variable, body]."""
|
|
620
|
+
return Quantifier(str(items[0]), items[1], items[2])
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
# --- prefix: ¬ (LukNegation) in MSFL and FL ---
|
|
624
|
+
for _m in ("msfl", "fl"):
|
|
625
|
+
register_parser_op(LukNegation, _m, "prefix", "luk_not_", '"¬" prefix',
|
|
626
|
+
lambda items: LukNegation(items[0]))
|
|
627
|
+
|
|
628
|
+
# --- level2: ∧ ∨ ⊗ ⊕ (weak/strong) in MSFL and FL ---
|
|
629
|
+
for _m in ("msfl", "fl"):
|
|
630
|
+
register_parser_op(WeakConjunction, _m, "level2", "weak_and_", '"∧"',
|
|
631
|
+
lambda items: _fold_binary(items, WeakConjunction),
|
|
632
|
+
only_name="only_weak_and")
|
|
633
|
+
register_parser_op(WeakDisjunction, _m, "level2", "weak_or_", '"∨"',
|
|
634
|
+
lambda items: _fold_binary(items, WeakDisjunction),
|
|
635
|
+
only_name="only_weak_or")
|
|
636
|
+
register_parser_op(StrongConjunction, _m, "level2", "strong_and_", '"⊗"',
|
|
637
|
+
lambda items: _fold_binary(items, StrongConjunction),
|
|
638
|
+
only_name="only_strong_and")
|
|
639
|
+
register_parser_op(StrongDisjunction, _m, "level2", "strong_or_", '"⊕"',
|
|
640
|
+
lambda items: _fold_binary(items, StrongDisjunction),
|
|
641
|
+
only_name="only_strong_or")
|
|
642
|
+
|
|
643
|
+
# --- implication: → (LukImplication) in MSFL and FL ---
|
|
644
|
+
# Binary levels store just the glyph; build_grammar assembles the right-assoc rule.
|
|
645
|
+
for _m in ("msfl", "fl"):
|
|
646
|
+
register_parser_op(LukImplication, _m, "implication", "luk_implies_", '"→"',
|
|
647
|
+
lambda items: LukImplication(items[0], items[1]))
|
|
648
|
+
|
|
649
|
+
# --- biimplication: ↔ (LukEquivalence) in MSFL and FL ---
|
|
650
|
+
for _m in ("msfl", "fl"):
|
|
651
|
+
register_parser_op(LukEquivalence, _m, "biimplication", "luk_iff_", '"↔"',
|
|
652
|
+
lambda items: LukEquivalence(items[0], items[1]))
|
|
653
|
+
|
|
654
|
+
# --- quantifier: sorted ∀x:S / ∃x:S (MSFOL, MSFL); unsorted (FL) ---
|
|
655
|
+
register_parser_op(SortedQuantifier, "msfol", "quantifier", "sorted_quantifier_",
|
|
656
|
+
"(FORALL | EXISTS) VARIABLE SORT prefix", _sorted_quantifier_transform)
|
|
657
|
+
register_parser_op(SortedQuantifier, "msfl", "quantifier", "sorted_quantifier_",
|
|
658
|
+
"(FORALL | EXISTS) VARIABLE SORT prefix", _sorted_quantifier_transform)
|
|
659
|
+
register_parser_op(Quantifier, "fl", "quantifier", "quantifier_",
|
|
660
|
+
"(FORALL | EXISTS) VARIABLE prefix", _luk_quantifier_transform)
|
|
661
|
+
|
|
662
|
+
# --- sorted constant transform (term layer, MSFOL + MSFL via the SORTED flag) ---
|
|
663
|
+
# build_grammar emits the NAME SORT / CONSTANT SORT / QUOTED_NAME SORT ->
|
|
664
|
+
# sorted_const_ rules for
|
|
665
|
+
# sorted modes; the transform is registered as a non-grammar-contributing handler
|
|
666
|
+
# so it is attached to the assembled Transformer for those modes.
|
|
667
|
+
for _m in ("msfol", "msfl"):
|
|
668
|
+
register_parser_op(SortedConstant, _m, "quantifier", "sorted_const_",
|
|
669
|
+
"", _sorted_const_transform)
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def _sorted_count_transform(items):
|
|
673
|
+
"""Build a SortedCount from [COUNTOP glyph, NUMBER, Variable, SORT, body]."""
|
|
674
|
+
op = _COUNT_TOKEN_TO_OP[str(items[0])]
|
|
675
|
+
n = _count_bound(items[0], items[1]) # a CountBoundError (ParsingError) if not a count
|
|
676
|
+
sort = str(items[3])[1:] # strip leading ':'
|
|
677
|
+
return SortedCount(op, Number(n), items[2], sort, items[4])
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
def _sorted_cardinality_transform(items):
|
|
681
|
+
"""Build a SortedCardinality from [VARIABLE, SORT, formula]."""
|
|
682
|
+
var, sort_tok, formula = items
|
|
683
|
+
return SortedCardinality(var, str(sort_tok)[1:], formula) # strip leading ':'
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
# --- sorted counting quantifier: ∃≥n / ∃≤n / ∃=n x:S (SortedCount), MSFOL only ---
|
|
687
|
+
# The classical unsorted Count lives in fol/modal/second-order; MSFOL, where every
|
|
688
|
+
# binder is sorted, gets the sort-annotated counting quantifier instead. COUNTOP is
|
|
689
|
+
# the shared priority-5 terminal (∃ followed by ≥/≤/=).
|
|
690
|
+
register_parser_op(SortedCount, "msfol", "quantifier", "sorted_count_",
|
|
691
|
+
"COUNTOP NUMBER VARIABLE SORT prefix", _sorted_count_transform,
|
|
692
|
+
terminal_name="COUNTOP", terminal_def="COUNTOP.5: /∃[≥≤=]/")
|
|
693
|
+
|
|
694
|
+
# --- sorted set-cardinality term: |{v:S : φ}| (SortedCardinality), MSFOL only ---
|
|
695
|
+
# Handler-only op: the grammar alternative is emitted by the SORTED term-extra in
|
|
696
|
+
# build_grammar (like sorted_const_); the transform is attached to the Transformer.
|
|
697
|
+
register_parser_op(SortedCardinality, "msfol", "quantifier", "sorted_cardinality_",
|
|
698
|
+
"", _sorted_cardinality_transform)
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
# =========================
|
|
702
|
+
# Lambda-Calculus Nodes
|
|
703
|
+
# =========================
|
|
704
|
+
|
|
705
|
+
@dataclass(frozen=True)
|
|
706
|
+
class LambdaVar(Node):
|
|
707
|
+
"""A lambda-bound variable, distinct from logical Variable.
|
|
708
|
+
|
|
709
|
+
Kept separate so lambda binding and logical binding never get confused.
|
|
710
|
+
"""
|
|
711
|
+
|
|
712
|
+
name: str
|
|
713
|
+
|
|
714
|
+
def _tree_parts(self):
|
|
715
|
+
return f"LambdaVar: {self.name}", []
|
|
716
|
+
|
|
717
|
+
def to_dict(self):
|
|
718
|
+
return {"_type": "LambdaVar", "name": self.name}
|
|
719
|
+
|
|
720
|
+
@staticmethod
|
|
721
|
+
def from_dict(d):
|
|
722
|
+
return LambdaVar(d["name"])
|
|
723
|
+
|
|
724
|
+
def to_msfol(self) -> "Node":
|
|
725
|
+
raise NotImplementedError("Beta-reduce lambda terms before the MSFL export pipeline.")
|
|
726
|
+
|
|
727
|
+
def _relativize(self, facts: list) -> "Node":
|
|
728
|
+
raise NotImplementedError("Beta-reduce lambda terms before the MSFL export pipeline.")
|
|
729
|
+
|
|
730
|
+
def to_z3(self, env: Optional[Z3Env] = None):
|
|
731
|
+
raise NotImplementedError("Lambda terms must be beta-reduced and lambda-eliminated before export.")
|
|
732
|
+
|
|
733
|
+
def to_prover9(self) -> str:
|
|
734
|
+
raise NotImplementedError("Lambda terms must be beta-reduced and lambda-eliminated before export.")
|
|
735
|
+
|
|
736
|
+
def to_tptp(self) -> str:
|
|
737
|
+
raise NotImplementedError("Lambda terms must be beta-reduced and lambda-eliminated before export.")
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
@dataclass(frozen=True)
|
|
741
|
+
class Lambda(Node):
|
|
742
|
+
"""A lambda abstraction λparam. body.
|
|
743
|
+
|
|
744
|
+
param is a LambdaVar object, mirroring how Quantifier holds a Variable object.
|
|
745
|
+
"""
|
|
746
|
+
|
|
747
|
+
param: LambdaVar
|
|
748
|
+
body: Node
|
|
749
|
+
|
|
750
|
+
def _tree_parts(self):
|
|
751
|
+
return f"λ {self.param.name}", [self.body]
|
|
752
|
+
|
|
753
|
+
def to_dict(self):
|
|
754
|
+
return {"_type": "Lambda", "param": self.param.to_dict(), "body": self.body.to_dict()}
|
|
755
|
+
|
|
756
|
+
@staticmethod
|
|
757
|
+
def from_dict(d):
|
|
758
|
+
return Lambda(LambdaVar.from_dict(d["param"]), Node.from_dict(d["body"]))
|
|
759
|
+
|
|
760
|
+
def to_msfol(self) -> "Node":
|
|
761
|
+
raise NotImplementedError("Beta-reduce lambda terms before the MSFL export pipeline.")
|
|
762
|
+
|
|
763
|
+
def _relativize(self, facts: list) -> "Node":
|
|
764
|
+
raise NotImplementedError("Beta-reduce lambda terms before the MSFL export pipeline.")
|
|
765
|
+
|
|
766
|
+
def to_z3(self, env: Optional[Z3Env] = None):
|
|
767
|
+
raise NotImplementedError("Lambda terms must be beta-reduced and lambda-eliminated before export.")
|
|
768
|
+
|
|
769
|
+
def to_prover9(self) -> str:
|
|
770
|
+
raise NotImplementedError("Lambda terms must be beta-reduced and lambda-eliminated before export.")
|
|
771
|
+
|
|
772
|
+
def to_tptp(self) -> str:
|
|
773
|
+
raise NotImplementedError("Lambda terms must be beta-reduced and lambda-eliminated before export.")
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
@dataclass(frozen=True)
|
|
777
|
+
class Application(Node):
|
|
778
|
+
"""A lambda application func(arg)."""
|
|
779
|
+
|
|
780
|
+
func: Node
|
|
781
|
+
arg: Node
|
|
782
|
+
|
|
783
|
+
def _tree_parts(self):
|
|
784
|
+
return "App", [self.func, self.arg]
|
|
785
|
+
|
|
786
|
+
def to_dict(self):
|
|
787
|
+
return {"_type": "Application", "func": self.func.to_dict(), "arg": self.arg.to_dict()}
|
|
788
|
+
|
|
789
|
+
@staticmethod
|
|
790
|
+
def from_dict(d):
|
|
791
|
+
return Application(Node.from_dict(d["func"]), Node.from_dict(d["arg"]))
|
|
792
|
+
|
|
793
|
+
def to_msfol(self) -> "Node":
|
|
794
|
+
raise NotImplementedError("Beta-reduce lambda terms before the MSFL export pipeline.")
|
|
795
|
+
|
|
796
|
+
def _relativize(self, facts: list) -> "Node":
|
|
797
|
+
raise NotImplementedError("Beta-reduce lambda terms before the MSFL export pipeline.")
|
|
798
|
+
|
|
799
|
+
def to_z3(self, env: Optional[Z3Env] = None):
|
|
800
|
+
raise NotImplementedError("Lambda terms must be beta-reduced and lambda-eliminated before export.")
|
|
801
|
+
|
|
802
|
+
def to_prover9(self) -> str:
|
|
803
|
+
raise NotImplementedError("Lambda terms must be beta-reduced and lambda-eliminated before export.")
|
|
804
|
+
|
|
805
|
+
def to_tptp(self) -> str:
|
|
806
|
+
raise NotImplementedError("Lambda terms must be beta-reduced and lambda-eliminated before export.")
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
# =========================
|
|
810
|
+
# Free-variable computation
|
|
811
|
+
# =========================
|
|
812
|
+
|
|
813
|
+
def free_variables(node: Node) -> set:
|
|
814
|
+
"""Return the set of free Variable and LambdaVar occurrences in node.
|
|
815
|
+
|
|
816
|
+
The returned set is mixed: it may contain Variable objects (bound by
|
|
817
|
+
Quantifier / SortedQuantifier) and LambdaVar objects (bound by Lambda).
|
|
818
|
+
The two kinds are kept distinct so that a lambda binder over LambdaVar("x")
|
|
819
|
+
never accidentally removes a logical Variable("x") from the free set.
|
|
820
|
+
"""
|
|
821
|
+
if isinstance(node, (Variable, LambdaVar)):
|
|
822
|
+
return {node}
|
|
823
|
+
if isinstance(node, (Constant, Number, SortedConstant)):
|
|
824
|
+
return set()
|
|
825
|
+
if isinstance(node, Lambda):
|
|
826
|
+
return free_variables(node.body) - {node.param}
|
|
827
|
+
if isinstance(node, SlashedExists):
|
|
828
|
+
# The slash set is not decoration: each name is a free OCCURRENCE of the
|
|
829
|
+
# variable it references (the team column the witness must be independent
|
|
830
|
+
# of), so it belongs in the free set alongside the matrix's variables. A
|
|
831
|
+
# slash name is free unless the SlashedExists' own binder captures it (it
|
|
832
|
+
# cannot — a variable never independent of itself is meaningless, but the
|
|
833
|
+
# subtraction below keeps the rule uniform). Omitting them would let the
|
|
834
|
+
# capture-avoidance machinery (canonicalize / _subst) mint a fresh name
|
|
835
|
+
# equal to a free slash name and silently capture it.
|
|
836
|
+
slashed = {Variable(n) for n in node.slashed}
|
|
837
|
+
return (free_variables(node.formula) | slashed) - {node.variable}
|
|
838
|
+
if isinstance(node, (Quantifier, SortedQuantifier, Count, Cardinality,
|
|
839
|
+
SortedCount, SortedCardinality)):
|
|
840
|
+
# Count / Cardinality (and their sorted variants) also bind their variable
|
|
841
|
+
# over the matrix; Count's n is a Number (no free variables) and the sort is
|
|
842
|
+
# a plain string, so the binder rule is the same.
|
|
843
|
+
return free_variables(node.formula) - {node.variable}
|
|
844
|
+
if not is_dataclass(node):
|
|
845
|
+
raise TypeError(f"free_variables: unknown node type {type(node).__name__}")
|
|
846
|
+
# Structural: for any non-binder, non-leaf node the free variables are the
|
|
847
|
+
# union of its children's. Covers Atom, Function, Application, Not, and every
|
|
848
|
+
# binary connective uniformly — and any future structural node type.
|
|
849
|
+
result: set = set()
|
|
850
|
+
for child in node._child_nodes():
|
|
851
|
+
result |= free_variables(child)
|
|
852
|
+
return result
|
|
853
|
+
|
|
854
|
+
|
|
855
|
+
# =========================
|
|
856
|
+
# Capture-avoiding beta-reduction
|
|
857
|
+
# =========================
|
|
858
|
+
|
|
859
|
+
BETA_REDUCTION_LIMIT = 10_000
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
class ReductionLimitError(Exception):
|
|
863
|
+
"""Raised when a lambda reduction exceeds its limit.
|
|
864
|
+
|
|
865
|
+
``beta_reduce`` raises it after ``BETA_REDUCTION_LIMIT`` (10 000) steps, and
|
|
866
|
+
``beta_eta_normalize`` after ``BETA_ETA_ROUND_LIMIT`` (100) alternation
|
|
867
|
+
rounds — both signalling a term that is (likely) not strongly normalizing.
|
|
868
|
+
"""
|
|
869
|
+
pass
|
|
870
|
+
|
|
871
|
+
|
|
872
|
+
def _names_in(node: Node) -> set:
|
|
873
|
+
"""Return all Variable and LambdaVar nodes appearing in node (free and bound)."""
|
|
874
|
+
if isinstance(node, (Variable, LambdaVar)):
|
|
875
|
+
return {node}
|
|
876
|
+
if isinstance(node, (Constant, Number, SortedConstant)):
|
|
877
|
+
return set()
|
|
878
|
+
# Structural union over children. A binder's bound variable is itself a Node
|
|
879
|
+
# child (Lambda.param, Quantifier.variable), so it is included here — exactly
|
|
880
|
+
# what "names in, free and bound" requires.
|
|
881
|
+
result: set = set()
|
|
882
|
+
if isinstance(node, SlashedExists):
|
|
883
|
+
# Slash names are variable references but plain strings, so _child_nodes()
|
|
884
|
+
# misses them; add them so a fresh-name mint never collides with one.
|
|
885
|
+
result |= {Variable(n) for n in node.slashed}
|
|
886
|
+
for child in node._child_nodes():
|
|
887
|
+
result |= _names_in(child)
|
|
888
|
+
return result
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
def subst_slash_set(term: "SlashedExists", target: Node, replacement: Node) -> Node:
|
|
892
|
+
"""Rewrite a slashed existential's SLASH SET under ``target := replacement``.
|
|
893
|
+
|
|
894
|
+
The slash set names variables (team columns) bound by ENCLOSING quantifiers, so a
|
|
895
|
+
mention of ``target`` there is rewritten *before* the binder's own shadowing check
|
|
896
|
+
— unlike the matrix, the annotation is not shadowed by ``term.variable``.
|
|
897
|
+
Substituting a slash entry transforms the annotation by what replaces it:
|
|
898
|
+
|
|
899
|
+
* another **Variable** ``z`` → rename the entry ``x ↦ z``; independence is now
|
|
900
|
+
from column ``z``. The constraint is preserved, never dropped.
|
|
901
|
+
* a **ground or compound term** → that column no longer exists, so "independent
|
|
902
|
+
of ``x``" is vacuous and the entry is dropped. Since
|
|
903
|
+
:class:`~unicode_logic_kit.fol._team_nodes.SlashedExists` requires a non-empty
|
|
904
|
+
slash set, an emptied one degrades to a plain existential — so the return type
|
|
905
|
+
is a general :class:`Node`, not necessarily a ``SlashedExists``.
|
|
906
|
+
|
|
907
|
+
The matrix is left untouched; callers substitute into it themselves.
|
|
908
|
+
"""
|
|
909
|
+
slashed = term.slashed
|
|
910
|
+
if not (isinstance(target, Variable) and target.name in slashed):
|
|
911
|
+
return term
|
|
912
|
+
if isinstance(replacement, Variable):
|
|
913
|
+
return replace(term, slashed=tuple(replacement.name if n == target.name else n
|
|
914
|
+
for n in slashed))
|
|
915
|
+
slashed = tuple(n for n in slashed if n != target.name)
|
|
916
|
+
if not slashed:
|
|
917
|
+
return Quantifier("∃", term.variable, term.formula)
|
|
918
|
+
return replace(term, slashed=slashed)
|
|
919
|
+
|
|
920
|
+
|
|
921
|
+
def _fresh_name(base: str, avoid: set) -> str:
|
|
922
|
+
"""A fresh object-variable name for the binder ``base`` that is being alpha-renamed.
|
|
923
|
+
|
|
924
|
+
``avoid`` is a set of ``Variable`` / ``LambdaVar`` nodes and the result differs
|
|
925
|
+
from the name of every one of them. It is also a name the kit's own parser
|
|
926
|
+
reads back: one letter and digits (``y`` becomes ``y0``, then ``y1``, …).
|
|
927
|
+
The earlier ``y_0`` was printed by ``to_unicode_str`` and rejected by every
|
|
928
|
+
parser mode, because VARIABLE takes no underscore. Deterministic: the first
|
|
929
|
+
free ``letter`` + N, N = 0, 1, …
|
|
930
|
+
|
|
931
|
+
A name is only as safe as the set it is checked against: the caller passes
|
|
932
|
+
the replacement's free variables AND every name inside the scope being
|
|
933
|
+
renamed, bound ones included, or the renamed occurrences are captured by an
|
|
934
|
+
inner binder that already used the new name.
|
|
935
|
+
"""
|
|
936
|
+
return fresh_variable_like(base, {n.name for n in avoid})
|
|
937
|
+
|
|
938
|
+
|
|
939
|
+
def _binder_avoid(binder: Node, body: Node, target: Node, replacement: Node,
|
|
940
|
+
fv_repl: set, slashed: Tuple[str, ...] = ()) -> frozenset:
|
|
941
|
+
"""Every name a binder that is renamed during ``body[target := replacement]`` must avoid.
|
|
942
|
+
|
|
943
|
+
``binder`` is the bound variable being given a new name and ``body`` its scope. The new
|
|
944
|
+
name is checked, as a string, against
|
|
945
|
+
|
|
946
|
+
* every name the scope and the replacement carry, of every kind: a constant, a function,
|
|
947
|
+
a predicate or a sort spelled like the new binder would print as the very same word,
|
|
948
|
+
and a constant of the replacement that ends up inside the scope is one of them;
|
|
949
|
+
* the free variables of the replacement: the binder is renamed because of them;
|
|
950
|
+
* the TARGET: the renamed scope is substituted next, and a binder renamed onto the
|
|
951
|
+
target's own spelling would have its occurrences replaced by the replacement (``∀x1 P(x1)``
|
|
952
|
+
under ``x0 := x1`` would come out as ``∀x0 P(x1)``, with a free ``x1`` the input
|
|
953
|
+
does not have);
|
|
954
|
+
* the old binder itself, and the slash names of a slashed existential, which refer to
|
|
955
|
+
enclosing variables and are plain strings no walk of the scope would find.
|
|
956
|
+
|
|
957
|
+
The one place this set is built, so the generic substitution and the proof searches'
|
|
958
|
+
substitution (:func:`unicode_logic_kit.atp.fitch._subst_var`) mint the same name: a checker
|
|
959
|
+
that recomputes an instance compares it with the searched one structurally.
|
|
960
|
+
"""
|
|
961
|
+
names = set(symbol_names(body, replacement))
|
|
962
|
+
names.update(v.name for v in fv_repl)
|
|
963
|
+
names.update(slashed)
|
|
964
|
+
names.add(binder.name)
|
|
965
|
+
target_name = getattr(target, "name", None)
|
|
966
|
+
if target_name is not None:
|
|
967
|
+
names.add(target_name)
|
|
968
|
+
return frozenset(names)
|
|
969
|
+
|
|
970
|
+
|
|
971
|
+
def _fresh_binder_name(binder: Node, body: Node, target: Node, replacement: Node,
|
|
972
|
+
fv_repl: set, slashed: Tuple[str, ...] = ()) -> str:
|
|
973
|
+
"""A name for the renamed object variable ``binder`` (see :func:`_binder_avoid`)."""
|
|
974
|
+
return fresh_variable_like(
|
|
975
|
+
binder.name, _binder_avoid(binder, body, target, replacement, fv_repl, slashed))
|
|
976
|
+
|
|
977
|
+
|
|
978
|
+
def _spelled_like_a_free_variable(binder: Node, fv_repl: set) -> bool:
|
|
979
|
+
"""True iff ``binder`` has the name of a free variable of the replacement, of either kind.
|
|
980
|
+
|
|
981
|
+
A :class:`Variable` and a :class:`LambdaVar` of one name are two nodes, but one name in
|
|
982
|
+
every text the kit writes: under ``λy.`` the text ``R(y, y)`` cannot say that its first
|
|
983
|
+
``y`` is the free variable that was substituted in and its second the parameter, and it
|
|
984
|
+
reads back with both bound. So a binder is renamed whenever the NAME meets, whichever
|
|
985
|
+
kind of variable it binds.
|
|
986
|
+
"""
|
|
987
|
+
return any(free.name == binder.name for free in fv_repl)
|
|
988
|
+
|
|
989
|
+
|
|
990
|
+
def _rename(term: Node, old_var, new_var) -> Node:
|
|
991
|
+
"""Replace all free occurrences of old_var with new_var, stopping at shadowing binders.
|
|
992
|
+
|
|
993
|
+
Caller guarantees new_var.name does not appear anywhere in term,
|
|
994
|
+
so no capture check is needed here.
|
|
995
|
+
"""
|
|
996
|
+
if term == old_var:
|
|
997
|
+
return new_var
|
|
998
|
+
if isinstance(term, (Variable, LambdaVar, Constant, Number, SortedConstant)):
|
|
999
|
+
return term # leaf that does not match old_var
|
|
1000
|
+
if isinstance(term, Lambda):
|
|
1001
|
+
if term.param == old_var:
|
|
1002
|
+
return term # shadowed
|
|
1003
|
+
return Lambda(term.param, _rename(term.body, old_var, new_var))
|
|
1004
|
+
if isinstance(term, Quantifier):
|
|
1005
|
+
if term.variable == old_var:
|
|
1006
|
+
return term # shadowed
|
|
1007
|
+
return Quantifier(term.type, term.variable, _rename(term.formula, old_var, new_var))
|
|
1008
|
+
if isinstance(term, SortedQuantifier):
|
|
1009
|
+
if term.variable == old_var:
|
|
1010
|
+
return term # shadowed
|
|
1011
|
+
return SortedQuantifier(term.type, term.variable, term.sort,
|
|
1012
|
+
_rename(term.formula, old_var, new_var))
|
|
1013
|
+
if isinstance(term, (Count, Cardinality, SortedCount, SortedCardinality)):
|
|
1014
|
+
if term.variable == old_var:
|
|
1015
|
+
return term # shadowed by the counting/cardinality binder
|
|
1016
|
+
return replace(term, formula=_rename(term.formula, old_var, new_var))
|
|
1017
|
+
if isinstance(term, SlashedExists):
|
|
1018
|
+
# The slash set refers to ENCLOSING binders by name, so a mention of the
|
|
1019
|
+
# renamed variable is updated even when the matrix is shadowed.
|
|
1020
|
+
slashed = tuple(new_var.name if n == old_var.name else n
|
|
1021
|
+
for n in term.slashed)
|
|
1022
|
+
if term.variable == old_var:
|
|
1023
|
+
return replace(term, slashed=slashed) # matrix shadowed
|
|
1024
|
+
return replace(term, slashed=slashed,
|
|
1025
|
+
formula=_rename(term.formula, old_var, new_var))
|
|
1026
|
+
# Structural: Atom, Function, Application, Not, and the binary connectives —
|
|
1027
|
+
# none are binders, so recurse uniformly into every child.
|
|
1028
|
+
return term.map_children(lambda c: _rename(c, old_var, new_var))
|
|
1029
|
+
|
|
1030
|
+
|
|
1031
|
+
def _subst(term: Node, target: LambdaVar, replacement: Node, fv_repl: set) -> Node:
|
|
1032
|
+
"""Capture-avoiding substitution of target with replacement in term.
|
|
1033
|
+
|
|
1034
|
+
fv_repl = free_variables(replacement), precomputed by the caller.
|
|
1035
|
+
target is a LambdaVar (beta-reduction) or a Variable (grounding a quantified
|
|
1036
|
+
object variable); replacement may be any Node. Substitution stops at a binder that
|
|
1037
|
+
rebinds the target (Lambda/Quantifier/SortedQuantifier) and alpha-renames any binder
|
|
1038
|
+
that would otherwise capture a free variable of replacement.
|
|
1039
|
+
"""
|
|
1040
|
+
if term == target:
|
|
1041
|
+
return replacement
|
|
1042
|
+
if isinstance(term, (Variable, LambdaVar, Constant, Number, SortedConstant)):
|
|
1043
|
+
return term
|
|
1044
|
+
if isinstance(term, Lambda):
|
|
1045
|
+
if term.param == target:
|
|
1046
|
+
return term # target rebound here — substitution stops
|
|
1047
|
+
if _spelled_like_a_free_variable(term.param, fv_repl):
|
|
1048
|
+
# The binder is spelled like a free variable of the replacement (a LambdaVar it
|
|
1049
|
+
# would capture, or a Variable its text could not tell from itself); alpha-convert.
|
|
1050
|
+
# A parameter may be a variable (``λx.``), a NAME (``λfoo.``) or a predicate
|
|
1051
|
+
# (``λP.``), and the body uses it in that position, so the new name keeps the
|
|
1052
|
+
# kind: ``x`` becomes ``x0``, ``foo`` becomes ``foo_0`` and ``P`` becomes
|
|
1053
|
+
# ``P_0`` (see :func:`~unicode_logic_kit.fol._identifiers.fresh_like`). It avoids
|
|
1054
|
+
# the target as well as the scope and the replacement: the renamed body is
|
|
1055
|
+
# substituted next.
|
|
1056
|
+
fresh_param = LambdaVar(fresh_like(
|
|
1057
|
+
term.param.name,
|
|
1058
|
+
_binder_avoid(term.param, term.body, target, replacement, fv_repl)))
|
|
1059
|
+
new_body = _rename(term.body, term.param, fresh_param)
|
|
1060
|
+
return Lambda(fresh_param, _subst(new_body, target, replacement, fv_repl))
|
|
1061
|
+
return Lambda(term.param, _subst(term.body, target, replacement, fv_repl))
|
|
1062
|
+
if isinstance(term, Quantifier):
|
|
1063
|
+
# If the quantifier rebinds the target, the body is shadowed and substitution
|
|
1064
|
+
# stops here. This matters when target is a Variable (e.g. satisfies_modal
|
|
1065
|
+
# grounding an object quantifier); for a LambdaVar target the equality is always
|
|
1066
|
+
# False (distinct classes), so this is a no-op on the beta-reduction path.
|
|
1067
|
+
if term.variable == target:
|
|
1068
|
+
return term
|
|
1069
|
+
# Otherwise the quantifier variable may capture a free Variable from replacement.
|
|
1070
|
+
if _spelled_like_a_free_variable(term.variable, fv_repl):
|
|
1071
|
+
fresh = Variable(_fresh_binder_name(
|
|
1072
|
+
term.variable, term.formula, target, replacement, fv_repl))
|
|
1073
|
+
new_formula = _rename(term.formula, term.variable, fresh)
|
|
1074
|
+
return Quantifier(term.type, fresh,
|
|
1075
|
+
_subst(new_formula, target, replacement, fv_repl))
|
|
1076
|
+
return Quantifier(term.type, term.variable,
|
|
1077
|
+
_subst(term.formula, target, replacement, fv_repl))
|
|
1078
|
+
if isinstance(term, SortedQuantifier):
|
|
1079
|
+
if term.variable == target:
|
|
1080
|
+
return term # target rebound here — substitution stops
|
|
1081
|
+
if _spelled_like_a_free_variable(term.variable, fv_repl):
|
|
1082
|
+
fresh = Variable(_fresh_binder_name(
|
|
1083
|
+
term.variable, term.formula, target, replacement, fv_repl))
|
|
1084
|
+
new_formula = _rename(term.formula, term.variable, fresh)
|
|
1085
|
+
return SortedQuantifier(term.type, fresh, term.sort,
|
|
1086
|
+
_subst(new_formula, target, replacement, fv_repl))
|
|
1087
|
+
return SortedQuantifier(term.type, term.variable, term.sort,
|
|
1088
|
+
_subst(term.formula, target, replacement, fv_repl))
|
|
1089
|
+
if isinstance(term, SlashedExists):
|
|
1090
|
+
rewritten = subst_slash_set(term, target, replacement)
|
|
1091
|
+
if not isinstance(rewritten, SlashedExists):
|
|
1092
|
+
# The slash set was emptied and degraded to a plain ∃; substitute there.
|
|
1093
|
+
return _subst(rewritten, target, replacement, fv_repl)
|
|
1094
|
+
term = rewritten
|
|
1095
|
+
if term.variable == target:
|
|
1096
|
+
return term # target rebound here — substitution stops
|
|
1097
|
+
if _spelled_like_a_free_variable(term.variable, fv_repl):
|
|
1098
|
+
# The slash set names variables too (plain strings, so no walk of the
|
|
1099
|
+
# matrix can see them): a fresh binder called like one of them would
|
|
1100
|
+
# silently rewire the independence set.
|
|
1101
|
+
fresh = Variable(_fresh_binder_name(
|
|
1102
|
+
term.variable, term.formula, target, replacement, fv_repl,
|
|
1103
|
+
slashed=term.slashed))
|
|
1104
|
+
new_formula = _rename(term.formula, term.variable, fresh)
|
|
1105
|
+
return replace(term, variable=fresh,
|
|
1106
|
+
formula=_subst(new_formula, target, replacement, fv_repl))
|
|
1107
|
+
return replace(term, formula=_subst(term.formula, target, replacement, fv_repl))
|
|
1108
|
+
if isinstance(term, (Count, Cardinality, SortedCount, SortedCardinality)):
|
|
1109
|
+
# Count / Cardinality (and sorted variants) bind their variable over the
|
|
1110
|
+
# matrix — same shadowing / capture-avoidance rules as Quantifier (replace()
|
|
1111
|
+
# preserves Count's op/n and the sorted variants' sort).
|
|
1112
|
+
if term.variable == target:
|
|
1113
|
+
return term # target rebound here — substitution stops
|
|
1114
|
+
if _spelled_like_a_free_variable(term.variable, fv_repl):
|
|
1115
|
+
fresh = Variable(_fresh_binder_name(
|
|
1116
|
+
term.variable, term.formula, target, replacement, fv_repl))
|
|
1117
|
+
new_formula = _rename(term.formula, term.variable, fresh)
|
|
1118
|
+
return replace(term, variable=fresh,
|
|
1119
|
+
formula=_subst(new_formula, target, replacement, fv_repl))
|
|
1120
|
+
return replace(term, formula=_subst(term.formula, target, replacement, fv_repl))
|
|
1121
|
+
# Structural: Atom, Function, Application, Not, and the binary connectives.
|
|
1122
|
+
# The binders above handle capture avoidance; these are not binders, so just
|
|
1123
|
+
# substitute into every child.
|
|
1124
|
+
return term.map_children(lambda c: _subst(c, target, replacement, fv_repl))
|
|
1125
|
+
|
|
1126
|
+
|
|
1127
|
+
def substitute(term: Node, target, replacement: Node) -> Node:
|
|
1128
|
+
"""Substitute target with replacement in term, with full capture avoidance.
|
|
1129
|
+
|
|
1130
|
+
``target`` is a LambdaVar (beta-reduction) or a Variable (grounding a quantified
|
|
1131
|
+
object variable into a Constant). Returns a new Node; the input is never mutated.
|
|
1132
|
+
"""
|
|
1133
|
+
return _subst(term, target, replacement, free_variables(replacement))
|
|
1134
|
+
|
|
1135
|
+
|
|
1136
|
+
def _beta_reduce(node: Node, steps: list) -> Node:
|
|
1137
|
+
# The Application case is iterative so that divergent terms (e.g. Omega) hit the step
|
|
1138
|
+
# counter before Python's recursion limit. All other cases recurse normally.
|
|
1139
|
+
while True:
|
|
1140
|
+
if isinstance(node, Application):
|
|
1141
|
+
func = _beta_reduce(node.func, steps)
|
|
1142
|
+
if isinstance(func, Lambda):
|
|
1143
|
+
steps[0] += 1
|
|
1144
|
+
if steps[0] > BETA_REDUCTION_LIMIT:
|
|
1145
|
+
raise ReductionLimitError(
|
|
1146
|
+
f"beta-reduction exceeded {BETA_REDUCTION_LIMIT} steps; "
|
|
1147
|
+
"term may not be strongly normalizing."
|
|
1148
|
+
)
|
|
1149
|
+
node = substitute(func.body, func.param, node.arg)
|
|
1150
|
+
continue # reduce the substituted result in the same frame
|
|
1151
|
+
return Application(func, _beta_reduce(node.arg, steps))
|
|
1152
|
+
if isinstance(node, (Variable, LambdaVar, Constant, Number, SortedConstant)):
|
|
1153
|
+
return node # leaves
|
|
1154
|
+
# Structural (Lambda, Quantifier, SortedQuantifier, Atom, Function, Not,
|
|
1155
|
+
# and the binary connectives): reduce inside every child. Bound-variable
|
|
1156
|
+
# fields are leaves, so they pass through unchanged.
|
|
1157
|
+
return node.map_children(lambda c: _beta_reduce(c, steps))
|
|
1158
|
+
|
|
1159
|
+
|
|
1160
|
+
def beta_reduce(node: Node) -> Node:
|
|
1161
|
+
"""Reduce node to beta-normal form using normal-order strategy.
|
|
1162
|
+
|
|
1163
|
+
Raises ReductionLimitError if more than BETA_REDUCTION_LIMIT steps are taken.
|
|
1164
|
+
Returns a new Node; the input is never mutated.
|
|
1165
|
+
"""
|
|
1166
|
+
steps = [0]
|
|
1167
|
+
return _beta_reduce(node, steps)
|
|
1168
|
+
|
|
1169
|
+
|
|
1170
|
+
# =========================
|
|
1171
|
+
# Eta-reduction
|
|
1172
|
+
# =========================
|
|
1173
|
+
|
|
1174
|
+
def _eta_reduce(node: Node) -> Node:
|
|
1175
|
+
"""Single bottom-up pass contracting all eta-redexes.
|
|
1176
|
+
|
|
1177
|
+
At each Lambda node, after recursing the body, checks three conditions:
|
|
1178
|
+
1. body is an Application,
|
|
1179
|
+
2. body.arg is the bound parameter (same LambdaVar),
|
|
1180
|
+
3. the parameter is NOT free in body.func.
|
|
1181
|
+
When all hold, contracts λp. f(p) → f. One pass suffices because
|
|
1182
|
+
contraction returns body.func, which was already recursed.
|
|
1183
|
+
"""
|
|
1184
|
+
if isinstance(node, (Variable, LambdaVar, Constant, Number, SortedConstant)):
|
|
1185
|
+
return node
|
|
1186
|
+
if isinstance(node, Lambda):
|
|
1187
|
+
reduced_body = _eta_reduce(node.body)
|
|
1188
|
+
if (isinstance(reduced_body, Application)
|
|
1189
|
+
and reduced_body.arg == node.param
|
|
1190
|
+
and node.param not in free_variables(reduced_body.func)):
|
|
1191
|
+
return reduced_body.func # eta-contract: λp. f(p) → f
|
|
1192
|
+
return Lambda(node.param, reduced_body)
|
|
1193
|
+
# Structural (Application, Quantifier, SortedQuantifier, Atom, Function, Not,
|
|
1194
|
+
# and the binary connectives): recurse into every child. A Quantifier is
|
|
1195
|
+
# never an eta-redex; it is only recursed into.
|
|
1196
|
+
return node.map_children(_eta_reduce)
|
|
1197
|
+
|
|
1198
|
+
|
|
1199
|
+
def eta_reduce(node: Node) -> Node:
|
|
1200
|
+
"""Reduce node to eta-normal form (λx. f(x) → f when x ∉ fv(f)).
|
|
1201
|
+
|
|
1202
|
+
Contracts all eta-redexes bottom-up in a single structural pass.
|
|
1203
|
+
Quantifiers are NOT treated as eta-redexes; they are only recursed into.
|
|
1204
|
+
Returns a new Node; the input is never mutated.
|
|
1205
|
+
"""
|
|
1206
|
+
return _eta_reduce(node)
|
|
1207
|
+
|
|
1208
|
+
|
|
1209
|
+
BETA_ETA_ROUND_LIMIT = 100
|
|
1210
|
+
|
|
1211
|
+
|
|
1212
|
+
def beta_eta_normalize(node: Node) -> Node:
|
|
1213
|
+
"""Reduce node to beta-eta normal form by alternating beta_reduce and eta_reduce.
|
|
1214
|
+
|
|
1215
|
+
The alternation loop is a genuine necessity: eta-reduction can expose fresh
|
|
1216
|
+
beta-redexes (e.g. eta-contracting a func position turns an Application into
|
|
1217
|
+
a beta-redex), so the combined loop must iterate to a fixpoint rather than
|
|
1218
|
+
running each pass exactly once.
|
|
1219
|
+
|
|
1220
|
+
Raises ReductionLimitError if beta_reduce internally exceeds
|
|
1221
|
+
BETA_REDUCTION_LIMIT steps, or if the alternation loop itself exceeds
|
|
1222
|
+
BETA_ETA_ROUND_LIMIT rounds (which only fires on pathological terms that
|
|
1223
|
+
are not strongly normalizing under beta-eta).
|
|
1224
|
+
Returns a new Node; the input is never mutated.
|
|
1225
|
+
"""
|
|
1226
|
+
for _ in range(BETA_ETA_ROUND_LIMIT):
|
|
1227
|
+
after_beta = beta_reduce(node) # may raise ReductionLimitError
|
|
1228
|
+
after_eta = eta_reduce(after_beta)
|
|
1229
|
+
if after_eta == node:
|
|
1230
|
+
return after_eta
|
|
1231
|
+
node = after_eta
|
|
1232
|
+
raise ReductionLimitError(
|
|
1233
|
+
f"beta-eta normalization exceeded {BETA_ETA_ROUND_LIMIT} rounds; "
|
|
1234
|
+
"term may not be strongly normalizing."
|
|
1235
|
+
)
|
|
1236
|
+
|
|
1237
|
+
|
|
1238
|
+
# =========================
|
|
1239
|
+
# Lambda scope resolution
|
|
1240
|
+
# =========================
|
|
1241
|
+
|
|
1242
|
+
def _resolve(node: Node, bound: frozenset) -> Node:
|
|
1243
|
+
"""Top-down resolver threading the frozenset of currently lambda-bound names."""
|
|
1244
|
+
if isinstance(node, Variable):
|
|
1245
|
+
return LambdaVar(node.name) if node.name in bound else node
|
|
1246
|
+
if isinstance(node, PredicateTerm):
|
|
1247
|
+
# A predicate name in ARGUMENT position, under a lambda binder of
|
|
1248
|
+
# the same name: the binder captures it exactly as it captures an
|
|
1249
|
+
# individual variable.
|
|
1250
|
+
return LambdaVar(node.name) if node.name in bound else node
|
|
1251
|
+
if isinstance(node, (LambdaVar, Constant, Number, SortedConstant)):
|
|
1252
|
+
return node
|
|
1253
|
+
if isinstance(node, Lambda):
|
|
1254
|
+
return Lambda(node.param, _resolve(node.body, bound | {node.param.name}))
|
|
1255
|
+
if isinstance(node, Quantifier):
|
|
1256
|
+
# quantifier shadows any outer lambda of the same name — remove from bound set
|
|
1257
|
+
return Quantifier(node.type, node.variable,
|
|
1258
|
+
_resolve(node.formula, bound - {node.variable.name}))
|
|
1259
|
+
if isinstance(node, SortedQuantifier):
|
|
1260
|
+
return SortedQuantifier(node.type, node.variable, node.sort,
|
|
1261
|
+
_resolve(node.formula, bound - {node.variable.name}))
|
|
1262
|
+
if isinstance(node, (Count, Cardinality, SortedCount, SortedCardinality,
|
|
1263
|
+
SlashedExists)):
|
|
1264
|
+
# Counting / cardinality / slashed binders shadow an outer lambda of the
|
|
1265
|
+
# same name, exactly like a quantifier (replace() keeps op/n/sort/slash).
|
|
1266
|
+
return replace(node, formula=_resolve(node.formula, bound - {node.variable.name}))
|
|
1267
|
+
if isinstance(node, Atom):
|
|
1268
|
+
resolved_args = tuple(_resolve(a, bound) for a in node.args)
|
|
1269
|
+
if node.predicate in bound:
|
|
1270
|
+
result: Node = LambdaVar(node.predicate)
|
|
1271
|
+
for arg in resolved_args:
|
|
1272
|
+
result = Application(result, arg)
|
|
1273
|
+
return result # zero-arg → bare LambdaVar; n-arg → left-nested Application
|
|
1274
|
+
return Atom(node.predicate, resolved_args)
|
|
1275
|
+
if isinstance(node, Function):
|
|
1276
|
+
# Function names can be lambda-bound (e.g. λfoo. P(foo(x)) parses body as
|
|
1277
|
+
# Atom("P", [Function("foo", ...)]) because NAME "(" termlist ")" → function_).
|
|
1278
|
+
resolved_args = tuple(_resolve(a, bound) for a in node.args)
|
|
1279
|
+
if node.name in bound:
|
|
1280
|
+
result = LambdaVar(node.name)
|
|
1281
|
+
for arg in resolved_args:
|
|
1282
|
+
result = Application(result, arg)
|
|
1283
|
+
return result
|
|
1284
|
+
return Function(node.name, resolved_args)
|
|
1285
|
+
if not is_dataclass(node):
|
|
1286
|
+
raise TypeError(f"resolve_lambda_scope: unknown node type {type(node).__name__}")
|
|
1287
|
+
# Structural: Not, the binary connectives, and Application introduce no
|
|
1288
|
+
# binders — recurse into every child with the same bound set.
|
|
1289
|
+
return node.map_children(lambda c: _resolve(c, bound))
|
|
1290
|
+
|
|
1291
|
+
|
|
1292
|
+
def resolve_lambda_scope(node: Node) -> Node:
|
|
1293
|
+
"""Rewrite body occurrences of lambda-bound names using lexical scope.
|
|
1294
|
+
|
|
1295
|
+
After parsing, lambda parameters are LambdaVar but body occurrences keep
|
|
1296
|
+
their default parse types (Variable for single-letter params, Atom for
|
|
1297
|
+
predicate-class params). This pass performs two rewrites driven by the
|
|
1298
|
+
current lambda-bound set:
|
|
1299
|
+
|
|
1300
|
+
1. Variable(name) whose name is lambda-bound → LambdaVar(name).
|
|
1301
|
+
2. Atom(pred, args) or Function(name, args) whose pred/name is lambda-bound
|
|
1302
|
+
→ left-nested curried Application over LambdaVar(pred/name) and the
|
|
1303
|
+
recursively resolved args. Zero args → bare LambdaVar.
|
|
1304
|
+
|
|
1305
|
+
Scope rules — innermost binder wins:
|
|
1306
|
+
|
|
1307
|
+
- Lambda(p, body): p.name is ADDED to the bound set for body.
|
|
1308
|
+
- Quantifier / SortedQuantifier(_, v, _, body): v.name is REMOVED from the
|
|
1309
|
+
bound set for body. The quantifier shadows any outer lambda of the same
|
|
1310
|
+
name; inside the quantifier, the name is logical (Variable), not lambda-bound.
|
|
1311
|
+
|
|
1312
|
+
Returns a new Node; the input is never mutated.
|
|
1313
|
+
"""
|
|
1314
|
+
return _resolve(node, frozenset())
|
|
1315
|
+
|
|
1316
|
+
|
|
1317
|
+
# =========================
|
|
1318
|
+
# Unicode rendering (parser round-trip)
|
|
1319
|
+
# =========================
|
|
1320
|
+
#
|
|
1321
|
+
# These functions render any node back to a Unicode formula string that, when
|
|
1322
|
+
# re-parsed in the matching MSFLParser mode, yields a structurally equal AST.
|
|
1323
|
+
# Dispatch is by class name so this single block covers both the FOL nodes
|
|
1324
|
+
# (from _fol_nodes.py) and the MSFL/lambda nodes defined above.
|
|
1325
|
+
#
|
|
1326
|
+
# The regular formula operators (every connective/modal that the precedence-driven
|
|
1327
|
+
# renderers below format) self-register an OperatorSpec in OPERATORS next to their
|
|
1328
|
+
# class definition. The renderers read OPERATORS at call time, so adding an
|
|
1329
|
+
# operator needs NO edit here. The fixed entries below are the NON-operator
|
|
1330
|
+
# nodes the renderers still special-case explicitly — Lambda/Application and the
|
|
1331
|
+
# three quantifier binders — together with their formula precedences.
|
|
1332
|
+
#
|
|
1333
|
+
# Formula precedence — higher binds tighter — mirrors the grammar layering
|
|
1334
|
+
# (biimplication < implication < same-level binary < prefix < atomic). Operators
|
|
1335
|
+
# carry their precedence in their spec; these are the base (non-operator) entries:
|
|
1336
|
+
_UNI_BASE_PREC = {
|
|
1337
|
+
"Lambda": 0, "Application": 0,
|
|
1338
|
+
"Quantifier": 4, "SortedQuantifier": 4, "SecondOrderQuantifier": 4,
|
|
1339
|
+
"Count": 4, "SortedCount": 4, "SlashedExists": 4,
|
|
1340
|
+
# PAL announcements bind like a prefix operator (their post-formula slot is
|
|
1341
|
+
# a prefix-level operand); they are not in the operator registry because no
|
|
1342
|
+
# registered fixity can express the bracket-delimited two-argument shape.
|
|
1343
|
+
"Announce": 4, "AnnounceDiamond": 4,
|
|
1344
|
+
# Group-epistemic operators E_{…}/D_{…}/C_{…} are prefix operators over a
|
|
1345
|
+
# variable-length agent list, which no registered fixity expresses either.
|
|
1346
|
+
"EverybodyKnows": 4, "DistributedKnowledge": 4, "CommonKnowledge": 4,
|
|
1347
|
+
# The ↓ binder: same binding tightness as a plain quantifier (it IS one,
|
|
1348
|
+
# structurally — see fol._hybrid_nodes.Down's docstring).
|
|
1349
|
+
"Down": 4,
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
# The same-level binary group (∧ ∨ ⊗ ⊕, grammar precedence 3) is identified by
|
|
1353
|
+
# its registered fixity == 'level2' — see the dispatch in _uni()/_latex(), which
|
|
1354
|
+
# reads it straight off the operator's spec. Membership is therefore derived from
|
|
1355
|
+
# the registry: a new same-level operator needs no edit here. Such operators
|
|
1356
|
+
# cannot be mixed without parentheses, and chains are left-folded.
|
|
1357
|
+
|
|
1358
|
+
_UNI_INFIX_COMPARE = frozenset({"=", "≠", "<", ">", "≤", "≥"})
|
|
1359
|
+
_UNI_ARITH_OPS = frozenset({"+", "-", "*", "/"})
|
|
1360
|
+
|
|
1361
|
+
#: The two truth constants (the nullary atoms ``$true`` / ``$false``, and the nullary
|
|
1362
|
+
#: atoms named like their glyphs, which are the same constants) as
|
|
1363
|
+
#: ``(unicode glyph, LaTeX command)``.
|
|
1364
|
+
_TRUTH_GLYPH = {"$true": ("⊤", "\\top"), "$false": ("⊥", "\\bot"),
|
|
1365
|
+
"⊤": ("⊤", "\\top"), "⊥": ("⊥", "\\bot")}
|
|
1366
|
+
|
|
1367
|
+
|
|
1368
|
+
def _uni_prec(node) -> float:
|
|
1369
|
+
"""Formula precedence of a node; atomic nodes (atoms, terms) default to 5.
|
|
1370
|
+
|
|
1371
|
+
Regular operators read their precedence from the registry; the binders,
|
|
1372
|
+
Lambda and Application fall back to the fixed base table; anything else
|
|
1373
|
+
(atoms, terms) is atomic at 5.
|
|
1374
|
+
"""
|
|
1375
|
+
cls = type(node).__name__
|
|
1376
|
+
spec = OPERATORS.get(cls)
|
|
1377
|
+
if spec is not None:
|
|
1378
|
+
return spec.precedence
|
|
1379
|
+
return _UNI_BASE_PREC.get(cls, 5)
|
|
1380
|
+
|
|
1381
|
+
|
|
1382
|
+
def _uni_wrap(node, min_prec: float) -> str:
|
|
1383
|
+
"""Render node, parenthesising it when it binds looser than the slot allows."""
|
|
1384
|
+
s = _uni(node)
|
|
1385
|
+
return f"({s})" if _uni_prec(node) < min_prec else s
|
|
1386
|
+
|
|
1387
|
+
|
|
1388
|
+
def _uni_level2_child(node, parent_cls: str, side: str) -> str:
|
|
1389
|
+
"""Render a same-level (∧ ∨ ⊗ ⊕) operand with no-mixing / left-assoc parens.
|
|
1390
|
+
|
|
1391
|
+
Left operand: a same-class chain stays flat (a ∧ b ∧ c); a different
|
|
1392
|
+
same-level operator is parenthesised (no silent mixing). Right operand:
|
|
1393
|
+
any same-level node is parenthesised, since the parser left-folds chains.
|
|
1394
|
+
"""
|
|
1395
|
+
s = _uni(node)
|
|
1396
|
+
p = _uni_prec(node)
|
|
1397
|
+
if side == "left":
|
|
1398
|
+
need = p < 3 or (p == 3 and type(node).__name__ != parent_cls)
|
|
1399
|
+
else:
|
|
1400
|
+
need = p < 4
|
|
1401
|
+
return f"({s})" if need else s
|
|
1402
|
+
|
|
1403
|
+
|
|
1404
|
+
def _uni_atom(node) -> str:
|
|
1405
|
+
"""Render an Atom: infix comparison, nullary predicate, or applied predicate."""
|
|
1406
|
+
if node.predicate in _UNI_INFIX_COMPARE and len(node.args) == 2:
|
|
1407
|
+
return f"{_uni_term(node.args[0])} {node.predicate} {_uni_term(node.args[1])}"
|
|
1408
|
+
if not node.args and node.predicate in _TRUTH_GLYPH:
|
|
1409
|
+
# The truth constants, which every unicode grammar mode reads back.
|
|
1410
|
+
return _TRUTH_GLYPH[node.predicate][0]
|
|
1411
|
+
if not node.args:
|
|
1412
|
+
return node.predicate
|
|
1413
|
+
return f"{node.predicate}(" + ", ".join(_uni_term(a) for a in node.args) + ")"
|
|
1414
|
+
|
|
1415
|
+
|
|
1416
|
+
def _uni_term_prec(node) -> int:
|
|
1417
|
+
"""Arithmetic term precedence: + - → 1, * / → 2, everything atomic → 3."""
|
|
1418
|
+
if (type(node).__name__ == "Function"
|
|
1419
|
+
and node.name in _UNI_ARITH_OPS and len(node.args) == 2):
|
|
1420
|
+
return 2 if node.name in ("*", "/") else 1
|
|
1421
|
+
return 3
|
|
1422
|
+
|
|
1423
|
+
|
|
1424
|
+
def _uni_term_wrap(node, parent_prec: int, is_right: bool) -> str:
|
|
1425
|
+
"""Render an arithmetic operand, parenthesising per left-associative precedence."""
|
|
1426
|
+
s = _uni_term(node)
|
|
1427
|
+
p = _uni_term_prec(node)
|
|
1428
|
+
need = p < parent_prec or (p == parent_prec and is_right)
|
|
1429
|
+
return f"({s})" if need else s
|
|
1430
|
+
|
|
1431
|
+
|
|
1432
|
+
def _uni_spine(node):
|
|
1433
|
+
"""Uncurry a left-nested Application into (head, [arg0, arg1, …])."""
|
|
1434
|
+
args = []
|
|
1435
|
+
n = node
|
|
1436
|
+
while isinstance(n, Application):
|
|
1437
|
+
args.append(n.arg)
|
|
1438
|
+
n = n.func
|
|
1439
|
+
args.reverse()
|
|
1440
|
+
return n, args
|
|
1441
|
+
|
|
1442
|
+
|
|
1443
|
+
#: Whether the rendering in progress writes every constant by its bare name (the key
|
|
1444
|
+
#: of an atom, see :func:`key_text`) instead of by the text that reads back as it. A
|
|
1445
|
+
#: context variable and not a parameter: a rendering recurses through methods of
|
|
1446
|
+
#: several node modules (modal, linear, ...) that call ``to_unicode_str()`` on their
|
|
1447
|
+
#: children, and a parameter would be lost there.
|
|
1448
|
+
_BARE_CONSTANTS = contextvars.ContextVar("unicode_logic_kit_bare_constants", default=False)
|
|
1449
|
+
|
|
1450
|
+
|
|
1451
|
+
def key_text(node) -> str:
|
|
1452
|
+
"""The text of ``node`` used as a KEY: ``to_unicode_str()`` with every constant
|
|
1453
|
+
written by its bare name.
|
|
1454
|
+
|
|
1455
|
+
``Node.to_unicode_str()`` is the text of a formula, and a constant whose name
|
|
1456
|
+
does not read back bare is written in quotes there (``P('a')``). A route that
|
|
1457
|
+
names an atom, a model entry, a sort order or a target identifier by the printed
|
|
1458
|
+
text of an atom needs the name of the constant instead, as it was before the
|
|
1459
|
+
quoted form existed: a user types ``"P(a)"`` as the key of the atom ``P`` over
|
|
1460
|
+
the domain element ``a``, and the identifier of a target is derived from that
|
|
1461
|
+
text. Use this function wherever the text is stored, looked up, compared,
|
|
1462
|
+
sorted, hashed or turned into a name; use ``to_unicode_str()`` wherever it is
|
|
1463
|
+
shown to a person as a formula or handed to the parser.
|
|
1464
|
+
|
|
1465
|
+
The two renderings are one code path and differ ONLY in the text of
|
|
1466
|
+
``Constant`` and ``SortedConstant`` names, so for a node whose constants all
|
|
1467
|
+
read back bare they are the same string. Here a name is written as it is and is
|
|
1468
|
+
never refused (the empty name is written as nothing, as it always was).
|
|
1469
|
+
|
|
1470
|
+
The setting is a context variable that is restored when the call ends, however it
|
|
1471
|
+
ends, so a rendering that is running is not disturbed by a call to this
|
|
1472
|
+
function, nor this call by a rendering.
|
|
1473
|
+
"""
|
|
1474
|
+
token = _BARE_CONSTANTS.set(True)
|
|
1475
|
+
try:
|
|
1476
|
+
return node.to_unicode_str()
|
|
1477
|
+
finally:
|
|
1478
|
+
_BARE_CONSTANTS.reset(token)
|
|
1479
|
+
|
|
1480
|
+
|
|
1481
|
+
def _constant_name_text(name) -> str:
|
|
1482
|
+
"""The text of the name of a constant in the rendering that is running."""
|
|
1483
|
+
return name if _BARE_CONSTANTS.get() else constant_text(name)
|
|
1484
|
+
|
|
1485
|
+
|
|
1486
|
+
def _uni_term(node) -> str:
|
|
1487
|
+
"""Render a node occurring in term (argument) position.
|
|
1488
|
+
|
|
1489
|
+
Higher-order applications produced by scope resolution (e.g. foo(x) under
|
|
1490
|
+
λfoo, parsed as a Function then rewritten to Application(LambdaVar, …)) are
|
|
1491
|
+
rendered back as function-call syntax so they re-parse and re-resolve to the
|
|
1492
|
+
same node.
|
|
1493
|
+
|
|
1494
|
+
A ``Constant`` or ``SortedConstant`` is written by ``constant_text``: bare when
|
|
1495
|
+
its name reads back as that constant, in quotes when it does not (``'k2'``,
|
|
1496
|
+
``'Alice'``, ``'G-910'``), and a name that has no text is refused. ``key_text``
|
|
1497
|
+
switches this one place to the bare name. A variable, a lambda variable, a
|
|
1498
|
+
predicate term and the head of a function or of an application keep printing
|
|
1499
|
+
their name as it is.
|
|
1500
|
+
"""
|
|
1501
|
+
cls = type(node).__name__
|
|
1502
|
+
if cls == "Constant":
|
|
1503
|
+
return _constant_name_text(node.name)
|
|
1504
|
+
if cls in ("Variable", "LambdaVar", "PredicateTerm"):
|
|
1505
|
+
return node.name
|
|
1506
|
+
if cls == "Number":
|
|
1507
|
+
return _number_text(node.value)
|
|
1508
|
+
if cls == "SortedConstant":
|
|
1509
|
+
return f"{_constant_name_text(node.name)}:{node.sort}"
|
|
1510
|
+
if cls == "Measure":
|
|
1511
|
+
# μ(entity, dimension) — a measure-function term.
|
|
1512
|
+
return f"μ({_uni_term(node.entity)}, {_uni_term(node.dimension)})"
|
|
1513
|
+
if cls == "Cardinality":
|
|
1514
|
+
# |{v : φ}| — a set-cardinality term; φ renders at the full formula level.
|
|
1515
|
+
return "|{" + node.variable.name + " : " + _uni(node.formula) + "}|"
|
|
1516
|
+
if cls == "SortedCardinality":
|
|
1517
|
+
# |{v:S : φ}| — a sort-restricted set-cardinality term.
|
|
1518
|
+
return ("|{" + node.variable.name + ":" + node.sort + " : "
|
|
1519
|
+
+ _uni(node.formula) + "}|")
|
|
1520
|
+
if cls == "Function":
|
|
1521
|
+
if node.name in _UNI_ARITH_OPS and len(node.args) == 2:
|
|
1522
|
+
p = _uni_term_prec(node)
|
|
1523
|
+
left = _uni_term_wrap(node.args[0], p, is_right=False)
|
|
1524
|
+
right = _uni_term_wrap(node.args[1], p, is_right=True)
|
|
1525
|
+
return f"{left} {node.name} {right}"
|
|
1526
|
+
return f"{node.name}(" + ", ".join(_uni_term(a) for a in node.args) + ")"
|
|
1527
|
+
if cls == "Application":
|
|
1528
|
+
head, args = _uni_spine(node)
|
|
1529
|
+
if isinstance(head, (LambdaVar, Variable, Constant)) and args:
|
|
1530
|
+
return f"{head.name}(" + ", ".join(_uni_term(a) for a in args) + ")"
|
|
1531
|
+
return f"({_uni(node.func)})({_uni(node.arg)})"
|
|
1532
|
+
# Atoms / other formula nodes are not valid terms; best-effort fall-through.
|
|
1533
|
+
return _uni(node)
|
|
1534
|
+
|
|
1535
|
+
|
|
1536
|
+
def _uni(node) -> str:
|
|
1537
|
+
"""Render node as a formula-level Unicode string (no surrounding parens)."""
|
|
1538
|
+
cls = type(node).__name__
|
|
1539
|
+
|
|
1540
|
+
if cls in ("Variable", "LambdaVar", "Constant", "Number", "SortedConstant",
|
|
1541
|
+
"Function", "Measure", "Cardinality", "SortedCardinality",
|
|
1542
|
+
"PredicateTerm"):
|
|
1543
|
+
return _uni_term(node)
|
|
1544
|
+
if cls == "Atom":
|
|
1545
|
+
return _uni_atom(node)
|
|
1546
|
+
|
|
1547
|
+
# Regular operators are driven entirely by the registry: the spec's fixity
|
|
1548
|
+
# selects the operand arrangement and spec.unicode supplies the glyph/prefix.
|
|
1549
|
+
spec = OPERATORS.get(cls)
|
|
1550
|
+
if spec is not None:
|
|
1551
|
+
fix = spec.fixity
|
|
1552
|
+
if fix == "prefix":
|
|
1553
|
+
# Prefix (¬ and the prefix modal/temporal ops) bind like ¬: operand
|
|
1554
|
+
# wrapped at the prefix level.
|
|
1555
|
+
return spec.unicode + _uni_wrap(node.formula, 4)
|
|
1556
|
+
if fix == "agent_prefix":
|
|
1557
|
+
# K_<agent> / B_<agent>: glyph, agent's name, space, then wrapped operand.
|
|
1558
|
+
# The agent is a term (Variable/Constant); a bare string is also accepted.
|
|
1559
|
+
agent = node.agent
|
|
1560
|
+
agent = agent if isinstance(agent, str) else getattr(agent, "name", None) or agent.to_unicode_str()
|
|
1561
|
+
return f"{spec.unicode}{agent} " + _uni_wrap(node.formula, 4)
|
|
1562
|
+
if fix == "binary_until":
|
|
1563
|
+
# Ⓤ right-assoc: left slot same_level_ops (≥3), right slot until (≥2.5).
|
|
1564
|
+
return f"{_uni_wrap(node.left, 3)} {spec.unicode} {_uni_wrap(node.right, 2.5)}"
|
|
1565
|
+
if fix == "binary_iff":
|
|
1566
|
+
# ↔ right-assoc: left slot is implication (≥2), right slot biimplication (≥1)
|
|
1567
|
+
return f"{_uni_wrap(node.left, 2)} {spec.unicode} {_uni_wrap(node.right, 1)}"
|
|
1568
|
+
if fix == "binary_implies":
|
|
1569
|
+
# → right-assoc: left slot same_level_ops (≥3), right slot implication (≥2)
|
|
1570
|
+
return f"{_uni_wrap(node.left, 3)} {spec.unicode} {_uni_wrap(node.right, 2)}"
|
|
1571
|
+
if fix == "level2":
|
|
1572
|
+
left = _uni_level2_child(node.left, cls, "left")
|
|
1573
|
+
right = _uni_level2_child(node.right, cls, "right")
|
|
1574
|
+
return f"{left} {spec.unicode} {right}"
|
|
1575
|
+
|
|
1576
|
+
if cls == "Quantifier":
|
|
1577
|
+
return f"{node.type}{node.variable.name} " + _uni_wrap(node.formula, 4)
|
|
1578
|
+
if cls == "Count":
|
|
1579
|
+
# ∃≥n / ∃≤n / ∃=n x body (binds as tightly as a quantifier).
|
|
1580
|
+
return (f"{_COUNT_OPS[node.op]}{node.n.value} {node.variable.name} "
|
|
1581
|
+
+ _uni_wrap(node.formula, 4))
|
|
1582
|
+
if cls == "SortedCount":
|
|
1583
|
+
# ∃≥n / ∃≤n / ∃=n x:S body (sorted counting quantifier).
|
|
1584
|
+
return (f"{_COUNT_OPS[node.op]}{node.n.value} {node.variable.name}:{node.sort} "
|
|
1585
|
+
+ _uni_wrap(node.formula, 4))
|
|
1586
|
+
if cls == "SlashedExists":
|
|
1587
|
+
# ∃x/{y, z} body (IF-logic slashed existential; binds like a quantifier).
|
|
1588
|
+
return (f"∃{node.variable.name}/{{{', '.join(node.slashed)}}} "
|
|
1589
|
+
+ _uni_wrap(node.formula, 4))
|
|
1590
|
+
if cls == "Nominal":
|
|
1591
|
+
# A hybrid nominal renders as its bare (NAME-legal) name.
|
|
1592
|
+
return node.name
|
|
1593
|
+
if cls == "Down":
|
|
1594
|
+
# ↓x.body (binds as tightly as a quantifier — see _UNI_BASE_PREC; the
|
|
1595
|
+
# "." separator mirrors the grammar and Lambda's own "λx. body").
|
|
1596
|
+
return f"↓{node.variable.name}." + _uni_wrap(node.formula, 4)
|
|
1597
|
+
if cls == "One":
|
|
1598
|
+
# The multiplicative unit of the linear mode.
|
|
1599
|
+
return "𝟙"
|
|
1600
|
+
if cls == "Top":
|
|
1601
|
+
# The additive truth of the linear mode.
|
|
1602
|
+
return "⊤"
|
|
1603
|
+
if cls == "Zero":
|
|
1604
|
+
# The additive falsity of the linear mode.
|
|
1605
|
+
return "𝟘"
|
|
1606
|
+
if cls in ("Announce", "AnnounceDiamond",
|
|
1607
|
+
"EverybodyKnows", "DistributedKnowledge", "CommonKnowledge"):
|
|
1608
|
+
# PAL announcements ([φ!]ψ / ⟨φ!⟩ψ) and the group-epistemic prefixes
|
|
1609
|
+
# (E_{a,b} φ …) render via their own methods; precedence for the
|
|
1610
|
+
# enclosing slot comes from _UNI_BASE_PREC above.
|
|
1611
|
+
return node.to_unicode_str()
|
|
1612
|
+
if cls == "Dependence":
|
|
1613
|
+
# The dependence atom =(t1, …, tn); terms render at the term level.
|
|
1614
|
+
return "=(" + ", ".join(_uni_term(a) for a in node.args) + ")"
|
|
1615
|
+
if cls == "SortedQuantifier":
|
|
1616
|
+
return f"{node.type}{node.variable.name}:{node.sort} " + _uni_wrap(node.formula, 4)
|
|
1617
|
+
if cls == "SecondOrderQuantifier":
|
|
1618
|
+
# Arity is NOT printed: it is re-inferred from the body on re-parse, so
|
|
1619
|
+
# printing it would break the round-trip. e.g. ∀P P(x, y).
|
|
1620
|
+
return f"{node.type}{node.predicate} " + _uni_wrap(node.formula, 4)
|
|
1621
|
+
|
|
1622
|
+
if cls == "Lambda":
|
|
1623
|
+
# Body extends rightward through the whole formula; never wrapped here.
|
|
1624
|
+
return f"λ{node.param.name}. " + _uni(node.body)
|
|
1625
|
+
if cls == "Application":
|
|
1626
|
+
# (func)(arg): both sides are delimited by parens in the grammar.
|
|
1627
|
+
return f"({_uni(node.func)})({_uni(node.arg)})"
|
|
1628
|
+
|
|
1629
|
+
raise TypeError(f"to_unicode_str: unknown node type {cls}")
|
|
1630
|
+
|
|
1631
|
+
|
|
1632
|
+
# =========================
|
|
1633
|
+
# LaTeX rendering
|
|
1634
|
+
# =========================
|
|
1635
|
+
#
|
|
1636
|
+
# Mirrors the Unicode renderer above but emits LaTeX math-mode markup. It reuses
|
|
1637
|
+
# the same precedence machinery (the operator registry and _uni_prec) so the
|
|
1638
|
+
# parenthesisation is identical; only the operator markup (spec.latex) and term
|
|
1639
|
+
# formatting differ. Output is not parseable by MSFLParser (so no round-trip),
|
|
1640
|
+
# hence tests assert on exact strings.
|
|
1641
|
+
|
|
1642
|
+
# The LaTeX markup for every regular operator now lives in its OperatorSpec.latex
|
|
1643
|
+
# (read at call time below), so the per-operator LaTeX glyph tables are gone.
|
|
1644
|
+
# Only the term-level / quantifier-level tables — which are NOT driven by the
|
|
1645
|
+
# operator registry — remain here.
|
|
1646
|
+
|
|
1647
|
+
_LATEX_COMPARE = {
|
|
1648
|
+
"=": "=", "≠": "\\neq", "<": "<", ">": ">", "≤": "\\leq", "≥": "\\geq",
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
_LATEX_ARITH = {"+": "+", "-": "-", "*": "\\cdot", "/": "/"}
|
|
1652
|
+
|
|
1653
|
+
_LATEX_QUANT = {"∀": "\\forall", "forall": "\\forall", "∃": "\\exists", "exists": "\\exists"}
|
|
1654
|
+
|
|
1655
|
+
|
|
1656
|
+
def _latex_escape(name: str) -> str:
|
|
1657
|
+
"""Escape LaTeX math-mode specials in a verbatim symbol name.
|
|
1658
|
+
|
|
1659
|
+
Only the underscore can occur in a grammar-produced name (a c_-prefixed
|
|
1660
|
+
constant such as ``c_zero``); left bare it would be read as the subscript
|
|
1661
|
+
operator, so it is backslash-escaped. Other name classes (variables,
|
|
1662
|
+
predicates, functions, sorts) cannot contain LaTeX specials.
|
|
1663
|
+
"""
|
|
1664
|
+
return name.replace("_", "\\_")
|
|
1665
|
+
|
|
1666
|
+
|
|
1667
|
+
def _latex_wrap(node, min_prec: float) -> str:
|
|
1668
|
+
s = _latex(node)
|
|
1669
|
+
return f"({s})" if _uni_prec(node) < min_prec else s
|
|
1670
|
+
|
|
1671
|
+
|
|
1672
|
+
def _latex_level2_child(node, parent_cls: str, side: str) -> str:
|
|
1673
|
+
s = _latex(node)
|
|
1674
|
+
p = _uni_prec(node)
|
|
1675
|
+
if side == "left":
|
|
1676
|
+
need = p < 3 or (p == 3 and type(node).__name__ != parent_cls)
|
|
1677
|
+
else:
|
|
1678
|
+
need = p < 4
|
|
1679
|
+
return f"({s})" if need else s
|
|
1680
|
+
|
|
1681
|
+
|
|
1682
|
+
def _latex_term(node) -> str:
|
|
1683
|
+
cls = type(node).__name__
|
|
1684
|
+
if cls in ("Variable", "LambdaVar", "Constant", "PredicateTerm"):
|
|
1685
|
+
return _latex_escape(node.name)
|
|
1686
|
+
if cls == "Number":
|
|
1687
|
+
return _number_text(node.value)
|
|
1688
|
+
if cls == "SortedConstant":
|
|
1689
|
+
return f"{_latex_escape(node.name)}{{:}}\\mathrm{{{node.sort}}}"
|
|
1690
|
+
if cls == "Measure":
|
|
1691
|
+
return f"\\mu({_latex_term(node.entity)}, {_latex_term(node.dimension)})"
|
|
1692
|
+
if cls == "Cardinality":
|
|
1693
|
+
return ("\\lvert\\{" + _latex_escape(node.variable.name) + " : "
|
|
1694
|
+
+ _latex(node.formula) + "\\}\\rvert")
|
|
1695
|
+
if cls == "SortedCardinality":
|
|
1696
|
+
return ("\\lvert\\{" + _latex_escape(node.variable.name)
|
|
1697
|
+
+ "{:}\\mathrm{" + node.sort + "} : "
|
|
1698
|
+
+ _latex(node.formula) + "\\}\\rvert")
|
|
1699
|
+
if cls == "Function":
|
|
1700
|
+
if node.name in _UNI_ARITH_OPS and len(node.args) == 2:
|
|
1701
|
+
p = _uni_term_prec(node)
|
|
1702
|
+
left = node.args[0]
|
|
1703
|
+
right = node.args[1]
|
|
1704
|
+
ls = _latex_term(left)
|
|
1705
|
+
rs = _latex_term(right)
|
|
1706
|
+
if _uni_term_prec(left) < p:
|
|
1707
|
+
ls = f"({ls})"
|
|
1708
|
+
if _uni_term_prec(right) < p or (_uni_term_prec(right) == p):
|
|
1709
|
+
rs = f"({rs})"
|
|
1710
|
+
return f"{ls} {_LATEX_ARITH[node.name]} {rs}"
|
|
1711
|
+
return f"{node.name}(" + ", ".join(_latex_term(a) for a in node.args) + ")"
|
|
1712
|
+
if cls == "Application":
|
|
1713
|
+
head, args = _uni_spine(node)
|
|
1714
|
+
if isinstance(head, (LambdaVar, Variable, Constant)) and args:
|
|
1715
|
+
return f"{head.name}(" + ", ".join(_latex_term(a) for a in args) + ")"
|
|
1716
|
+
return f"({_latex(node.func)})({_latex(node.arg)})"
|
|
1717
|
+
return _latex(node)
|
|
1718
|
+
|
|
1719
|
+
|
|
1720
|
+
def _latex_atom(node) -> str:
|
|
1721
|
+
if node.predicate in _UNI_INFIX_COMPARE and len(node.args) == 2:
|
|
1722
|
+
return f"{_latex_term(node.args[0])} {_LATEX_COMPARE[node.predicate]} {_latex_term(node.args[1])}"
|
|
1723
|
+
if not node.args and node.predicate in _TRUTH_GLYPH:
|
|
1724
|
+
return _TRUTH_GLYPH[node.predicate][1]
|
|
1725
|
+
if not node.args:
|
|
1726
|
+
return node.predicate
|
|
1727
|
+
return f"{node.predicate}(" + ", ".join(_latex_term(a) for a in node.args) + ")"
|
|
1728
|
+
|
|
1729
|
+
|
|
1730
|
+
def _latex(node) -> str:
|
|
1731
|
+
"""Render node as a LaTeX math-mode string (no surrounding parens)."""
|
|
1732
|
+
cls = type(node).__name__
|
|
1733
|
+
|
|
1734
|
+
if cls in ("Variable", "LambdaVar", "Constant", "Number", "SortedConstant",
|
|
1735
|
+
"Function", "Measure", "Cardinality", "SortedCardinality",
|
|
1736
|
+
"PredicateTerm"):
|
|
1737
|
+
return _latex_term(node)
|
|
1738
|
+
if cls == "Atom":
|
|
1739
|
+
return _latex_atom(node)
|
|
1740
|
+
|
|
1741
|
+
# Regular operators are driven entirely by the registry: the spec's fixity
|
|
1742
|
+
# selects the operand arrangement and spec.latex supplies the markup (already
|
|
1743
|
+
# including any trailing space for the prefix forms).
|
|
1744
|
+
spec = OPERATORS.get(cls)
|
|
1745
|
+
if spec is not None:
|
|
1746
|
+
fix = spec.fixity
|
|
1747
|
+
if fix == "prefix":
|
|
1748
|
+
return spec.latex + _latex_wrap(node.formula, 4)
|
|
1749
|
+
if fix == "agent_prefix":
|
|
1750
|
+
agent = node.agent
|
|
1751
|
+
agent = agent if isinstance(agent, str) else getattr(agent, "name", None) or agent.to_unicode_str()
|
|
1752
|
+
return f"{spec.latex}_{{{agent}}} " + _latex_wrap(node.formula, 4)
|
|
1753
|
+
if fix == "binary_until":
|
|
1754
|
+
return f"{_latex_wrap(node.left, 3)} {spec.latex} {_latex_wrap(node.right, 2.5)}"
|
|
1755
|
+
if fix == "binary_iff":
|
|
1756
|
+
return f"{_latex_wrap(node.left, 2)} {spec.latex} {_latex_wrap(node.right, 1)}"
|
|
1757
|
+
if fix == "binary_implies":
|
|
1758
|
+
return f"{_latex_wrap(node.left, 3)} {spec.latex} {_latex_wrap(node.right, 2)}"
|
|
1759
|
+
if fix == "level2":
|
|
1760
|
+
left = _latex_level2_child(node.left, cls, "left")
|
|
1761
|
+
right = _latex_level2_child(node.right, cls, "right")
|
|
1762
|
+
return f"{left} {spec.latex} {right}"
|
|
1763
|
+
|
|
1764
|
+
if cls == "Quantifier":
|
|
1765
|
+
return f"{_LATEX_QUANT[node.type]} {node.variable.name}\\, " + _latex_wrap(node.formula, 4)
|
|
1766
|
+
if cls == "Count":
|
|
1767
|
+
rel = {"ge": "\\geq", "le": "\\leq", "eq": "="}[node.op]
|
|
1768
|
+
return (f"\\exists^{{{rel} {node.n.value}}} {node.variable.name}\\, "
|
|
1769
|
+
+ _latex_wrap(node.formula, 4))
|
|
1770
|
+
if cls == "SortedCount":
|
|
1771
|
+
rel = {"ge": "\\geq", "le": "\\leq", "eq": "="}[node.op]
|
|
1772
|
+
return (f"\\exists^{{{rel} {node.n.value}}} {node.variable.name}{{:}}\\mathrm{{{node.sort}}}\\, "
|
|
1773
|
+
+ _latex_wrap(node.formula, 4))
|
|
1774
|
+
if cls == "SlashedExists":
|
|
1775
|
+
slashed = ", ".join(_latex_escape(n) for n in node.slashed)
|
|
1776
|
+
return (f"\\exists {node.variable.name} / \\{{{slashed}\\}}\\, "
|
|
1777
|
+
+ _latex_wrap(node.formula, 4))
|
|
1778
|
+
if cls == "Nominal":
|
|
1779
|
+
return f"\\mathsf{{{_latex_escape(node.name)}}}"
|
|
1780
|
+
if cls == "Down":
|
|
1781
|
+
return (f"\\downarrow {_latex_escape(node.variable.name)}.\\, "
|
|
1782
|
+
+ _latex_wrap(node.formula, 4))
|
|
1783
|
+
if cls == "One":
|
|
1784
|
+
return "\\mathbf{1}"
|
|
1785
|
+
if cls == "Top":
|
|
1786
|
+
return "\\top"
|
|
1787
|
+
if cls == "Zero":
|
|
1788
|
+
return "\\mathbf{0}"
|
|
1789
|
+
if cls in ("Announce", "AnnounceDiamond",
|
|
1790
|
+
"EverybodyKnows", "DistributedKnowledge", "CommonKnowledge"):
|
|
1791
|
+
# PAL announcements and group-epistemic prefixes render via their own
|
|
1792
|
+
# LaTeX methods.
|
|
1793
|
+
return node.to_latex()
|
|
1794
|
+
if cls == "Dependence":
|
|
1795
|
+
return "{=}(" + ", ".join(_latex_term(a) for a in node.args) + ")"
|
|
1796
|
+
if cls == "SortedQuantifier":
|
|
1797
|
+
return (f"{_LATEX_QUANT[node.type]} {node.variable.name}{{:}}\\mathrm{{{node.sort}}}\\, "
|
|
1798
|
+
+ _latex_wrap(node.formula, 4))
|
|
1799
|
+
if cls == "SecondOrderQuantifier":
|
|
1800
|
+
# Arity is not rendered (mirrors the Unicode renderer). e.g. \forall P\, P(x).
|
|
1801
|
+
return f"{_LATEX_QUANT[node.type]} {node.predicate}\\, " + _latex_wrap(node.formula, 4)
|
|
1802
|
+
|
|
1803
|
+
if cls == "Lambda":
|
|
1804
|
+
return f"\\lambda {node.param.name}.\\, " + _latex(node.body)
|
|
1805
|
+
if cls == "Application":
|
|
1806
|
+
return f"({_latex(node.func)})({_latex(node.arg)})"
|
|
1807
|
+
|
|
1808
|
+
raise TypeError(f"to_latex: unknown node type {cls}")
|
|
1809
|
+
|
|
1810
|
+
|
|
1811
|
+
# =========================
|
|
1812
|
+
# Registry extension
|
|
1813
|
+
# =========================
|
|
1814
|
+
|
|
1815
|
+
NODE_CLASSES.update({
|
|
1816
|
+
"SortedQuantifier": SortedQuantifier,
|
|
1817
|
+
"SortedConstant": SortedConstant,
|
|
1818
|
+
"SortedCount": SortedCount,
|
|
1819
|
+
"SortedCardinality": SortedCardinality,
|
|
1820
|
+
"WeakConjunction": WeakConjunction,
|
|
1821
|
+
"WeakDisjunction": WeakDisjunction,
|
|
1822
|
+
"StrongConjunction": StrongConjunction,
|
|
1823
|
+
"StrongDisjunction": StrongDisjunction,
|
|
1824
|
+
"LukNegation": LukNegation,
|
|
1825
|
+
"LukImplication": LukImplication,
|
|
1826
|
+
"LukEquivalence": LukEquivalence,
|
|
1827
|
+
"LambdaVar": LambdaVar,
|
|
1828
|
+
"Lambda": Lambda,
|
|
1829
|
+
"Application": Application,
|
|
1830
|
+
})
|
|
1831
|
+
|
|
1832
|
+
|
|
1833
|
+
# =========================
|
|
1834
|
+
# MSFL Reductions
|
|
1835
|
+
# =========================
|
|
1836
|
+
|
|
1837
|
+
def _reduce_nl_nodes(n: Node) -> Node:
|
|
1838
|
+
"""Reduce the natural-language nodes with a classical reading to plain FOL.
|
|
1839
|
+
|
|
1840
|
+
``Contrast`` is truth-functionally conjunction (its own contract; the Z3 /
|
|
1841
|
+
Prover9 / TPTP exports already lower it as ``And``), and ``Count`` carries the
|
|
1842
|
+
standard distinct-witnesses expansion (:meth:`Count._expand`, the same one its
|
|
1843
|
+
first-order exports use). ``Cardinality`` is deliberately NOT reduced here —
|
|
1844
|
+
set cardinality has no first-order counterpart, so it must keep raising at the
|
|
1845
|
+
first-order boundary rather than being silently mistranslated.
|
|
1846
|
+
"""
|
|
1847
|
+
n = n.map_children(_reduce_nl_nodes)
|
|
1848
|
+
if isinstance(n, Contrast):
|
|
1849
|
+
return And(n.left, n.right)
|
|
1850
|
+
if isinstance(n, Count):
|
|
1851
|
+
# The expansion is built from the already-reduced matrix, but it wraps it
|
|
1852
|
+
# in fresh classical structure only, so one more pass is not needed.
|
|
1853
|
+
return n._expand()
|
|
1854
|
+
return n
|
|
1855
|
+
|
|
1856
|
+
|
|
1857
|
+
def subsort_axioms(signature: "Signature") -> Tuple[Node, ...]:
|
|
1858
|
+
"""Return one ``∀x (S(x) → T(x))`` sentence per DIRECT subsort edge ``S < T``.
|
|
1859
|
+
|
|
1860
|
+
The subsort counterpart of :func:`nonempty_sort_axioms`, under the SAME
|
|
1861
|
+
contract: a caller deciding sorted input against a
|
|
1862
|
+
:class:`~unicode_logic_kit.fol.signature.Signature` that declares subsorts
|
|
1863
|
+
adds these as their OWN extra, top-level, never-negated assumptions --
|
|
1864
|
+
premises for a validity/entailment check, extra conjuncts for a
|
|
1865
|
+
satisfiability check -- e.g.
|
|
1866
|
+
``api.prove(goal, [*premises, *subsort_axioms(sig)])``. They are never
|
|
1867
|
+
folded into :func:`to_fol`'s per-formula translation: an axiom conjoined
|
|
1868
|
+
onto the formula being PROVED becomes something the prover has to prove
|
|
1869
|
+
too, and ``∀x (S(x) → T(x))`` is not valid, so every validity check would
|
|
1870
|
+
come back refuted.
|
|
1871
|
+
|
|
1872
|
+
``S``/``T`` are the same 1-ary guard predicate names
|
|
1873
|
+
:meth:`SortedQuantifier._relativize` uses for sort membership, so the
|
|
1874
|
+
axioms constrain exactly the predicates a relativised formula reads,
|
|
1875
|
+
nothing else. Only DIRECT edges are emitted: a transitive edge ``S < U``
|
|
1876
|
+
via ``S < T < U`` follows by chaining the two implications. Deterministic
|
|
1877
|
+
``(child, parent)`` order; an empty tuple for a signature without
|
|
1878
|
+
subsorts. Duck-typed: only ``signature.subsorts`` is read.
|
|
1879
|
+
"""
|
|
1880
|
+
x = Variable("x")
|
|
1881
|
+
return tuple(Quantifier("∀", x, Implies(Atom(child, (x,)), Atom(parent, (x,))))
|
|
1882
|
+
for child in sorted(signature.subsorts)
|
|
1883
|
+
for parent in sorted(signature.subsorts[child]))
|
|
1884
|
+
|
|
1885
|
+
|
|
1886
|
+
def signature_axioms(signature: "Signature") -> Tuple[Node, ...]:
|
|
1887
|
+
"""Return everything a :class:`~unicode_logic_kit.fol.signature.Signature` asserts, as plain FOL sentences.
|
|
1888
|
+
|
|
1889
|
+
A signature that declares sorts, constant sorts, function ranks and subsort
|
|
1890
|
+
edges says more than a decision procedure reads from the formulas alone. This
|
|
1891
|
+
function writes those declarations down, in the vocabulary of the one-universe
|
|
1892
|
+
reading of sorts (a sort ``S`` is the non-empty extension of the unary
|
|
1893
|
+
predicate ``S``; see :func:`sort_membership_axioms`), so that a caller can add
|
|
1894
|
+
them to the premises of ``premises ⊨ goal`` and ask the question the
|
|
1895
|
+
signature asks. The sentences, in this order (each family sorted by name):
|
|
1896
|
+
|
|
1897
|
+
1. ``∃x S(x)`` for every sort the signature names: the sorts it lists, the
|
|
1898
|
+
sorts of its constants, the argument and result sorts of its functions
|
|
1899
|
+
and the argument sorts of its predicates, and both ends of a subsort edge.
|
|
1900
|
+
2. :func:`subsort_axioms`: ``∀x (S(x) → T(x))`` for every direct edge
|
|
1901
|
+
``S < T``.
|
|
1902
|
+
3. ``S(c)`` for every constant ``c`` declared with sort ``S``.
|
|
1903
|
+
4. ``∀x1 … ∀xn (S1(x1) ∧ … ∧ Sn(xn) → S(f(x1, …, xn)))`` for every function
|
|
1904
|
+
``f`` declared with result sort ``S`` and argument sorts ``S1 … Sn``. An
|
|
1905
|
+
argument position declared without a sort contributes no guard, a function
|
|
1906
|
+
declared without a result sort contributes no sentence, and a nullary
|
|
1907
|
+
function gives ``S(f)``.
|
|
1908
|
+
|
|
1909
|
+
A predicate's declared argument sorts give NO sentence. Under the one-universe
|
|
1910
|
+
reading a predicate is a relation over the whole universe and may hold of
|
|
1911
|
+
anything, whatever sort its arguments were declared with: ``P: A`` does not
|
|
1912
|
+
say ``∀x (P(x) → A(x))``, and a sentence saying so would be a different
|
|
1913
|
+
question. The relativisation of a sorted theory to plain first-order logic
|
|
1914
|
+
needs non-emptiness of the sorts and closure of the declared functions, and
|
|
1915
|
+
nothing about predicates.
|
|
1916
|
+
|
|
1917
|
+
The contract is that of :func:`nonempty_sort_axioms` and
|
|
1918
|
+
:func:`subsort_axioms`: the sentences are background facts that a caller adds
|
|
1919
|
+
as its own top-level, never-negated assumptions (premises for an entailment,
|
|
1920
|
+
extra asserted conjuncts for a satisfiability question). ``api.prove`` and
|
|
1921
|
+
``api.countermodel`` add them when given ``signature=``. Duck-typed: the
|
|
1922
|
+
``sorts``, ``predicates``, ``functions``, ``constants`` and ``subsorts``
|
|
1923
|
+
attributes are read. An empty tuple for an empty signature.
|
|
1924
|
+
|
|
1925
|
+
Returns:
|
|
1926
|
+
The sentences, with exact repetitions removed.
|
|
1927
|
+
"""
|
|
1928
|
+
named = set(signature.sorts)
|
|
1929
|
+
for predicate in signature.predicates.values():
|
|
1930
|
+
named.update(s for s in (predicate.arg_sorts or ()) if s is not None)
|
|
1931
|
+
for function in signature.functions.values():
|
|
1932
|
+
named.update(s for s in (function.arg_sorts or ()) if s is not None)
|
|
1933
|
+
if function.result_sort is not None:
|
|
1934
|
+
named.add(function.result_sort)
|
|
1935
|
+
for constant in signature.constants.values():
|
|
1936
|
+
if constant.sort is not None:
|
|
1937
|
+
named.add(constant.sort)
|
|
1938
|
+
for child, parents in signature.subsorts.items():
|
|
1939
|
+
named.add(child)
|
|
1940
|
+
named.update(parents)
|
|
1941
|
+
|
|
1942
|
+
x = Variable("x")
|
|
1943
|
+
sentences: List[Node] = [Quantifier("∃", x, Atom(sort, (x,))) for sort in sorted(named)]
|
|
1944
|
+
sentences.extend(subsort_axioms(signature))
|
|
1945
|
+
for name in sorted(signature.constants):
|
|
1946
|
+
sort = signature.constants[name].sort
|
|
1947
|
+
if sort is not None:
|
|
1948
|
+
sentences.append(Atom(sort, (Constant(name),)))
|
|
1949
|
+
for name in sorted(signature.functions):
|
|
1950
|
+
function = signature.functions[name]
|
|
1951
|
+
if function.result_sort is None:
|
|
1952
|
+
continue
|
|
1953
|
+
arguments = [Variable(f"x{i + 1}") for i in range(function.arity)]
|
|
1954
|
+
term = Function(name, tuple(arguments)) if arguments else Constant(name)
|
|
1955
|
+
body: Node = Atom(function.result_sort, (term,))
|
|
1956
|
+
guards = [Atom(sort, (argument,))
|
|
1957
|
+
for sort, argument in zip(function.arg_sorts or (), arguments)
|
|
1958
|
+
if sort is not None]
|
|
1959
|
+
if guards:
|
|
1960
|
+
guard: Node = guards[0]
|
|
1961
|
+
for extra in guards[1:]:
|
|
1962
|
+
guard = And(guard, extra)
|
|
1963
|
+
body = Implies(guard, body)
|
|
1964
|
+
for argument in reversed(arguments):
|
|
1965
|
+
body = Quantifier("∀", argument, body)
|
|
1966
|
+
sentences.append(body)
|
|
1967
|
+
|
|
1968
|
+
unique: List[Node] = []
|
|
1969
|
+
for sentence in sentences:
|
|
1970
|
+
if sentence not in unique:
|
|
1971
|
+
unique.append(sentence)
|
|
1972
|
+
return tuple(unique)
|
|
1973
|
+
|
|
1974
|
+
|
|
1975
|
+
def to_fol(node: Node, include_sort_facts: bool = False) -> Node:
|
|
1976
|
+
"""Reduce an MSFL (or plain FOL) node to a purely classical FOL node.
|
|
1977
|
+
|
|
1978
|
+
Three-phase reduction:
|
|
1979
|
+
|
|
1980
|
+
1. to_msfol() — replaces Łukasiewicz operators with classical boolean
|
|
1981
|
+
counterparts (And/Or/Not/Implies/Iff); sort annotations are preserved.
|
|
1982
|
+
2. _relativize() — replaces SortedQuantifier with a guarded plain
|
|
1983
|
+
Quantifier; replaces SortedConstant with a plain Constant and collects
|
|
1984
|
+
sort-membership atoms as a side-effect.
|
|
1985
|
+
3. _reduce_nl_nodes() — collapses ``Contrast`` to ``And`` and expands
|
|
1986
|
+
``Count`` (including a relativized ``SortedCount``) via the
|
|
1987
|
+
distinct-witnesses encoding, so the returned node honours the
|
|
1988
|
+
"classical FOL constructs only" contract for those inputs too.
|
|
1989
|
+
|
|
1990
|
+
Background facts about sorts are deliberately NOT folded in here, because
|
|
1991
|
+
this reduction is polarity-blind: sort non-emptiness and a Signature's
|
|
1992
|
+
subsort edges come from :func:`nonempty_sort_axioms` and
|
|
1993
|
+
:func:`subsort_axioms`, which a caller adds as separate premises.
|
|
1994
|
+
|
|
1995
|
+
Args:
|
|
1996
|
+
node: any Node (MSFL or classical FOL).
|
|
1997
|
+
include_sort_facts: if True, deduplicated sort-membership atoms are
|
|
1998
|
+
conjoined as a prefix block at the top level. Dedup is by
|
|
1999
|
+
(sort-predicate, constant-name) in first-occurrence order.
|
|
2000
|
+
|
|
2001
|
+
Returns:
|
|
2002
|
+
A Node built from classical FOL constructs only.
|
|
2003
|
+
"""
|
|
2004
|
+
msfol = node.to_msfol()
|
|
2005
|
+
facts: list = []
|
|
2006
|
+
fol = _reduce_nl_nodes(msfol._relativize(facts))
|
|
2007
|
+
if include_sort_facts and facts:
|
|
2008
|
+
seen: set = set()
|
|
2009
|
+
dedup = []
|
|
2010
|
+
for f in facts:
|
|
2011
|
+
key = (f.predicate, f.args[0].name)
|
|
2012
|
+
if key not in seen:
|
|
2013
|
+
seen.add(key)
|
|
2014
|
+
dedup.append(f)
|
|
2015
|
+
conj = dedup[0]
|
|
2016
|
+
for f in dedup[1:]:
|
|
2017
|
+
conj = And(conj, f)
|
|
2018
|
+
fol = And(conj, fol)
|
|
2019
|
+
return fol
|
|
2020
|
+
|
|
2021
|
+
|
|
2022
|
+
# =========================
|
|
2023
|
+
# Sort non-emptiness axioms
|
|
2024
|
+
# =========================
|
|
2025
|
+
|
|
2026
|
+
#: The four many-sorted node types that carry a ``.sort: str`` field — the
|
|
2027
|
+
#: same set :func:`~unicode_logic_kit.atp.finite_domain.lower_msfol` scans for.
|
|
2028
|
+
_SORTED_NODE_TYPES = (SortedQuantifier, SortedConstant, SortedCount, SortedCardinality)
|
|
2029
|
+
|
|
2030
|
+
|
|
2031
|
+
def nonempty_sort_axioms(*sentences: Node, avoid_names=()) -> Tuple[Node, ...]:
|
|
2032
|
+
"""Return one ``∃x (S(x))`` sentence per distinct sort name in ``sentences``.
|
|
2033
|
+
|
|
2034
|
+
Many-sorted FOL (MSFOL), by convention, never gives a sort an EMPTY
|
|
2035
|
+
universe (see the classical-reasoning guide's many-sorted section, and
|
|
2036
|
+
:func:`~unicode_logic_kit.semantics.modelfinder._nonempty_subsets`, which
|
|
2037
|
+
enumerates only non-empty subsets of the domain as a sort's universe).
|
|
2038
|
+
``to_fol``'s own relativisation above — ``∀x:S φ`` becomes
|
|
2039
|
+
``∀x (S(x) → φ)``, ``∃x:S φ`` becomes ``∃x (S(x) ∧ φ)`` — carries NO such
|
|
2040
|
+
guarantee by itself: a classical decision procedure remains free to make
|
|
2041
|
+
``S`` empty, which makes every ``∀x:S φ`` vacuously TRUE and can turn an
|
|
2042
|
+
otherwise-valid many-sorted entailment into a spurious countermodel (an
|
|
2043
|
+
empty-sort "model" no legal MSFOL structure would ever be), or turn a
|
|
2044
|
+
genuinely unsatisfiable many-sorted theory satisfiable.
|
|
2045
|
+
|
|
2046
|
+
This is the ONE place that gap is closed, factored out of
|
|
2047
|
+
:func:`~unicode_logic_kit.atp.finite_domain.lower_msfol` (the ASP/CP
|
|
2048
|
+
backends' own many-sorted front door, which builds exactly these
|
|
2049
|
+
sentences) so every OTHER route deciding sorted input through ``to_fol``
|
|
2050
|
+
— directly, or via a node's own ``to_z3``/``to_prover9``/``to_tptp``
|
|
2051
|
+
auto-reduction — can reuse the identical construction instead of
|
|
2052
|
+
drifting apart. A caller must add the returned sentences as their OWN
|
|
2053
|
+
extra, top-level, NEVER-negated assumptions alongside whatever it is
|
|
2054
|
+
deciding — on the premise side for a validity/entailment check (an extra
|
|
2055
|
+
assumption the goal may rely on), as an extra asserted conjunct for a
|
|
2056
|
+
satisfiability/model-finding check — and never fold them INSIDE the
|
|
2057
|
+
per-formula translation itself: ``to_fol``/``to_z3``/``to_prover9``/
|
|
2058
|
+
``to_tptp`` are polarity-blind (the very same node translates the same
|
|
2059
|
+
way whether it sits under a positive or a negated position), so an extra
|
|
2060
|
+
existential planted inside one of them would land under the WRONG
|
|
2061
|
+
polarity whenever the sorted node is itself under a negation, and would
|
|
2062
|
+
be duplicated once per occurrence of the sort besides.
|
|
2063
|
+
|
|
2064
|
+
Every one of the four many-sorted node types (:class:`SortedQuantifier`,
|
|
2065
|
+
:class:`SortedConstant`, :class:`SortedCount`, :class:`SortedCardinality`)
|
|
2066
|
+
carries a ``.sort: str`` field; this walks every sentence for all four,
|
|
2067
|
+
collecting distinct sort names in first-occurrence order (stable, so
|
|
2068
|
+
re-running this on the same input returns the same sentences in the same
|
|
2069
|
+
order), and returns one bound sentence ``∃x (S(x))`` per name — using the
|
|
2070
|
+
SAME predicate name ``to_fol``'s own relativisation uses for that sort's
|
|
2071
|
+
guard atom, so asserting this alongside the relativised sentences
|
|
2072
|
+
constrains exactly the predicate they read, nothing else.
|
|
2073
|
+
|
|
2074
|
+
Returns an EMPTY tuple when no sentence contains any of the four sorted
|
|
2075
|
+
node types — the same no-op signal :func:`lower_msfol` uses to guarantee
|
|
2076
|
+
a fully unsorted caller sees byte-identical solver input.
|
|
2077
|
+
|
|
2078
|
+
The witness variable is named by
|
|
2079
|
+
:func:`~unicode_logic_kit.fol._identifiers.fresh_variables` (``x0``, ``x1``,
|
|
2080
|
+
… skipping every name the input uses, of every kind: a predicate, a
|
|
2081
|
+
function, a constant and a sort are skipped as well as a variable). Until
|
|
2082
|
+
0.28.2 it was ``_msfol_<Sort>_witness``, which the kit's OWN parser rejects
|
|
2083
|
+
— VARIABLE is one letter plus digits — so an axiom this function returned
|
|
2084
|
+
could be printed but not read back, and ``api.parse_any`` said no to text
|
|
2085
|
+
the kit had just written. The sort is still visible in the axiom: the guard
|
|
2086
|
+
atom is the sort predicate itself (``∃x0 Human(x0)``).
|
|
2087
|
+
|
|
2088
|
+
A sort named like the witness would make ``∃x0 x0(x0)``, which a text with
|
|
2089
|
+
one namespace for variables and predicates (SMT-LIB, where cvc5 ends the
|
|
2090
|
+
process on it) cannot say, so the witness is fresh against the sorts too.
|
|
2091
|
+
``avoid_names`` adds names that the CALLER knows and the sentences do not
|
|
2092
|
+
hold (the other formulas of the problem, when the axioms are asked for
|
|
2093
|
+
apart from them); the default is none.
|
|
2094
|
+
"""
|
|
2095
|
+
sort_names: List[str] = []
|
|
2096
|
+
for s in sentences:
|
|
2097
|
+
for node in s.walk():
|
|
2098
|
+
if isinstance(node, _SORTED_NODE_TYPES) and node.sort not in sort_names:
|
|
2099
|
+
sort_names.append(node.sort)
|
|
2100
|
+
witnesses = fresh_variables(len(sort_names),
|
|
2101
|
+
avoid=symbol_names(*sentences) | frozenset(avoid_names))
|
|
2102
|
+
return tuple(
|
|
2103
|
+
Quantifier("∃", Variable(witness), Atom(name, (Variable(witness),)))
|
|
2104
|
+
for name, witness in zip(sort_names, witnesses)
|
|
2105
|
+
)
|
|
2106
|
+
|
|
2107
|
+
|
|
2108
|
+
def sort_membership_axioms(*sentences: Node) -> Tuple[Node, ...]:
|
|
2109
|
+
"""Return the atom ``S(c)`` for every distinct sorted constant ``c:S`` in ``sentences``.
|
|
2110
|
+
|
|
2111
|
+
What a sort IS in this kit, stated once. There is ONE universe. A sort ``S``
|
|
2112
|
+
is the extension of the unary predicate ``S`` -- the sort and the predicate
|
|
2113
|
+
of that name are one symbol -- and it is never empty. Sorts may overlap. A
|
|
2114
|
+
sorted quantifier ``∀x:S φ`` ranges over ``S``, an unsorted one over
|
|
2115
|
+
everything. A sorted constant ``c:S`` denotes an element of ``S``; a constant
|
|
2116
|
+
written with two sorts lies in both, and ``c:S`` here and plain ``c`` there
|
|
2117
|
+
are one constant. An unsorted constant and the value of a function may be
|
|
2118
|
+
any element. That is what the finite model finder
|
|
2119
|
+
(:mod:`~unicode_logic_kit.semantics.modelfinder`) enumerates.
|
|
2120
|
+
|
|
2121
|
+
:func:`to_fol` writes the first half of that: ``∀x:S φ`` becomes
|
|
2122
|
+
``∀x (S(x) → φ)`` and ``c:S`` becomes ``c``. :func:`nonempty_sort_axioms`
|
|
2123
|
+
states that no sort is empty. THIS function states the remaining fact, that
|
|
2124
|
+
a sorted constant is in its sort. Without it the reduction forgets the
|
|
2125
|
+
annotation altogether, and ``∀x:Human Mortal(x) ⊢ Mortal(socrates:Human)``
|
|
2126
|
+
has the countermodel in which ``socrates`` is no ``Human`` at all.
|
|
2127
|
+
|
|
2128
|
+
The contract is :func:`nonempty_sort_axioms`' own. The atoms are background
|
|
2129
|
+
facts: a caller adds them as its OWN top-level, never-negated assumptions --
|
|
2130
|
+
premises for an entailment, extra asserted conjuncts for a satisfiability
|
|
2131
|
+
question -- next to whatever it decides, and computes them over the premises
|
|
2132
|
+
AND the conclusion. They are never folded into the per-formula translation,
|
|
2133
|
+
which is polarity-blind: conjoined onto a conclusion, ``S(c)`` would become
|
|
2134
|
+
something to prove. (``to_fol(..., include_sort_facts=True)`` does conjoin
|
|
2135
|
+
them, and is therefore right only for a sentence that is itself asserted.)
|
|
2136
|
+
|
|
2137
|
+
One atom per distinct ``(constant, sort)`` pair, in first-occurrence order;
|
|
2138
|
+
an empty tuple when there is no sorted constant. The atom is built over the
|
|
2139
|
+
name AS WRITTEN IN ``sentences``. A writer that renames a constant on its
|
|
2140
|
+
way out (the TPTP, Prover9 and SMT-LIB writers do) must therefore take the
|
|
2141
|
+
atoms from the formulas it actually writes, or the fact would be about
|
|
2142
|
+
another symbol than the one its premises mention. A modal route, whose sort
|
|
2143
|
+
guard carries a world argument, lifts the atom itself.
|
|
2144
|
+
"""
|
|
2145
|
+
pairs: List[Tuple[str, str]] = []
|
|
2146
|
+
for s in sentences:
|
|
2147
|
+
for node in s.walk():
|
|
2148
|
+
if isinstance(node, SortedConstant) and (node.sort, node.name) not in pairs:
|
|
2149
|
+
pairs.append((node.sort, node.name))
|
|
2150
|
+
return tuple(Atom(sort, (Constant(name),)) for sort, name in pairs)
|
|
2151
|
+
|
|
2152
|
+
|
|
2153
|
+
def sort_axioms(*sentences: Node, avoid_names=()) -> Tuple[Node, ...]:
|
|
2154
|
+
"""Everything the guard reading of ``sentences`` needs as background:
|
|
2155
|
+
:func:`nonempty_sort_axioms` followed by :func:`sort_membership_axioms`.
|
|
2156
|
+
``avoid_names`` is :func:`nonempty_sort_axioms`' own.
|
|
2157
|
+
|
|
2158
|
+
This is the premise list to hand to a decision procedure together with the
|
|
2159
|
+
:func:`to_fol` images (or with the sorted sentences themselves, whose
|
|
2160
|
+
``to_z3`` / ``to_tptp`` / ``to_prover9`` reduce on their own)::
|
|
2161
|
+
|
|
2162
|
+
api.prove(to_fol(goal), [to_fol(p) for p in premises] + list(sort_axioms(*premises, goal)))
|
|
2163
|
+
|
|
2164
|
+
``api.prove`` and the backends add it themselves for sorted input.
|
|
2165
|
+
:func:`subsort_axioms` is the third family, for a
|
|
2166
|
+
:class:`~unicode_logic_kit.fol.signature.Signature` that declares subsorts.
|
|
2167
|
+
"""
|
|
2168
|
+
return nonempty_sort_axioms(*sentences, avoid_names=avoid_names) + sort_membership_axioms(*sentences)
|
|
2169
|
+
|
|
2170
|
+
|
|
2171
|
+
def lower_counting(node: Node, avoid_names=None) -> Node:
|
|
2172
|
+
"""Replace every counting quantifier of ``node``, plain and sorted, by its expansion.
|
|
2173
|
+
|
|
2174
|
+
``∃≥n x φ`` becomes the distinct-witnesses formula of :meth:`Count._expand`, and a
|
|
2175
|
+
sorted ``∃≥n x:S φ`` the same over the guarded matrix ``S(x) ∧ φ`` (what
|
|
2176
|
+
:meth:`SortedCount._relativize` makes of it). Nothing else changes: the other sorted
|
|
2177
|
+
nodes stay, so a caller that goes on to translate or rename the result sees them.
|
|
2178
|
+
|
|
2179
|
+
This is the step to take BEFORE a problem's symbols are renamed for a text with one
|
|
2180
|
+
namespace (the SMT-LIB writers): the witnesses are minted here, so they are names of
|
|
2181
|
+
the problem like any other and the renaming keeps every symbol of every kind apart.
|
|
2182
|
+
``avoid_names`` is the set of every name of the whole problem (see
|
|
2183
|
+
:func:`~unicode_logic_kit.fol._identifiers.symbol_names`); the witnesses are fresh
|
|
2184
|
+
against it and are added to it, so that the expansions of one problem mint distinct
|
|
2185
|
+
names. ``None`` avoids only the names of ``node`` itself.
|
|
2186
|
+
|
|
2187
|
+
Raises:
|
|
2188
|
+
NotImplementedError: a bound beyond what :meth:`Count._expand` materialises.
|
|
2189
|
+
"""
|
|
2190
|
+
node = node.map_children(lambda child: lower_counting(child, avoid_names))
|
|
2191
|
+
if isinstance(node, SortedCount):
|
|
2192
|
+
node = Count(node.op, node.n, node.variable,
|
|
2193
|
+
And(Atom(node.sort, (node.variable,)), node.formula))
|
|
2194
|
+
if isinstance(node, Count):
|
|
2195
|
+
return node._expand(avoid_names)
|
|
2196
|
+
return node
|