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,975 @@
|
|
|
1
|
+
"""Classical (two-valued) Tarskian model theory for FOL and MSFOL.
|
|
2
|
+
|
|
3
|
+
A :class:`Structure` is a first-order "world": a non-empty domain of
|
|
4
|
+
individuals together with interpretations of the constant, function, and
|
|
5
|
+
predicate symbols (and, for MSFOL, the named sorts). :func:`satisfies`
|
|
6
|
+
computes the truth value of a formula in such a structure under a variable
|
|
7
|
+
assignment, following Tarski's recursive definition of satisfaction.
|
|
8
|
+
|
|
9
|
+
Only the classical fragment is interpreted here. Łukasiewicz (fuzzy) operators
|
|
10
|
+
and lambda nodes are rejected with a clear error: the former need the
|
|
11
|
+
many-valued evaluator, the latter must be beta-reduced / lambda-eliminated
|
|
12
|
+
first.
|
|
13
|
+
|
|
14
|
+
Functional style: the variable assignment dict is never mutated. When a
|
|
15
|
+
quantifier ranges over the domain, the binding is added to a *copy* of the
|
|
16
|
+
assignment for each candidate individual.
|
|
17
|
+
|
|
18
|
+
**What a many-sorted structure is.** There is ONE domain. A sort ``S`` is a
|
|
19
|
+
non-empty subset of it, and sorts may overlap. The sort ``S`` and the unary
|
|
20
|
+
predicate ``S`` are ONE symbol: an atom ``S(t)`` holds exactly when the value of
|
|
21
|
+
``t`` is in the sort (a structure that names ``S`` only as a sort needs no
|
|
22
|
+
second table for the predicate, and one that gives both must give them the same
|
|
23
|
+
extension). A sorted constant ``c:S`` denotes an element of ``S``. An unsorted
|
|
24
|
+
constant, an unsorted variable and the value of a function may be any element of
|
|
25
|
+
the domain; a predicate is a relation over the whole domain. A structure that
|
|
26
|
+
breaks one of these laws is not a structure of the definition, and evaluating a
|
|
27
|
+
formula in it is an :class:`IllegalStructureError` naming the law, not a truth
|
|
28
|
+
value: a truth value in such a structure answers a question nobody asked. The
|
|
29
|
+
checks run where the evaluator reads the thing they are about (a sort when it is
|
|
30
|
+
read, a sorted constant when it is evaluated); :func:`check_structure` runs all
|
|
31
|
+
of them at once, up front.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
import html
|
|
35
|
+
import operator
|
|
36
|
+
from typing import Any, Callable, Dict, Iterable, List, Mapping, Optional, Set, Tuple, Union
|
|
37
|
+
|
|
38
|
+
from ..fol.nodes import (
|
|
39
|
+
Node,
|
|
40
|
+
Variable, Constant, Number, Function,
|
|
41
|
+
Atom, Not, And, Or, Xor, Implies, Iff, Quantifier,
|
|
42
|
+
SortedQuantifier, SortedConstant,
|
|
43
|
+
Count, Cardinality, SortedCount, SortedCardinality, Measure,
|
|
44
|
+
LukNegation, WeakConjunction, WeakDisjunction,
|
|
45
|
+
StrongConjunction, StrongDisjunction,
|
|
46
|
+
LukImplication, LukEquivalence,
|
|
47
|
+
LambdaVar, Lambda, Application,
|
|
48
|
+
)
|
|
49
|
+
from ..fol._fol_nodes import numeral_key
|
|
50
|
+
from ..fol._numeral_symbols import term_numerals
|
|
51
|
+
from ..fol._tptp_symbols import is_tptp_boolean_atom as _is_tptp_boolean_atom
|
|
52
|
+
from ..fol._tptp_symbols import truth_constant_word as _truth_constant_word
|
|
53
|
+
|
|
54
|
+
# Quantifier.type spellings accepted for each quantifier kind.
|
|
55
|
+
_FORALL = ("∀", "forall")
|
|
56
|
+
_EXISTS = ("∃", "exists")
|
|
57
|
+
|
|
58
|
+
#: The function symbol a :class:`Measure` term denotes. Matches ``Measure.to_z3`` and
|
|
59
|
+
#: ``Measure.to_prover9``, so a structure found here interprets the same symbol the
|
|
60
|
+
#: provers see.
|
|
61
|
+
_MEASURE_FUNC = ("measure", 2)
|
|
62
|
+
|
|
63
|
+
# Infix comparison predicates that are NOT equality/disequality. Their
|
|
64
|
+
# extensions live in Structure.predicates like any ordinary relation.
|
|
65
|
+
_ORDER_COMPARISONS = frozenset({"<", ">", "≤", "≥"})
|
|
66
|
+
|
|
67
|
+
#: Numeric readings of the order comparisons (see :func:`_order_value`).
|
|
68
|
+
_ORDER_OPS = {
|
|
69
|
+
"<": operator.lt, ">": operator.gt, "≤": operator.le, "≥": operator.ge,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# Łukasiewicz node types: two-valued Tarski cannot interpret them.
|
|
73
|
+
_FUZZY_TYPES = (
|
|
74
|
+
LukNegation, WeakConjunction, WeakDisjunction,
|
|
75
|
+
StrongConjunction, StrongDisjunction,
|
|
76
|
+
LukImplication, LukEquivalence,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Lambda-calculus node types: must be eliminated before evaluation.
|
|
80
|
+
_LAMBDA_TYPES = (LambdaVar, Lambda, Application)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class IllegalStructureError(ValueError):
|
|
84
|
+
"""A :class:`Structure` that is not a structure of the many-sorted definition.
|
|
85
|
+
|
|
86
|
+
Raised instead of a truth value when a formula is evaluated in a structure
|
|
87
|
+
that breaks one of the laws in the module docstring: a sort that is empty or
|
|
88
|
+
holds an element outside the domain, a name that is a sort and a unary
|
|
89
|
+
predicate with two different extensions, or a sorted constant whose value is
|
|
90
|
+
not in its sort. The message names the law and the symbols that break it.
|
|
91
|
+
|
|
92
|
+
It is also raised when a :class:`Structure` is BUILT with a table keyed in a shape
|
|
93
|
+
the evaluator never reads (see :class:`Structure`): such a table would be dropped
|
|
94
|
+
without a word and its symbol would denote the empty relation.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _first_length(table: Any) -> Optional[int]:
|
|
99
|
+
"""The one length shared by every argument tuple in ``table``, if it has one.
|
|
100
|
+
|
|
101
|
+
``table`` is the extension of a predicate (a container of argument tuples) or the
|
|
102
|
+
table of a function (a mapping from argument tuples); ``None`` when it is
|
|
103
|
+
empty, a callable or has tuples of different lengths (a bool is a nullary
|
|
104
|
+
predicate's value, whose tuple has length 0).
|
|
105
|
+
"""
|
|
106
|
+
if isinstance(table, bool):
|
|
107
|
+
return 0
|
|
108
|
+
try:
|
|
109
|
+
lengths = {len(row) for row in table}
|
|
110
|
+
except TypeError:
|
|
111
|
+
return None
|
|
112
|
+
return lengths.pop() if len(lengths) == 1 else None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _table_key_problems(constants: Mapping[Any, Any], functions: Mapping[Any, Any],
|
|
116
|
+
predicates: Mapping[Any, Any], sorts: Mapping[Any, Any]) -> List[str]:
|
|
117
|
+
"""Every table key of a structure that the evaluator would never read.
|
|
118
|
+
|
|
119
|
+
A function or predicate table is read under ``(name, arity)`` (``name`` a ``str``,
|
|
120
|
+
``arity`` a non-negative ``int``); a constant and a sort are read under their name, a
|
|
121
|
+
``str``. A table under any other key is not an extension of anything: it is dropped,
|
|
122
|
+
and the symbol it was meant for denotes the empty relation (a predicate) or has no
|
|
123
|
+
value (a function). One line per offending key, naming the table, the key and what
|
|
124
|
+
to write instead.
|
|
125
|
+
"""
|
|
126
|
+
problems: List[str] = []
|
|
127
|
+
for table_name, table, consequence in (
|
|
128
|
+
("functions", functions, "the function would have no value"),
|
|
129
|
+
("predicates", predicates, "the predicate would denote the empty relation")):
|
|
130
|
+
for key, value in table.items():
|
|
131
|
+
if (isinstance(key, tuple) and len(key) == 2 and isinstance(key[0], str)
|
|
132
|
+
and isinstance(key[1], int) and not isinstance(key[1], bool) and key[1] >= 0):
|
|
133
|
+
continue
|
|
134
|
+
if isinstance(key, str):
|
|
135
|
+
length = _first_length(value)
|
|
136
|
+
arity = "arity" if length is None else str(length)
|
|
137
|
+
problems.append(
|
|
138
|
+
f"{table_name}[{key!r}] is keyed by the bare name, but a table is read "
|
|
139
|
+
f"under (name, arity); this one would never be read and {consequence}. "
|
|
140
|
+
f"Write the key as ({key!r}, {arity})")
|
|
141
|
+
else:
|
|
142
|
+
problems.append(
|
|
143
|
+
f"{table_name}[{key!r}] is not keyed by (name, arity), a pair of a str "
|
|
144
|
+
f"and a non-negative int, so it would never be read and {consequence}")
|
|
145
|
+
for table_name, table in (("constants", constants), ("sorts", sorts)):
|
|
146
|
+
for key in table:
|
|
147
|
+
if not isinstance(key, str):
|
|
148
|
+
problems.append(
|
|
149
|
+
f"{table_name}[{key!r}] is not keyed by a name (a str), so it would "
|
|
150
|
+
f"never be read")
|
|
151
|
+
return problems
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class Structure:
|
|
155
|
+
"""A first-order structure (model / "world") over a non-empty domain.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
domain: a non-empty iterable of individuals (any hashable Python
|
|
159
|
+
values, e.g. ``{"alice", "bob"}`` or ``{0, 1}``). Stored as a
|
|
160
|
+
tuple preserving order; duplicates are dropped.
|
|
161
|
+
constants: maps a constant NAME (str) to an individual in the domain.
|
|
162
|
+
Interprets both :class:`Constant` and :class:`SortedConstant`.
|
|
163
|
+
:class:`Number` ``n`` defaults to the individual ``n`` unless the
|
|
164
|
+
name of its VALUE is overridden here (``"1"`` for ``1`` and for
|
|
165
|
+
``1.0``, ``"2.5"`` for ``2.5``: one constant per value, see
|
|
166
|
+
:func:`~unicode_logic_kit.fol._fol_nodes.numeral_key`) — except where ``n``
|
|
167
|
+
is compared with a cardinality, which always reads it as the numeral
|
|
168
|
+
itself.
|
|
169
|
+
functions: maps ``(name, arity)`` to either a Python callable
|
|
170
|
+
``(*args) -> individual`` or a plain dict ``{arg_tuple: individual}``.
|
|
171
|
+
A dict is looked up by the tuple of evaluated argument individuals.
|
|
172
|
+
predicates: maps ``(name, arity)`` to the relation's extension — a set
|
|
173
|
+
(or any container) of argument tuples of individuals. A nullary
|
|
174
|
+
predicate maps ``(name, 0)`` to a bool. A missing predicate denotes
|
|
175
|
+
the empty relation (always false), except for the order comparisons
|
|
176
|
+
``< > ≤ ≥``, which fall back to arithmetic when both operands
|
|
177
|
+
evaluate to numbers — see :func:`_order_value`.
|
|
178
|
+
sorts: maps a sort name (str) to its universe — a NON-EMPTY subset of
|
|
179
|
+
the domain. Used by :class:`SortedQuantifier`, the sorted counting
|
|
180
|
+
quantifiers and cardinality terms to restrict the range, and read
|
|
181
|
+
by an atom ``S(t)`` over a name ``S`` that is a sort and has no
|
|
182
|
+
predicate table of its own: a sort and the unary predicate of the
|
|
183
|
+
same name are one symbol. A structure that lists both must give
|
|
184
|
+
them the same extension. A sorted constant ``c:S`` must denote an
|
|
185
|
+
element of ``S``. Anything else is an
|
|
186
|
+
:class:`IllegalStructureError` when the formula is evaluated.
|
|
187
|
+
|
|
188
|
+
All mapping arguments default to empty, so a bare ``Structure(domain)`` is
|
|
189
|
+
a valid (if symbol-free) world.
|
|
190
|
+
|
|
191
|
+
**Keys are checked when the structure is built.** ``functions`` and ``predicates``
|
|
192
|
+
are keyed by ``(name, arity)`` and ``constants`` and ``sorts`` by the name; the
|
|
193
|
+
evaluator reads a table under no other key. A table under a bare name
|
|
194
|
+
(``predicates={"Q": {(0,)}}``) would therefore be dropped without a word and ``Q``
|
|
195
|
+
would denote the empty relation, so a key of any other shape is refused with an
|
|
196
|
+
:class:`IllegalStructureError` that names the key and the key to write instead
|
|
197
|
+
(``("Q", 1)``).
|
|
198
|
+
|
|
199
|
+
The laws on ``sorts`` are checked when a sort is first read and again after
|
|
200
|
+
its table (or the domain, or the predicate table of the same name) is
|
|
201
|
+
replaced by another object; changing a set in place after the first read is
|
|
202
|
+
not noticed. :func:`check_structure` checks everything, every time.
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
def __init__(
|
|
206
|
+
self,
|
|
207
|
+
domain: Iterable[Any],
|
|
208
|
+
constants: Optional[Mapping[str, Any]] = None,
|
|
209
|
+
functions: Optional[Mapping[Tuple[str, int], Union[Callable, Mapping]]] = None,
|
|
210
|
+
predicates: Optional[Mapping[Tuple[str, int], Any]] = None,
|
|
211
|
+
sorts: Optional[Mapping[str, Iterable[Any]]] = None,
|
|
212
|
+
):
|
|
213
|
+
"""Build a structure, copying each mapping so later edits never leak in.
|
|
214
|
+
|
|
215
|
+
Raises:
|
|
216
|
+
ValueError: the domain is empty.
|
|
217
|
+
~unicode_logic_kit.semantics.tarski.IllegalStructureError:
|
|
218
|
+
a ``functions`` or ``predicates`` table is not keyed
|
|
219
|
+
by ``(name, arity)`` (a bare name included), or a ``constants`` or
|
|
220
|
+
``sorts`` table is not keyed by a name: the evaluator would never read
|
|
221
|
+
such a table, and its symbol would silently denote nothing. The
|
|
222
|
+
message names each offending key and what to write instead.
|
|
223
|
+
"""
|
|
224
|
+
# Deduplicate while preserving order; reject an empty domain.
|
|
225
|
+
seen = []
|
|
226
|
+
for d in domain:
|
|
227
|
+
if d not in seen:
|
|
228
|
+
seen.append(d)
|
|
229
|
+
if not seen:
|
|
230
|
+
raise ValueError("Structure domain must be non-empty.")
|
|
231
|
+
self.domain: Tuple[Any, ...] = tuple(seen)
|
|
232
|
+
|
|
233
|
+
problems = _table_key_problems(constants or {}, functions or {},
|
|
234
|
+
predicates or {}, sorts or {})
|
|
235
|
+
if problems:
|
|
236
|
+
raise IllegalStructureError("Structure: " + "; ".join(problems))
|
|
237
|
+
|
|
238
|
+
self.constants: Dict[str, Any] = dict(constants or {})
|
|
239
|
+
self.functions: Dict[Tuple[str, int], Union[Callable, Mapping]] = dict(functions or {})
|
|
240
|
+
self.predicates: Dict[Tuple[str, int], Any] = dict(predicates or {})
|
|
241
|
+
self.sorts: Dict[str, Tuple[Any, ...]] = {
|
|
242
|
+
name: tuple(universe) for name, universe in (sorts or {}).items()
|
|
243
|
+
}
|
|
244
|
+
# sort name -> the objects (universe, predicate table, domain) the laws
|
|
245
|
+
# on that sort were last verified against; see sort_universe.
|
|
246
|
+
self._verified_sorts: Dict[str, Tuple[Any, Any, Any]] = {}
|
|
247
|
+
|
|
248
|
+
def __repr__(self) -> str:
|
|
249
|
+
"""Show the domain size and the symbol tables for quick inspection."""
|
|
250
|
+
return (
|
|
251
|
+
f"Structure(domain={self.domain!r}, "
|
|
252
|
+
f"constants={self.constants!r}, "
|
|
253
|
+
f"functions={list(self.functions)!r}, "
|
|
254
|
+
f"predicates={list(self.predicates)!r}, "
|
|
255
|
+
f"sorts={self.sorts!r})"
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
def _repr_html_(self) -> str:
|
|
259
|
+
"""Jupyter/IPython rich-display hook: an HTML summary table.
|
|
260
|
+
|
|
261
|
+
Same conservative shape as :meth:`__repr__`: the (already fully
|
|
262
|
+
materialised, finite) domain and constant VALUES are shown, but
|
|
263
|
+
functions/predicates are represented by their ``(name, arity)`` KEYS
|
|
264
|
+
only — an interpretation may be a plain Python callable, and it is
|
|
265
|
+
never invoked here, exactly as ``__repr__`` already chooses not to
|
|
266
|
+
dump ``self.functions``/``self.predicates`` themselves. Every value is
|
|
267
|
+
rendered through ``repr()`` and HTML-escaped, since a domain
|
|
268
|
+
individual, constant name, or sort name may be an arbitrary
|
|
269
|
+
user-supplied string (e.g. containing ``<``/``>``/``&``).
|
|
270
|
+
"""
|
|
271
|
+
def esc(value: Any) -> str:
|
|
272
|
+
return html.escape(repr(value))
|
|
273
|
+
|
|
274
|
+
def keys_html(mapping: Mapping[Tuple[str, int], Any]) -> str:
|
|
275
|
+
return ", ".join(html.escape(f"{name}/{arity}")
|
|
276
|
+
for name, arity in sorted(mapping)) or "—"
|
|
277
|
+
|
|
278
|
+
constants_html = ", ".join(
|
|
279
|
+
f"{html.escape(name)} = {esc(value)}"
|
|
280
|
+
for name, value in sorted(self.constants.items())
|
|
281
|
+
) or "—"
|
|
282
|
+
sorts_html = ", ".join(
|
|
283
|
+
f"{html.escape(name)} = {{{', '.join(esc(v) for v in universe)}}}"
|
|
284
|
+
for name, universe in sorted(self.sorts.items())
|
|
285
|
+
) or "—"
|
|
286
|
+
rows = [
|
|
287
|
+
("domain", ", ".join(esc(d) for d in self.domain) or "—"),
|
|
288
|
+
("constants", constants_html),
|
|
289
|
+
("functions", keys_html(self.functions)),
|
|
290
|
+
("predicates", keys_html(self.predicates)),
|
|
291
|
+
("sorts", sorts_html),
|
|
292
|
+
]
|
|
293
|
+
body = "".join(f"<tr><th>{label}</th><td>{cell}</td></tr>" for label, cell in rows)
|
|
294
|
+
return f"<table><tbody>{body}</tbody></table>"
|
|
295
|
+
|
|
296
|
+
def sort_universe(self, sort: str) -> Tuple[Any, ...]:
|
|
297
|
+
"""Return the universe of a named sort.
|
|
298
|
+
|
|
299
|
+
Raises:
|
|
300
|
+
KeyError: if the sort is undeclared. An undeclared sort is an error
|
|
301
|
+
rather than the empty set, since ``∀x:Undeclared φ`` vacuously
|
|
302
|
+
true and ``∃x:Undeclared φ`` false would silently mask a typo.
|
|
303
|
+
IllegalStructureError: if the sort is empty, holds an element that
|
|
304
|
+
is not in the domain, or is also a unary predicate of this
|
|
305
|
+
structure with a different extension. A vacuous ``∀`` over an
|
|
306
|
+
empty sort would be a truth value in a structure that is not a
|
|
307
|
+
structure of the definition.
|
|
308
|
+
"""
|
|
309
|
+
if sort not in self.sorts:
|
|
310
|
+
raise KeyError(
|
|
311
|
+
f"Sort {sort!r} is not declared in this structure "
|
|
312
|
+
f"(known sorts: {sorted(self.sorts)})."
|
|
313
|
+
)
|
|
314
|
+
universe = self.sorts[sort]
|
|
315
|
+
predicate = self.predicates.get((sort, 1))
|
|
316
|
+
verified = self._verified_sorts.get(sort)
|
|
317
|
+
if (verified is None or verified[0] is not universe
|
|
318
|
+
or verified[1] is not predicate or verified[2] is not self.domain):
|
|
319
|
+
problems = _sort_problems(self, sort)
|
|
320
|
+
if problems:
|
|
321
|
+
raise IllegalStructureError("; ".join(problems))
|
|
322
|
+
self._verified_sorts[sort] = (universe, predicate, self.domain)
|
|
323
|
+
return universe
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _sort_problems(structure: Structure, name: str) -> List[str]:
|
|
327
|
+
"""Every way the sort ``name`` of ``structure`` breaks the definition (none: empty list).
|
|
328
|
+
|
|
329
|
+
A sort is a NON-EMPTY subset of the domain, and a sort that is also a unary
|
|
330
|
+
predicate of the structure has the same extension as that predicate.
|
|
331
|
+
"""
|
|
332
|
+
universe = structure.sorts[name]
|
|
333
|
+
problems: List[str] = []
|
|
334
|
+
if not universe:
|
|
335
|
+
problems.append(f"sort {name!r} is empty, and a sort is never empty")
|
|
336
|
+
outside = [d for d in universe if d not in structure.domain]
|
|
337
|
+
if outside:
|
|
338
|
+
problems.append(
|
|
339
|
+
f"sort {name!r} holds {outside!r}, which "
|
|
340
|
+
f"{'is' if len(outside) == 1 else 'are'} not in the domain"
|
|
341
|
+
)
|
|
342
|
+
declared = structure.predicates.get((name, 1))
|
|
343
|
+
if declared is not None:
|
|
344
|
+
in_sort = [d for d in structure.domain if d in universe]
|
|
345
|
+
in_predicate = [d for d in structure.domain if (d,) in declared]
|
|
346
|
+
if in_sort != in_predicate:
|
|
347
|
+
problems.append(
|
|
348
|
+
f"{name!r} is both a sort and a unary predicate, and they have "
|
|
349
|
+
f"different extensions: the sort holds {in_sort!r}, the "
|
|
350
|
+
f"predicate holds {in_predicate!r}"
|
|
351
|
+
)
|
|
352
|
+
return problems
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _undeclared_sort_message(sort: str) -> str:
|
|
356
|
+
"""The message for a formula that uses a sort the structure does not declare."""
|
|
357
|
+
return f"the formula uses the sort {sort!r}, which this structure does not declare"
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _sorted_constant_problem(structure: Structure, term: SortedConstant) -> Optional[str]:
|
|
361
|
+
"""Why ``term`` (``c:S``) is not an element of its declared sort in ``structure``, or None.
|
|
362
|
+
|
|
363
|
+
A constant the structure does not interpret has no value to be outside the
|
|
364
|
+
sort; a sort the structure does not declare is a different violation (see
|
|
365
|
+
:func:`_undeclared_sort_message`).
|
|
366
|
+
"""
|
|
367
|
+
if term.name not in structure.constants or term.sort not in structure.sorts:
|
|
368
|
+
return None
|
|
369
|
+
value = structure.constants[term.name]
|
|
370
|
+
if value not in structure.sorts[term.sort]:
|
|
371
|
+
return (f"sorted constant {term.name}:{term.sort} denotes {value!r}, "
|
|
372
|
+
f"which is not in the sort {term.sort!r} "
|
|
373
|
+
f"({list(structure.sorts[term.sort])!r})")
|
|
374
|
+
return None
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def structure_violations(structure: Structure, *formulas: Node) -> List[str]:
|
|
378
|
+
"""Every way ``structure`` is not a structure of the many-sorted definition.
|
|
379
|
+
|
|
380
|
+
The structure's own laws are always checked: every sort in ``structure.sorts``
|
|
381
|
+
is a non-empty subset of the domain, and a sort that is also a unary
|
|
382
|
+
predicate has that predicate's extension. With ``formulas``, the laws that
|
|
383
|
+
depend on what is written are checked too: every sort a formula uses (a
|
|
384
|
+
sorted quantifier, counting quantifier, cardinality term or constant) is
|
|
385
|
+
declared, and every sorted constant ``c:S`` the structure interprets denotes
|
|
386
|
+
an element of ``S``. A constant written with two sorts must therefore lie in
|
|
387
|
+
both.
|
|
388
|
+
|
|
389
|
+
Returns:
|
|
390
|
+
One message per violation, in a stable order; an empty list for a
|
|
391
|
+
legal structure. Nothing is raised.
|
|
392
|
+
"""
|
|
393
|
+
problems: List[str] = []
|
|
394
|
+
for name in structure.sorts:
|
|
395
|
+
problems.extend(_sort_problems(structure, name))
|
|
396
|
+
for formula in formulas:
|
|
397
|
+
for node in formula.walk():
|
|
398
|
+
if not isinstance(node, (SortedQuantifier, SortedCount, SortedCardinality,
|
|
399
|
+
SortedConstant)):
|
|
400
|
+
continue
|
|
401
|
+
if node.sort not in structure.sorts:
|
|
402
|
+
problems.append(_undeclared_sort_message(node.sort))
|
|
403
|
+
elif isinstance(node, SortedConstant):
|
|
404
|
+
problem = _sorted_constant_problem(structure, node)
|
|
405
|
+
if problem is not None:
|
|
406
|
+
problems.append(problem)
|
|
407
|
+
return list(dict.fromkeys(problems))
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def check_structure(structure: Structure, *formulas: Node) -> None:
|
|
411
|
+
"""Raise :class:`IllegalStructureError` unless ``structure`` is a structure of the definition.
|
|
412
|
+
|
|
413
|
+
The up-front, complete version of the checks the evaluator makes as it
|
|
414
|
+
reads sorts and sorted constants: see :func:`structure_violations` for what
|
|
415
|
+
is checked. The error lists every violation, not only the first.
|
|
416
|
+
|
|
417
|
+
Raises:
|
|
418
|
+
IllegalStructureError: if :func:`structure_violations` is not empty.
|
|
419
|
+
"""
|
|
420
|
+
problems = structure_violations(structure, *formulas)
|
|
421
|
+
if problems:
|
|
422
|
+
raise IllegalStructureError("; ".join(problems))
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def _refuse_numeral_constant_pair(formulas: Iterable[Node], where: str) -> None:
|
|
426
|
+
"""Refuse formulas that hold a numeral and a constant spelled like its value.
|
|
427
|
+
|
|
428
|
+
A structure interprets a constant under its name and a numeral under the name of its
|
|
429
|
+
VALUE (:func:`~unicode_logic_kit.fol._fol_nodes.numeral_key`: ``1`` and ``1.0`` are
|
|
430
|
+
``'1'``), in the one table ``constants``. ``Number(1)`` next to ``Constant('1')`` would
|
|
431
|
+
therefore be ONE entry, and a structure could not tell them apart: the evaluator and the
|
|
432
|
+
finite model finder (which enumerates that table) would read two symbols as one. A
|
|
433
|
+
numeral is a constant of its own, so the pair is refused by name, as the Z3 route, the
|
|
434
|
+
cvc5 route, the Prover9 writer and the TPTP writers refuse it. A number that is the bound
|
|
435
|
+
of a counting quantifier, or the operand a cardinality is compared with, is no individual
|
|
436
|
+
and is not looked up there; it clashes with nothing. ``where`` is the caller, which the
|
|
437
|
+
refusal names.
|
|
438
|
+
|
|
439
|
+
Raises:
|
|
440
|
+
NotImplementedError: a :class:`Constant` or :class:`SortedConstant` is named like
|
|
441
|
+
the value of a numeral of the formulas.
|
|
442
|
+
"""
|
|
443
|
+
formulas = list(formulas)
|
|
444
|
+
numerals: Dict[str, Number] = {}
|
|
445
|
+
for formula in formulas:
|
|
446
|
+
for numeral in term_numerals(formula, counting_comparisons=True):
|
|
447
|
+
numerals.setdefault(numeral_key(numeral.value), numeral)
|
|
448
|
+
if not numerals:
|
|
449
|
+
return
|
|
450
|
+
for formula in formulas:
|
|
451
|
+
for node in formula.walk():
|
|
452
|
+
if isinstance(node, (Constant, SortedConstant)) and node.name in numerals:
|
|
453
|
+
raise _numeral_constant_clash(node.name, where)
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def _numeral_constant_clash(name: str, where: str) -> NotImplementedError:
|
|
457
|
+
"""The refusal of a numeral and a constant that are one entry ``name`` of a structure."""
|
|
458
|
+
return NotImplementedError(
|
|
459
|
+
f"{where}: the number {name} and the constant {name!r} would be ONE entry of a "
|
|
460
|
+
"structure's constants (a numeral is the constant of its value and is interpreted "
|
|
461
|
+
"under that name), so a structure could not tell them apart. A numeral is a constant "
|
|
462
|
+
"of its own, identified by its value (1 and 1.0 are one constant), and the kit "
|
|
463
|
+
"refuses to merge it with the constant of the same spelling. Rename the constant.")
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
#: The comparisons a cardinality may be an operand of (arithmetic over the natural numbers).
|
|
467
|
+
_COUNTING_COMPARISONS = frozenset({"=", "≠"}) | _ORDER_COMPARISONS
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def _cardinality_vs_individual(where: str, predicate: str, operand: Node) -> NotImplementedError:
|
|
471
|
+
"""The refusal of a cardinality compared with something that is not a number."""
|
|
472
|
+
return NotImplementedError(
|
|
473
|
+
f"{where}: {predicate!r} compares a cardinality with {operand.to_unicode_str()}, which "
|
|
474
|
+
"is not a number. A cardinality |{v : φ}| is a natural number the evaluator counts, "
|
|
475
|
+
"not an element of the domain, so it is only compared with a numeral or with another "
|
|
476
|
+
"cardinality. Compare it with a number, or quantify over the individuals instead.")
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def _cardinality_as_individual(where: str, cardinality: Node, position: str) -> NotImplementedError:
|
|
480
|
+
"""The refusal of a cardinality that stands where an individual is expected."""
|
|
481
|
+
return NotImplementedError(
|
|
482
|
+
f"{where}: the cardinality {cardinality.to_unicode_str()} is {position}. A cardinality "
|
|
483
|
+
"is a natural number the evaluator counts, not an element of the domain, so it has no "
|
|
484
|
+
"value there: it would be read as the element that shares its value. It can only be "
|
|
485
|
+
"compared with a number (= ≠ < > ≤ ≥ against a numeral or another cardinality). "
|
|
486
|
+
"Compare it, or quantify over the individuals instead.")
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def _refuse_cardinality_as_individual(formulas: Iterable[Node], where: str) -> None:
|
|
490
|
+
"""Refuse a cardinality term that does not stand as an operand of a comparison with a number.
|
|
491
|
+
|
|
492
|
+
``|{v : φ}|`` is a natural number that the evaluator counts, not an element of the
|
|
493
|
+
domain. Its one reading is arithmetic: an operand of ``= ≠ < > ≤ ≥`` whose other operand
|
|
494
|
+
is a number (a numeral or another cardinality). Anywhere else (an argument of an ordinary
|
|
495
|
+
predicate or of a function, or compared with an individual) it would have to be read as
|
|
496
|
+
the element of the domain that happens to share its value (the count ``1`` as the element
|
|
497
|
+
``1``), which is another statement and one that changes with how the domain is named. The
|
|
498
|
+
finite routes that state counting (the ASP and MiniZinc encodings, ``model_eval``) refuse
|
|
499
|
+
these forms, and so does the evaluator, by name, instead of answering for a different
|
|
500
|
+
question. ``where`` is the caller, which the refusal names.
|
|
501
|
+
|
|
502
|
+
Raises:
|
|
503
|
+
NotImplementedError: a :class:`Cardinality` or :class:`SortedCardinality` is an
|
|
504
|
+
argument of a predicate, a function or another term, or an operand of a
|
|
505
|
+
comparison whose other operand is not a number.
|
|
506
|
+
"""
|
|
507
|
+
cardinalities = (Cardinality, SortedCardinality)
|
|
508
|
+
for formula in formulas:
|
|
509
|
+
readable: Set[int] = set()
|
|
510
|
+
for node in formula.walk():
|
|
511
|
+
if (isinstance(node, Atom) and node.predicate in _COUNTING_COMPARISONS
|
|
512
|
+
and len(node.args) == 2
|
|
513
|
+
and any(isinstance(a, cardinalities) for a in node.args)):
|
|
514
|
+
for operand in node.args:
|
|
515
|
+
if not isinstance(operand, cardinalities + (Number,)):
|
|
516
|
+
raise _cardinality_vs_individual(where, node.predicate, operand)
|
|
517
|
+
readable.update(id(a) for a in node.args if isinstance(a, cardinalities))
|
|
518
|
+
for node in formula.walk():
|
|
519
|
+
for child in node._child_nodes():
|
|
520
|
+
if isinstance(child, cardinalities) and id(child) not in readable:
|
|
521
|
+
if isinstance(node, Atom):
|
|
522
|
+
position = f"an argument of the predicate {node.predicate!r}"
|
|
523
|
+
elif isinstance(node, Function):
|
|
524
|
+
position = f"an argument of the function {node.name!r}"
|
|
525
|
+
else:
|
|
526
|
+
position = f"an operand of a {type(node).__name__}"
|
|
527
|
+
raise _cardinality_as_individual(where, child, position)
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
#: The formulas ``_check_formula_once`` has already checked, by identity. The
|
|
531
|
+
#: formula is kept in the table so that its identity cannot be reused while the entry stands.
|
|
532
|
+
_CHECKED_FORMULAS: Dict[int, Node] = {}
|
|
533
|
+
_CHECKED_FORMULAS_MAX = 1024
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def _check_formula_once(formula: Node) -> None:
|
|
537
|
+
"""The checks of a formula as a whole (:func:`_refuse_numeral_constant_pair` and
|
|
538
|
+
:func:`_refuse_cardinality_as_individual`) for the evaluator's entry, which a model finder
|
|
539
|
+
calls once per candidate structure with the same formula: a formula that passed is
|
|
540
|
+
remembered (a table of bounded size), so the check costs one lookup after the first."""
|
|
541
|
+
if id(formula) in _CHECKED_FORMULAS:
|
|
542
|
+
return
|
|
543
|
+
_refuse_numeral_constant_pair([formula], "semantics.tarski.satisfies")
|
|
544
|
+
_refuse_cardinality_as_individual([formula], "semantics.tarski.satisfies")
|
|
545
|
+
if len(_CHECKED_FORMULAS) >= _CHECKED_FORMULAS_MAX:
|
|
546
|
+
_CHECKED_FORMULAS.clear()
|
|
547
|
+
_CHECKED_FORMULAS[id(formula)] = formula
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def term_value(term: Node, structure: Structure, assignment: Mapping[str, Any]) -> Any:
|
|
551
|
+
"""Evaluate a term to its individual in the structure under an assignment.
|
|
552
|
+
|
|
553
|
+
- :class:`Variable` ``v`` → ``assignment[v.name]``.
|
|
554
|
+
- :class:`Constant` ``c`` → ``structure.constants[c.name]``.
|
|
555
|
+
- :class:`SortedConstant` ``c:S`` → ``structure.constants[c.name]``, which
|
|
556
|
+
must be an element of the sort ``S`` of the structure.
|
|
557
|
+
- :class:`Number` ``n`` → ``structure.constants.get(numeral_key(n.value), n.value)``
|
|
558
|
+
(the literal value itself by default): a numeral is a constant named by its
|
|
559
|
+
VALUE, so ``Number(1)`` and ``Number(1.0)`` are one constant. A comparison with a
|
|
560
|
+
cardinality bypasses this and reads the numeral directly — see
|
|
561
|
+
:func:`_operand_value`.
|
|
562
|
+
- :class:`Function` → the interpreted function applied to the evaluated args;
|
|
563
|
+
the interpretation may be a callable or a ``{arg_tuple: value}`` dict.
|
|
564
|
+
|
|
565
|
+
Raises:
|
|
566
|
+
KeyError: for an unassigned variable, an uninterpreted constant, or the
|
|
567
|
+
sort of a sorted constant that the structure does not declare.
|
|
568
|
+
IllegalStructureError: for a sorted constant whose value is not an
|
|
569
|
+
element of its sort, or whose sort breaks the laws in
|
|
570
|
+
:meth:`Structure.sort_universe`.
|
|
571
|
+
ValueError: for an uninterpreted function symbol, a dict interpretation
|
|
572
|
+
missing an argument tuple, or a lambda / non-term node.
|
|
573
|
+
"""
|
|
574
|
+
if isinstance(term, Variable):
|
|
575
|
+
if term.name not in assignment:
|
|
576
|
+
raise KeyError(f"Variable {term.name!r} is not bound in the assignment.")
|
|
577
|
+
return assignment[term.name]
|
|
578
|
+
|
|
579
|
+
if isinstance(term, Constant):
|
|
580
|
+
if term.name not in structure.constants:
|
|
581
|
+
raise KeyError(
|
|
582
|
+
f"Constant {term.name!r} has no interpretation in the structure."
|
|
583
|
+
)
|
|
584
|
+
return structure.constants[term.name]
|
|
585
|
+
|
|
586
|
+
if isinstance(term, Number):
|
|
587
|
+
return structure.constants.get(numeral_key(term.value), term.value)
|
|
588
|
+
|
|
589
|
+
if isinstance(term, Function):
|
|
590
|
+
args = tuple(term_value(a, structure, assignment) for a in term.args)
|
|
591
|
+
key = (term.name, len(term.args))
|
|
592
|
+
if key not in structure.functions:
|
|
593
|
+
raise ValueError(
|
|
594
|
+
f"Function {term.name!r}/{len(term.args)} has no interpretation "
|
|
595
|
+
f"in the structure."
|
|
596
|
+
)
|
|
597
|
+
interp = structure.functions[key]
|
|
598
|
+
if callable(interp):
|
|
599
|
+
return interp(*args)
|
|
600
|
+
# Dict-style interpretation: look up the evaluated argument tuple.
|
|
601
|
+
if args not in interp:
|
|
602
|
+
raise ValueError(
|
|
603
|
+
f"Function {term.name!r}/{len(term.args)} is undefined for "
|
|
604
|
+
f"arguments {args!r}."
|
|
605
|
+
)
|
|
606
|
+
return interp[args]
|
|
607
|
+
|
|
608
|
+
if isinstance(term, (Cardinality, SortedCardinality)):
|
|
609
|
+
# |{v : φ}| counts the individuals satisfying φ. The count is a NATURAL
|
|
610
|
+
# NUMBER, not a domain individual: it has no value as one, and answering with
|
|
611
|
+
# the integer would read it as the element that shares its value. The only
|
|
612
|
+
# reading it has is the operand of a comparison with a number, which
|
|
613
|
+
# _operand_value counts itself.
|
|
614
|
+
raise _cardinality_as_individual(
|
|
615
|
+
"semantics.tarski.term_value", term, "evaluated as an individual")
|
|
616
|
+
|
|
617
|
+
if isinstance(term, Measure):
|
|
618
|
+
# μ(entity, dimension) is the binary function ``measure``, matching the Z3 and
|
|
619
|
+
# Prover9 lowerings — a structure that interprets it agrees with the provers.
|
|
620
|
+
args = (term_value(term.entity, structure, assignment),
|
|
621
|
+
term_value(term.dimension, structure, assignment))
|
|
622
|
+
if _MEASURE_FUNC not in structure.functions:
|
|
623
|
+
raise ValueError(
|
|
624
|
+
"Measure term μ(…) needs an interpretation for the function "
|
|
625
|
+
f"{_MEASURE_FUNC[0]!r}/{_MEASURE_FUNC[1]} in the structure."
|
|
626
|
+
)
|
|
627
|
+
interp = structure.functions[_MEASURE_FUNC]
|
|
628
|
+
if callable(interp):
|
|
629
|
+
return interp(*args)
|
|
630
|
+
if args not in interp:
|
|
631
|
+
raise ValueError(
|
|
632
|
+
f"Function {_MEASURE_FUNC[0]!r}/{_MEASURE_FUNC[1]} is undefined for "
|
|
633
|
+
f"arguments {args!r}."
|
|
634
|
+
)
|
|
635
|
+
return interp[args]
|
|
636
|
+
|
|
637
|
+
if isinstance(term, SortedConstant):
|
|
638
|
+
# Tested after the hot branches above, so an unsorted formula pays nothing
|
|
639
|
+
# for it. The value is the constant's, as for a plain Constant; the extra
|
|
640
|
+
# step is the law that it lies in the sort.
|
|
641
|
+
if term.name not in structure.constants:
|
|
642
|
+
raise KeyError(
|
|
643
|
+
f"Constant {term.name!r} has no interpretation in the structure."
|
|
644
|
+
)
|
|
645
|
+
value = structure.constants[term.name]
|
|
646
|
+
if value not in structure.sort_universe(term.sort):
|
|
647
|
+
raise IllegalStructureError(_sorted_constant_problem(structure, term))
|
|
648
|
+
return value
|
|
649
|
+
|
|
650
|
+
if isinstance(term, _LAMBDA_TYPES):
|
|
651
|
+
raise ValueError(
|
|
652
|
+
f"Cannot evaluate lambda node {type(term).__name__} as a term; "
|
|
653
|
+
"beta-reduce and lambda-eliminate the formula first."
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
raise ValueError(
|
|
657
|
+
f"term_value: {type(term).__name__} is not a term node."
|
|
658
|
+
)
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
def _witnesses(
|
|
662
|
+
binder: Node,
|
|
663
|
+
structure: Structure,
|
|
664
|
+
assignment: Mapping[str, Any],
|
|
665
|
+
) -> Iterable[Any]:
|
|
666
|
+
"""Yield the individuals in ``binder``'s range that satisfy its matrix.
|
|
667
|
+
|
|
668
|
+
Shared by the counting quantifiers and the cardinality terms: both bind one
|
|
669
|
+
variable over a matrix and differ only in what they do with the witnesses. A
|
|
670
|
+
sorted binder ranges over its sort's universe, an unsorted one over the domain.
|
|
671
|
+
"""
|
|
672
|
+
universe = (structure.sort_universe(binder.sort)
|
|
673
|
+
if isinstance(binder, (SortedCount, SortedCardinality))
|
|
674
|
+
else structure.domain)
|
|
675
|
+
name = binder.variable.name
|
|
676
|
+
for d in universe:
|
|
677
|
+
if _evaluate(binder.formula, structure, _extend(assignment, name, d)):
|
|
678
|
+
yield d
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
def _is_number(value: Any) -> bool:
|
|
682
|
+
"""Whether a term value counts as a number for an order comparison.
|
|
683
|
+
|
|
684
|
+
``bool`` is excluded even though Python makes it an ``int`` subclass: a truth
|
|
685
|
+
value is not a position on a scale, and letting ``True ≥ False`` quietly
|
|
686
|
+
succeed would hide a modelling error rather than surface it.
|
|
687
|
+
"""
|
|
688
|
+
return not isinstance(value, bool) and isinstance(value, (int, float))
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
def _compares_a_cardinality(atom: Atom) -> bool:
|
|
692
|
+
"""Whether a comparison atom has a cardinality operand."""
|
|
693
|
+
return any(isinstance(a, (Cardinality, SortedCardinality)) for a in atom.args)
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def _operand_value(term: Node, structure: Structure, assignment: Mapping[str, Any],
|
|
697
|
+
numeric: bool) -> Any:
|
|
698
|
+
"""Evaluate one operand of a comparison.
|
|
699
|
+
|
|
700
|
+
Next to a cardinality (``numeric``), a :class:`Number` is the numeral it
|
|
701
|
+
spells, never whatever ``structure.constants`` maps its name to: the other
|
|
702
|
+
side is a count, so ``|{x : P(x)}| > 1`` asks about the number one. Reading
|
|
703
|
+
the literal through the constant table instead would let a structure that
|
|
704
|
+
interprets the name ``"1"`` as some other individual change the question —
|
|
705
|
+
and a model finder that enumerates interpretations for every numeral it sees
|
|
706
|
+
finds exactly such a structure, reporting a countermodel to a valid
|
|
707
|
+
entailment.
|
|
708
|
+
|
|
709
|
+
Next to a cardinality the other operand has to be a number too: the count of a
|
|
710
|
+
cardinality is the only value of one, and an individual (a constant, a variable, a
|
|
711
|
+
function term) is not a number, whatever integer it happens to be.
|
|
712
|
+
|
|
713
|
+
Raises:
|
|
714
|
+
NotImplementedError: ``numeric`` and ``term`` is neither a numeral nor a cardinality.
|
|
715
|
+
"""
|
|
716
|
+
if numeric:
|
|
717
|
+
if isinstance(term, Number):
|
|
718
|
+
return term.value
|
|
719
|
+
if isinstance(term, (Cardinality, SortedCardinality)):
|
|
720
|
+
return sum(1 for _ in _witnesses(term, structure, assignment))
|
|
721
|
+
raise _cardinality_vs_individual("semantics.tarski", "a comparison", term)
|
|
722
|
+
return term_value(term, structure, assignment)
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
def _order_value(atom: Atom, structure: Structure, assignment: Mapping[str, Any]) -> bool:
|
|
726
|
+
"""Truth value of a binary order comparison ``< > ≤ ≥``.
|
|
727
|
+
|
|
728
|
+
Three readings apply, in this order of precedence:
|
|
729
|
+
|
|
730
|
+
1. A :class:`Cardinality` operand forces the **numeric** reading. A cardinality
|
|
731
|
+
is a natural number this evaluator computes itself, so there is no freedom
|
|
732
|
+
left to a structure; routing it through a relation extension would be a
|
|
733
|
+
category error. A :class:`Number` on the other side is read as the numeral
|
|
734
|
+
itself, not through ``structure.constants`` (see :func:`_operand_value`).
|
|
735
|
+
A cardinality compared against a non-number raises.
|
|
736
|
+
2. Otherwise a **declared** extension wins. The order symbols are ordinary
|
|
737
|
+
relation symbols of the language, and a structure may interpret ``<`` over
|
|
738
|
+
its domain however it likes — that is also the reading ``to_z3`` /
|
|
739
|
+
``to_prover9`` export, where the comparison is an uninterpreted relation.
|
|
740
|
+
3. Otherwise, if both operands evaluate to numbers, the **numeric** reading
|
|
741
|
+
applies. This is what makes an undeclared order over :class:`Measure` values
|
|
742
|
+
behave: ``μ`` is an uninterpreted function, so a structure that maps it to
|
|
743
|
+
numbers without also declaring ``≥`` would otherwise fall through to the
|
|
744
|
+
empty relation and be silently false — the same failure mode (1) exists to
|
|
745
|
+
prevent for cardinalities.
|
|
746
|
+
|
|
747
|
+
Anything else is the empty relation, hence false, like any uninterpreted
|
|
748
|
+
predicate. Note the asymmetry between (1) and (2) is deliberate and not an
|
|
749
|
+
inconsistency: a cardinality *is* a number, whereas a measure's values are
|
|
750
|
+
whatever the structure says they are.
|
|
751
|
+
"""
|
|
752
|
+
numeric = _compares_a_cardinality(atom)
|
|
753
|
+
left, right = (_operand_value(a, structure, assignment, numeric) for a in atom.args)
|
|
754
|
+
|
|
755
|
+
if numeric:
|
|
756
|
+
for value in (left, right):
|
|
757
|
+
if not _is_number(value):
|
|
758
|
+
raise ValueError(
|
|
759
|
+
f"Cannot compare a cardinality with {value!r}: an order "
|
|
760
|
+
f"comparison involving |{{v : φ}}| is numeric, so both operands "
|
|
761
|
+
f"must evaluate to numbers."
|
|
762
|
+
)
|
|
763
|
+
return _ORDER_OPS[atom.predicate](left, right)
|
|
764
|
+
|
|
765
|
+
key = (atom.predicate, 2)
|
|
766
|
+
if key in structure.predicates:
|
|
767
|
+
return (left, right) in structure.predicates[key]
|
|
768
|
+
|
|
769
|
+
if _is_number(left) and _is_number(right):
|
|
770
|
+
return _ORDER_OPS[atom.predicate](left, right)
|
|
771
|
+
|
|
772
|
+
return False
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
def _atom_value(atom: Atom, structure: Structure, assignment: Mapping[str, Any]) -> bool:
|
|
776
|
+
"""Compute the truth value of an atomic formula.
|
|
777
|
+
|
|
778
|
+
Equality ``=`` is identity of the two term values; ``≠`` is non-identity. When
|
|
779
|
+
one side is a cardinality, a :class:`Number` on the other is read as the
|
|
780
|
+
numeral itself, as for the order comparisons (see :func:`_operand_value`).
|
|
781
|
+
A nullary predicate reads its bool from ``predicates[(name, 0)]``. A binary
|
|
782
|
+
order comparison ``< > ≤ ≥`` is delegated to :func:`_order_value`. Every other
|
|
783
|
+
predicate is true iff the tuple of argument values lies in its extension; a
|
|
784
|
+
missing extension is the empty relation, hence false — unless the name is a
|
|
785
|
+
SORT of the structure: a sort and the unary predicate of that name are one
|
|
786
|
+
symbol, so ``S(t)`` is then true iff the value of ``t`` is in the sort (and
|
|
787
|
+
when the structure lists a predicate table for ``S`` as well, it must agree
|
|
788
|
+
with the sort — :meth:`Structure.sort_universe` checks that).
|
|
789
|
+
"""
|
|
790
|
+
if atom.predicate in ("=", "≠") and len(atom.args) == 2:
|
|
791
|
+
numeric = _compares_a_cardinality(atom)
|
|
792
|
+
left, right = (_operand_value(a, structure, assignment, numeric) for a in atom.args)
|
|
793
|
+
return (left == right) if atom.predicate == "=" else (left != right)
|
|
794
|
+
|
|
795
|
+
if not atom.args:
|
|
796
|
+
if _is_tptp_boolean_atom(atom):
|
|
797
|
+
return _truth_constant_word(atom) == "$true" # the truth constants, in every structure
|
|
798
|
+
return bool(structure.predicates.get((atom.predicate, 0), False))
|
|
799
|
+
|
|
800
|
+
if atom.predicate in _ORDER_COMPARISONS and len(atom.args) == 2:
|
|
801
|
+
return _order_value(atom, structure, assignment)
|
|
802
|
+
|
|
803
|
+
values = tuple(term_value(a, structure, assignment) for a in atom.args)
|
|
804
|
+
arity = len(atom.args)
|
|
805
|
+
extension = structure.predicates.get((atom.predicate, arity))
|
|
806
|
+
if structure.sorts and arity == 1 and atom.predicate in structure.sorts:
|
|
807
|
+
universe = structure.sort_universe(atom.predicate)
|
|
808
|
+
if extension is None:
|
|
809
|
+
return values[0] in universe
|
|
810
|
+
return extension is not None and values in extension
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
def _extend(assignment: Mapping[str, Any], name: str, value: Any) -> Dict[str, Any]:
|
|
814
|
+
"""Return a copy of the assignment with ``name`` bound to ``value``.
|
|
815
|
+
|
|
816
|
+
The input mapping is never mutated (functional style).
|
|
817
|
+
"""
|
|
818
|
+
extended = dict(assignment)
|
|
819
|
+
extended[name] = value
|
|
820
|
+
return extended
|
|
821
|
+
|
|
822
|
+
|
|
823
|
+
def satisfies(
|
|
824
|
+
formula: Node,
|
|
825
|
+
structure: Structure,
|
|
826
|
+
assignment: Optional[Mapping[str, Any]] = None,
|
|
827
|
+
) -> bool:
|
|
828
|
+
"""Return whether ``structure`` satisfies ``formula`` under ``assignment``.
|
|
829
|
+
|
|
830
|
+
``assignment`` maps logical variable names to individuals; it defaults to
|
|
831
|
+
the empty assignment (appropriate for a sentence with no free variables).
|
|
832
|
+
|
|
833
|
+
Connectives follow the classical truth tables. ``∀x φ`` holds iff every
|
|
834
|
+
individual of the domain satisfies ``φ`` with ``x`` bound to it; ``∃x φ``
|
|
835
|
+
iff some individual does. A :class:`SortedQuantifier` ranges over the named
|
|
836
|
+
sort's universe instead of the whole domain.
|
|
837
|
+
|
|
838
|
+
Raises:
|
|
839
|
+
ValueError: on a Łukasiewicz node (use the fuzzy evaluator — Tarski is
|
|
840
|
+
two-valued) or a lambda node (eliminate it first), or on an unknown
|
|
841
|
+
quantifier type or node type.
|
|
842
|
+
IllegalStructureError: when ``structure`` is not a structure of the
|
|
843
|
+
many-sorted definition and the evaluation reads the part that is
|
|
844
|
+
wrong: an empty sort or one holding a non-domain element, a name
|
|
845
|
+
that is a sort and a unary predicate with two extensions, a sorted
|
|
846
|
+
constant outside its sort (see the module docstring and
|
|
847
|
+
:func:`check_structure`). A subclass of ``ValueError``.
|
|
848
|
+
NotImplementedError: when ``formula`` holds a :class:`Number` and a constant
|
|
849
|
+
spelled like its value (``Number(1)`` next to ``Constant('1')``, or
|
|
850
|
+
``Number(1.0)`` next to ``Constant('1')``): a structure holds ONE entry
|
|
851
|
+
``'1'`` for both, so it could not tell them apart, and the kit refuses to
|
|
852
|
+
merge a numeral with the constant of the same spelling. Also when a cardinality
|
|
853
|
+
``|{v : φ}|`` is not an operand of a comparison with a number (an argument of a
|
|
854
|
+
predicate or a function, or compared with an individual): it is a natural number
|
|
855
|
+
the evaluator counts, not an element of the domain, so it has no value there.
|
|
856
|
+
"""
|
|
857
|
+
_check_formula_once(formula)
|
|
858
|
+
return _evaluate(formula, structure, {} if assignment is None else assignment)
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
def _evaluate(formula: Node, structure: Structure, assignment: Mapping[str, Any]) -> bool:
|
|
862
|
+
""":func:`satisfies` without the check of the formula as a whole, which the entry made.
|
|
863
|
+
|
|
864
|
+
Every recursive step lands here and not on :func:`satisfies`, so a formula is checked
|
|
865
|
+
once, when it is handed in, and not once per subformula and quantified individual.
|
|
866
|
+
"""
|
|
867
|
+
if isinstance(formula, Atom):
|
|
868
|
+
# `$true` / `$false` with no arguments are TPTP's DEFINED propositions (the
|
|
869
|
+
# kit's TPTP reader produces them), not letters of the user's: the same
|
|
870
|
+
# reading to_z3 and the TPTP writers give them.
|
|
871
|
+
if _is_tptp_boolean_atom(formula):
|
|
872
|
+
return _truth_constant_word(formula) == "$true"
|
|
873
|
+
return _atom_value(formula, structure, assignment)
|
|
874
|
+
|
|
875
|
+
if isinstance(formula, Not):
|
|
876
|
+
return not _evaluate(formula.formula, structure, assignment)
|
|
877
|
+
|
|
878
|
+
if isinstance(formula, And):
|
|
879
|
+
return (_evaluate(formula.left, structure, assignment)
|
|
880
|
+
and _evaluate(formula.right, structure, assignment))
|
|
881
|
+
|
|
882
|
+
if isinstance(formula, Or):
|
|
883
|
+
return (_evaluate(formula.left, structure, assignment)
|
|
884
|
+
or _evaluate(formula.right, structure, assignment))
|
|
885
|
+
|
|
886
|
+
if isinstance(formula, Xor):
|
|
887
|
+
return (_evaluate(formula.left, structure, assignment)
|
|
888
|
+
!= _evaluate(formula.right, structure, assignment))
|
|
889
|
+
|
|
890
|
+
if isinstance(formula, Implies):
|
|
891
|
+
return ((not _evaluate(formula.left, structure, assignment))
|
|
892
|
+
or _evaluate(formula.right, structure, assignment))
|
|
893
|
+
|
|
894
|
+
if isinstance(formula, Iff):
|
|
895
|
+
return (_evaluate(formula.left, structure, assignment)
|
|
896
|
+
== _evaluate(formula.right, structure, assignment))
|
|
897
|
+
|
|
898
|
+
if isinstance(formula, Quantifier):
|
|
899
|
+
return _eval_quantifier(
|
|
900
|
+
formula.type, formula.variable.name, structure.domain,
|
|
901
|
+
formula.formula, structure, assignment,
|
|
902
|
+
)
|
|
903
|
+
|
|
904
|
+
if isinstance(formula, SortedQuantifier):
|
|
905
|
+
universe = structure.sort_universe(formula.sort)
|
|
906
|
+
return _eval_quantifier(
|
|
907
|
+
formula.type, formula.variable.name, universe,
|
|
908
|
+
formula.formula, structure, assignment,
|
|
909
|
+
)
|
|
910
|
+
|
|
911
|
+
if isinstance(formula, (Count, SortedCount)):
|
|
912
|
+
# ∃≥n / ∃≤n / ∃=n: count the witnesses, compare against the bound. The whole
|
|
913
|
+
# (finite) universe is walked — ``le`` and ``eq`` need the exact count anyway,
|
|
914
|
+
# and the universes here are the small ones the model search enumerates.
|
|
915
|
+
witnesses = sum(1 for _ in _witnesses(formula, structure, assignment))
|
|
916
|
+
n = formula.n.value
|
|
917
|
+
if formula.op == "ge":
|
|
918
|
+
return witnesses >= n
|
|
919
|
+
if formula.op == "le":
|
|
920
|
+
return witnesses <= n
|
|
921
|
+
if formula.op == "eq":
|
|
922
|
+
return witnesses == n
|
|
923
|
+
raise ValueError(f"Unknown counting-quantifier op: {formula.op!r}")
|
|
924
|
+
|
|
925
|
+
if isinstance(formula, _FUZZY_TYPES):
|
|
926
|
+
raise ValueError(
|
|
927
|
+
f"Cannot evaluate Łukasiewicz node {type(formula).__name__} with the "
|
|
928
|
+
"two-valued Tarskian evaluator; use the fuzzy evaluator instead."
|
|
929
|
+
)
|
|
930
|
+
|
|
931
|
+
if isinstance(formula, _LAMBDA_TYPES):
|
|
932
|
+
raise ValueError(
|
|
933
|
+
f"Cannot evaluate lambda node {type(formula).__name__}; beta-reduce "
|
|
934
|
+
"and lambda-eliminate the formula before calling satisfies."
|
|
935
|
+
)
|
|
936
|
+
|
|
937
|
+
raise ValueError(
|
|
938
|
+
f"satisfies: unsupported node type {type(formula).__name__}."
|
|
939
|
+
)
|
|
940
|
+
|
|
941
|
+
|
|
942
|
+
def _eval_quantifier(
|
|
943
|
+
qtype: str,
|
|
944
|
+
var_name: str,
|
|
945
|
+
universe: Iterable[Any],
|
|
946
|
+
body: Node,
|
|
947
|
+
structure: Structure,
|
|
948
|
+
assignment: Mapping[str, Any],
|
|
949
|
+
) -> bool:
|
|
950
|
+
"""Evaluate a quantifier over a given universe of individuals.
|
|
951
|
+
|
|
952
|
+
``∀`` holds iff the body holds for every individual; ``∃`` iff for some.
|
|
953
|
+
Each candidate is bound in a fresh copy of the assignment (no mutation).
|
|
954
|
+
"""
|
|
955
|
+
if qtype in _FORALL:
|
|
956
|
+
return all(
|
|
957
|
+
_evaluate(body, structure, _extend(assignment, var_name, d))
|
|
958
|
+
for d in universe
|
|
959
|
+
)
|
|
960
|
+
if qtype in _EXISTS:
|
|
961
|
+
return any(
|
|
962
|
+
_evaluate(body, structure, _extend(assignment, var_name, d))
|
|
963
|
+
for d in universe
|
|
964
|
+
)
|
|
965
|
+
raise ValueError(f"Unknown quantifier type: {qtype!r}")
|
|
966
|
+
|
|
967
|
+
|
|
968
|
+
def models(formula: Node, structure: Structure) -> bool:
|
|
969
|
+
"""Convenience alias: ``satisfies(formula, structure, {})``.
|
|
970
|
+
|
|
971
|
+
Reads as "structure models formula" — the sentence is evaluated under the
|
|
972
|
+
empty assignment, so it is meaningful for closed formulas (no free
|
|
973
|
+
variables).
|
|
974
|
+
"""
|
|
975
|
+
return satisfies(formula, structure, {})
|