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,3586 @@
|
|
|
1
|
+
"""Z3 environment, base Node class, classical FOL nodes, registry, and Lark transformer."""
|
|
2
|
+
|
|
3
|
+
import contextvars
|
|
4
|
+
import functools
|
|
5
|
+
import re
|
|
6
|
+
import types
|
|
7
|
+
from decimal import Decimal
|
|
8
|
+
from typing import Any, Callable, List, Optional, Tuple, TypeVar, Union, Dict, cast
|
|
9
|
+
from lark import Transformer
|
|
10
|
+
from dataclasses import dataclass, fields
|
|
11
|
+
|
|
12
|
+
import z3
|
|
13
|
+
|
|
14
|
+
from . import _identifiers
|
|
15
|
+
from ._tptp_symbols import check_variable_names as _check_variable_names
|
|
16
|
+
from ._tptp_symbols import guard_class as _guard_to_tptp
|
|
17
|
+
from ._tptp_symbols import is_tptp_boolean_atom as _is_tptp_boolean_atom
|
|
18
|
+
from ._tptp_symbols import truth_constant_word as _truth_constant_word
|
|
19
|
+
from .naming import ParsingError
|
|
20
|
+
|
|
21
|
+
_SORT = z3.DeclareSort("S")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def numeral_key(value) -> str:
|
|
25
|
+
"""The text a numeral is known by: ONE text per VALUE.
|
|
26
|
+
|
|
27
|
+
``Number(1) == Number(1.0)`` is ``True`` in the kit (the two hash alike), so ``1``, ``1.0``
|
|
28
|
+
and ``01`` are one numeral and a route that makes a constant of a numeral makes ONE
|
|
29
|
+
constant of them. An integral value is written as an integer (``1.0`` and ``1`` are
|
|
30
|
+
``'1'``, ``-0.0`` is ``'0'``), any other value as ``str`` of it (``'2.5'``, ``'-1'``,
|
|
31
|
+
``'1e-07'``). Two numerals have the same key exactly when they are equal.
|
|
32
|
+
|
|
33
|
+
A :class:`Number` already stores an integral float as the integer it equals, so the key of
|
|
34
|
+
a node's value is ``str`` of it; the function takes any value, a raw ``1.0`` too.
|
|
35
|
+
"""
|
|
36
|
+
if isinstance(value, bool):
|
|
37
|
+
value = int(value)
|
|
38
|
+
elif isinstance(value, float) and value.is_integer():
|
|
39
|
+
value = int(value)
|
|
40
|
+
return str(value)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
#: What a Z3 name ends in when the symbol is a VARIABLE (``x`` is written ``x!v``), and what a
|
|
44
|
+
#: constant whose own name already ends that way, or in this, gets appended (``x!v`` is written
|
|
45
|
+
#: ``x!v!c``). A variable's name always ends in the first mark and a constant's never does, so a
|
|
46
|
+
#: constant and a variable of one name are two symbols, and the two maps are injective.
|
|
47
|
+
_VARIABLE_MARK = "!v"
|
|
48
|
+
_ESCAPE_MARK = "!c"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def z3_constant_name(name: str) -> str:
|
|
52
|
+
"""The name of the Z3 symbol of the constant ``name``: the name itself, except for a name
|
|
53
|
+
that ends in ``!v`` or ``!c``, which gets ``!c`` appended so that no constant is spelled
|
|
54
|
+
like a variable's symbol (see :func:`z3_variable_name`)."""
|
|
55
|
+
return name + _ESCAPE_MARK if name.endswith((_VARIABLE_MARK, _ESCAPE_MARK)) else name
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def z3_variable_name(name: str) -> str:
|
|
59
|
+
"""The name of the Z3 symbol of the variable ``name``: ``name`` followed by ``!v``."""
|
|
60
|
+
return name + _VARIABLE_MARK
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def kit_name_of_z3_symbol(z3_name: str) -> Tuple[str, bool]:
|
|
64
|
+
"""Read the name of a Z3 constant of sort ``S`` back as ``(kit name, is it a variable)``.
|
|
65
|
+
|
|
66
|
+
The inverse of :func:`z3_variable_name` and of :func:`z3_constant_name`, and of nothing
|
|
67
|
+
else: ``x!v`` is the variable ``x``, ``x!v!c`` the constant ``x!v``, ``x`` the constant
|
|
68
|
+
``x``. A name that neither writer produces is a constant of exactly that name: ``x!c``
|
|
69
|
+
is not written for any constant (the constant ``x`` is written ``x``, and only a name
|
|
70
|
+
that already ends in a mark gets ``!c`` appended), so a text that holds the symbols
|
|
71
|
+
``x`` and ``x!c`` reads them as two constants, ``x`` and ``x!c``, never as one. The
|
|
72
|
+
function reads ONE name, so it is no more than the inverse of the writers: a text that holds
|
|
73
|
+
``a!c`` (no writer's) and ``a!c!c`` (the writer's name of the constant ``a!c``) would be read
|
|
74
|
+
as one constant, ``a!c``, by calling it on each; the reader of a text
|
|
75
|
+
(:func:`~unicode_logic_kit.atp.z3_input.from_z3`) reads the second as written, ``a!c!c``.
|
|
76
|
+
Only the names of the nullary symbols of sort ``S`` are written this way (a function, a
|
|
77
|
+
predicate and a proposition keep their names), so only those are to be read with it.
|
|
78
|
+
"""
|
|
79
|
+
if z3_name.endswith(_VARIABLE_MARK):
|
|
80
|
+
return z3_name[:-len(_VARIABLE_MARK)], True
|
|
81
|
+
if z3_name.endswith(_ESCAPE_MARK):
|
|
82
|
+
stripped = z3_name[:-len(_ESCAPE_MARK)]
|
|
83
|
+
if stripped.endswith((_VARIABLE_MARK, _ESCAPE_MARK)):
|
|
84
|
+
return stripped, False
|
|
85
|
+
return z3_name, False
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def check_z3_name(name: str) -> None:
|
|
89
|
+
"""Refuse a symbol name that the Z3 C API cannot carry.
|
|
90
|
+
|
|
91
|
+
Z3 reads a name as a C string: it ends at the first NUL character, so ``a\\x00b`` and
|
|
92
|
+
``a\\x00c`` would be the one symbol ``a``, and a lone surrogate (which no UTF-8 text
|
|
93
|
+
holds) makes the call raise ``UnicodeEncodeError``. A problem that names two things
|
|
94
|
+
alike is not the problem that was asked, so the name is refused before anything is
|
|
95
|
+
declared, by every translation into Z3 (:class:`Z3Env`, the arithmetic environment).
|
|
96
|
+
|
|
97
|
+
Raises:
|
|
98
|
+
NotImplementedError: the name holds a NUL character or a lone surrogate.
|
|
99
|
+
"""
|
|
100
|
+
if "\x00" in name:
|
|
101
|
+
raise NotImplementedError(
|
|
102
|
+
f"to_z3: the name {name!r} holds a NUL character, which Z3 reads as the end of a name "
|
|
103
|
+
f"(so it would be the symbol {name.split(chr(0))[0]!r}). Rename the symbol.")
|
|
104
|
+
try:
|
|
105
|
+
name.encode("utf-8")
|
|
106
|
+
except UnicodeEncodeError:
|
|
107
|
+
raise NotImplementedError(
|
|
108
|
+
f"to_z3: the name {name!r} holds a lone surrogate, which is no text that Z3 can take "
|
|
109
|
+
f"as the name of a symbol. Rename the symbol.") from None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def numeral_constant_clash(text: str):
|
|
113
|
+
"""Refuse a numeral and a constant that are one symbol.
|
|
114
|
+
|
|
115
|
+
A numeral is translated to the symbol of its own text (:func:`numeral_key`), so
|
|
116
|
+
``Number(1)`` and a constant named ``1`` (``Constant('1')``) are the same Z3
|
|
117
|
+
symbol and ``P(1)`` would say what ``P('1')`` says. The problem writers for TPTP
|
|
118
|
+
refuse the pair for the same reason. Raised by :class:`Z3Env` and by the cvc5
|
|
119
|
+
sanitiser. A VARIABLE spelled like a numeral is another symbol and is not refused.
|
|
120
|
+
|
|
121
|
+
Raises:
|
|
122
|
+
NotImplementedError: always, naming the text and what to do instead.
|
|
123
|
+
"""
|
|
124
|
+
raise NotImplementedError(
|
|
125
|
+
f"to_z3: the numeral {text} and a constant named {text!r} are one symbol "
|
|
126
|
+
f"(a numeral is the symbol of its own text), so the problem would say about one "
|
|
127
|
+
f"thing what it says about two. Rename the constant, or write the number as a "
|
|
128
|
+
f"constant of another name.")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# =========================
|
|
132
|
+
# Z3 Environment
|
|
133
|
+
# =========================
|
|
134
|
+
|
|
135
|
+
class Z3Env:
|
|
136
|
+
"""Tracks declared Z3 symbols. Single sort for all terms.
|
|
137
|
+
|
|
138
|
+
**What is one symbol.** A constant is keyed on its name; a function and a
|
|
139
|
+
predicate on ``(name, arity)`` each, in a table of their own. So ``P(a)`` and
|
|
140
|
+
``P(a, b)`` are two predicates, ``f(a)`` and ``f(a, b)`` two functions,
|
|
141
|
+
``P(f(a))`` with a predicate ``P`` and a function ``P`` two symbols, and the
|
|
142
|
+
guard predicate ``Car`` of a sort (arity 1) is not the predicate ``Car`` of
|
|
143
|
+
``Car(x, y)`` (arity 2). A function of no arguments is the constant of its name;
|
|
144
|
+
a predicate of no arguments (a proposition) is not.
|
|
145
|
+
|
|
146
|
+
**A variable is a symbol of its own.** A :class:`~unicode_logic_kit.fol.nodes.Variable`
|
|
147
|
+
and a :class:`~unicode_logic_kit.fol.nodes.Constant` of one name are two symbols, in
|
|
148
|
+
every position: the quantifier of ``∀x P(x, c)`` with ``c = Constant('x')`` binds the
|
|
149
|
+
variable and leaves the constant alone. The Z3 symbol of the variable ``x`` is named
|
|
150
|
+
``x!v`` and a constant's is named as it is, except that a constant whose name ends in
|
|
151
|
+
``!v`` or ``!c`` gets ``!c`` appended (:func:`z3_constant_name`), so no constant can be
|
|
152
|
+
spelled like a variable's symbol, and the naming needs no state: two environments, or
|
|
153
|
+
two translations with no environment at all, agree on every name. ``variables_apart=False``
|
|
154
|
+
names a variable as it is named, like a constant; it is for a caller that has already
|
|
155
|
+
given every symbol of the problem a name of its own, in ONE namespace (the SMT-LIB text
|
|
156
|
+
routes do: their sanitiser gives every predicate, function, constant and variable a token
|
|
157
|
+
that no other has, and none that ends in ``!v`` or ``!c``, and they lower every counting
|
|
158
|
+
quantifier before they sanitise, so that the witnesses are in that namespace too) and wants
|
|
159
|
+
the text to hold the names it writes. A name minted for this environment by a ``to_z3``
|
|
160
|
+
method (the witnesses of a counting quantifier, the variable of a sort-axiom) is a
|
|
161
|
+
variable, so with the default naming it is a symbol ``x0!v`` that no name of a problem can
|
|
162
|
+
be, and needs no avoid set.
|
|
163
|
+
|
|
164
|
+
**One exception, refused.** The numeral ``Number(1)`` is written as the symbol of its
|
|
165
|
+
VALUE (``Number(1.0)`` is the same constant, see :func:`numeral_key`), which is also what
|
|
166
|
+
a constant named ``1`` is, so the two would be ONE Z3 symbol and ``P(1)`` would say the
|
|
167
|
+
same as ``P('1')`` (a TPTP writer refuses the pair for the same reason). The environment
|
|
168
|
+
remembers which kind of node first asked for a name and raises
|
|
169
|
+
:class:`NotImplementedError` when a numeral and a constant meet on one name. Translate
|
|
170
|
+
every formula of a problem through ONE environment (``to_z3(env)``) and the refusal
|
|
171
|
+
covers the whole problem, not only one formula.
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
def __init__(self, variables_apart: bool = True):
|
|
175
|
+
"""Initialise empty symbol, function, and predicate tables."""
|
|
176
|
+
self.variables_apart = variables_apart
|
|
177
|
+
self.symbols: Dict[str, z3.ExprRef] = {}
|
|
178
|
+
self.variables: Dict[str, z3.ExprRef] = {}
|
|
179
|
+
self.funcs: Dict[Tuple[str, int], z3.FuncDeclRef] = {}
|
|
180
|
+
self.preds: Dict[Tuple[str, int], z3.FuncDeclRef] = {}
|
|
181
|
+
# name -> "numeral" / "name": which kind of node asked for the symbol first
|
|
182
|
+
self._claims: Dict[str, str] = {}
|
|
183
|
+
|
|
184
|
+
def copy(self) -> "Z3Env":
|
|
185
|
+
"""An independent environment that knows everything this one knows."""
|
|
186
|
+
other = Z3Env(self.variables_apart)
|
|
187
|
+
other.symbols.update(self.symbols)
|
|
188
|
+
other.variables.update(self.variables)
|
|
189
|
+
other.funcs.update(self.funcs)
|
|
190
|
+
other.preds.update(self.preds)
|
|
191
|
+
other._claims.update(self._claims)
|
|
192
|
+
return other
|
|
193
|
+
|
|
194
|
+
def _claim(self, name: str, kind: str) -> None:
|
|
195
|
+
"""Record that ``kind`` (``"numeral"`` or ``"name"``) uses the symbol ``name``; refuse a mixture."""
|
|
196
|
+
seen = self._claims.setdefault(name, kind)
|
|
197
|
+
if seen != kind:
|
|
198
|
+
numeral_constant_clash(name)
|
|
199
|
+
|
|
200
|
+
def get_symbol(self, name: str, numeral: bool = False) -> z3.ExprRef:
|
|
201
|
+
"""Get or create the Z3 constant of the constant (or numeral) ``name``.
|
|
202
|
+
|
|
203
|
+
``numeral=True`` is the call of :class:`Number`, whose symbol is named by the text of
|
|
204
|
+
its value; it is refused (``NotImplementedError``) when a constant of the same name was
|
|
205
|
+
met, and the other way round. A variable is not asked for here (:meth:`get_variable`).
|
|
206
|
+
"""
|
|
207
|
+
self._claim(name, "numeral" if numeral else "name")
|
|
208
|
+
if name not in self.symbols:
|
|
209
|
+
check_z3_name(name)
|
|
210
|
+
self.symbols[name] = z3.Const(z3_constant_name(name), _SORT)
|
|
211
|
+
return self.symbols[name]
|
|
212
|
+
|
|
213
|
+
def get_variable(self, name: str) -> z3.ExprRef:
|
|
214
|
+
"""Get or create the Z3 constant that stands for the variable ``name``.
|
|
215
|
+
|
|
216
|
+
A symbol of its own, apart from the constant of the same name (see the class
|
|
217
|
+
docstring), so a quantifier over it never captures that constant.
|
|
218
|
+
"""
|
|
219
|
+
if not self.variables_apart:
|
|
220
|
+
return self.get_symbol(name)
|
|
221
|
+
if name not in self.variables:
|
|
222
|
+
check_z3_name(name)
|
|
223
|
+
self.variables[name] = z3.Const(z3_variable_name(name), _SORT)
|
|
224
|
+
return self.variables[name]
|
|
225
|
+
|
|
226
|
+
def get_func(self, name: str, arity: int) -> z3.FuncDeclRef:
|
|
227
|
+
"""Get or create an uninterpreted Z3 function of the given arity mapping S^arity -> S.
|
|
228
|
+
|
|
229
|
+
Keyed on ``(name, arity)``: one name at two arities is two functions.
|
|
230
|
+
"""
|
|
231
|
+
if arity == 0:
|
|
232
|
+
self._claim(name, "name") # a function of no arguments is a constant
|
|
233
|
+
key = (name, arity)
|
|
234
|
+
if key not in self.funcs:
|
|
235
|
+
check_z3_name(name)
|
|
236
|
+
z3_name = z3_constant_name(name) if arity == 0 else name
|
|
237
|
+
self.funcs[key] = z3.Function(z3_name, *([_SORT] * arity), _SORT)
|
|
238
|
+
return self.funcs[key]
|
|
239
|
+
|
|
240
|
+
def get_pred(self, name: str, arity: int) -> z3.FuncDeclRef:
|
|
241
|
+
"""Get or create an uninterpreted Z3 predicate of the given arity mapping S^arity -> Bool.
|
|
242
|
+
|
|
243
|
+
Keyed on ``(name, arity)``: one name at two arities is two predicates.
|
|
244
|
+
"""
|
|
245
|
+
key = (name, arity)
|
|
246
|
+
if key not in self.preds:
|
|
247
|
+
check_z3_name(name)
|
|
248
|
+
self.preds[key] = z3.Function(name, *([_SORT] * arity), z3.BoolSort())
|
|
249
|
+
return self.preds[key]
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
# =========================
|
|
253
|
+
# Base Node
|
|
254
|
+
# =========================
|
|
255
|
+
|
|
256
|
+
class Node:
|
|
257
|
+
"""Base class for all AST nodes."""
|
|
258
|
+
|
|
259
|
+
def __init_subclass__(cls, **kwargs):
|
|
260
|
+
"""Guard the new class's ``to_tptp`` (see :meth:`to_tptp`).
|
|
261
|
+
|
|
262
|
+
Whatever ``to_tptp`` the class resolves to, its own or one inherited
|
|
263
|
+
from a mixin, is replaced by the single-formula collision guard of
|
|
264
|
+
:mod:`unicode_logic_kit.fol._tptp_symbols`. This is what covers every
|
|
265
|
+
node family without each one being edited, and a family added later
|
|
266
|
+
without anyone remembering to ask.
|
|
267
|
+
"""
|
|
268
|
+
super().__init_subclass__(**kwargs)
|
|
269
|
+
_guard_to_tptp(cls)
|
|
270
|
+
|
|
271
|
+
def _tptp_symbol(self):
|
|
272
|
+
"""The name this node itself writes into TPTP text, or ``None``.
|
|
273
|
+
|
|
274
|
+
``(resolver, kit name)``: the kit name in the AST and the module-level
|
|
275
|
+
function (:func:`_predicate_symbol`, :func:`_function_symbol`,
|
|
276
|
+
:func:`_constant_symbol`) that turns it into ``(namespace, word, kind)``,
|
|
277
|
+
the identifier :meth:`to_tptp` writes for it. Only a node that writes a
|
|
278
|
+
NAME, a numeral or a variable overrides this (:class:`Atom`,
|
|
279
|
+
:class:`Function`, :class:`Constant`, :class:`Measure`, ``SortedConstant``,
|
|
280
|
+
:class:`Number`, :class:`Variable`); a node that LOWERS to others
|
|
281
|
+
(``SortedQuantifier`` writes the guard predicate of its sort, a ``Count``
|
|
282
|
+
its witnesses) names nothing itself and is seen through the nodes it is
|
|
283
|
+
rendered as. The single-formula guard and the
|
|
284
|
+
problem writers' collision check both read it, which is why they cannot
|
|
285
|
+
disagree about what a node writes. It only NAMES the symbol (it runs once
|
|
286
|
+
per rendered node); the fold runs once per distinct name, when the symbols
|
|
287
|
+
are checked.
|
|
288
|
+
"""
|
|
289
|
+
return None
|
|
290
|
+
|
|
291
|
+
def to_dict(self) -> dict:
|
|
292
|
+
"""Serialise this node to a JSON-compatible dictionary."""
|
|
293
|
+
raise NotImplementedError
|
|
294
|
+
|
|
295
|
+
def to_z3(self, env: Z3Env = None) -> z3.ExprRef:
|
|
296
|
+
"""Translate this node into a Z3 expression using the given environment."""
|
|
297
|
+
raise NotImplementedError
|
|
298
|
+
|
|
299
|
+
def to_prover9(self) -> str:
|
|
300
|
+
"""Render this node as a Prover9-syntax string.
|
|
301
|
+
|
|
302
|
+
**What it sees.** The OUTERMOST call of a node that has a binder in it sees the whole
|
|
303
|
+
node and writes text that means it: a binder that sits inside the scope of a binder of
|
|
304
|
+
its own name (the free variables of the node count: Prover9 closes a formula
|
|
305
|
+
universally) is renamed to a fresh variable, because LADR would rename it itself, to
|
|
306
|
+
``x0``, ``x1``, ... , and a constant of that spelling would then be bound by it; the
|
|
307
|
+
witnesses of a counting quantifier are fresh against every name of the node, of every
|
|
308
|
+
kind, compared case-folded (Prover9 writes a variable in upper case, so ``x0`` and
|
|
309
|
+
``X0`` are one variable there); and sorted nodes are lowered first. The problem writer
|
|
310
|
+
makes the same preparation of every formula of a problem, with the same functions.
|
|
311
|
+
|
|
312
|
+
**What it cannot see.** It renders ONE node and has no whole-problem view.
|
|
313
|
+
A variable is written as the upper-case of its name, so two variables that
|
|
314
|
+
differ only in case are one variable in the text unless a binder is renamed:
|
|
315
|
+
a binder inside the scope of another is (``∀x ∃X R(x, X)`` is written
|
|
316
|
+
``(all X (exists X0 R(X, X0)))``). What no renaming of a binder repairs is
|
|
317
|
+
refused by name instead of written as one variable: an occurrence that a
|
|
318
|
+
binder of another spelling encloses (a free ``x`` inside ``∀X``), and two free
|
|
319
|
+
variables of one upper-case name (``P(x) ∧ Q(X)``).
|
|
320
|
+
:func:`unicode_logic_kit.atp.prover9_entailment
|
|
321
|
+
.generate_prover9_input_with_mapping` checks every formula of a problem for
|
|
322
|
+
every such pair, harmless ones included, and refuses it by name (the check
|
|
323
|
+
:meth:`to_tptp` makes on its own, from :mod:`unicode_logic_kit.fol._tptp_symbols`);
|
|
324
|
+
build a problem with it, never by joining ``to_prover9()`` strings. A constant
|
|
325
|
+
or a propositional atom that
|
|
326
|
+
Prover9 would read as a variable (a name that begins with an upper-case
|
|
327
|
+
letter or an underscore) is written in double quotes, which Prover9 never
|
|
328
|
+
reads as a variable (see :meth:`Constant.to_prover9`); the writer renames
|
|
329
|
+
such a symbol instead and records the rename. For the same reason it cannot
|
|
330
|
+
see that one name is used for two symbols: a predicate of two arities, or one
|
|
331
|
+
word as a predicate and as a constant, is ONE symbol to Prover9, which
|
|
332
|
+
refuses the file, and the writer gives the later symbol a name of its own.
|
|
333
|
+
A name that is no word Prover9 reads as one symbol (a space, a non-ASCII
|
|
334
|
+
letter, a ``$``-word) is refused by name; the writer renames it. A numeral
|
|
335
|
+
that is not a digit string (``2.5``, ``-1``) is written in double quotes.
|
|
336
|
+
"""
|
|
337
|
+
raise NotImplementedError
|
|
338
|
+
|
|
339
|
+
def to_tptp(self) -> str:
|
|
340
|
+
"""Render this node as a TPTP-syntax string.
|
|
341
|
+
|
|
342
|
+
**One formula, checked.** The OUTERMOST call refuses (``NotImplementedError``,
|
|
343
|
+
naming both kit names and the word they share) when two DISTINCT names
|
|
344
|
+
of one kind inside this one formula would be written as the same TPTP
|
|
345
|
+
identifier. A name is written with its first character folded to
|
|
346
|
+
lower-case, so ``gaseous`` and ``Gaseous`` (two constants), ``Foo`` and
|
|
347
|
+
``foo`` (two predicates) or ``Bar`` and ``bar`` (two functions) would
|
|
348
|
+
otherwise become one symbol and ``P(gaseous) <-> P(Gaseous)`` would be
|
|
349
|
+
written as a tautology. The check sees every name that reaches the
|
|
350
|
+
text, including those a reduction introduces (the sort guard predicate
|
|
351
|
+
of a ``SortedQuantifier``), and is installed on every node class by
|
|
352
|
+
:meth:`__init_subclass__`; a nested call only records.
|
|
353
|
+
|
|
354
|
+
Three more cases are written as one word, and refused the same way: a
|
|
355
|
+
number and a constant spelled like it (``Number(1)`` and ``Constant('1')``
|
|
356
|
+
are both ``1``), an arithmetic or comparison symbol and a symbol written
|
|
357
|
+
like it (``+`` is ``$sum``, so a function named ``$sum`` is the same
|
|
358
|
+
word), and two variables that are one TPTP variable (``x`` and ``X``:
|
|
359
|
+
``∀x ∃X R(x, X)`` would be written ``![X]: ?[X]: r(X,X)``). A formula that
|
|
360
|
+
binds ``x`` in one place and ``X`` in another, even where they never meet,
|
|
361
|
+
is refused too, rather than analysed for scope.
|
|
362
|
+
|
|
363
|
+
A name that is written as something that is not a TPTP word is refused as
|
|
364
|
+
well, never written as it is: an unquoted TPTP name is a lower-case letter
|
|
365
|
+
followed by letters, digits and underscores, so ``has-part``,
|
|
366
|
+
``2008SummerOlympics``, ``_x`` and a non-ASCII predicate or function name
|
|
367
|
+
(a constant is transliterated, ``θ`` is ``theta``) have no rendering. The
|
|
368
|
+
problem writers rewrite such a name under a legal replacement and return
|
|
369
|
+
the map. A word that starts with ``$`` is one of TPTP's own, and is refused
|
|
370
|
+
as a RESERVED word, with ONE exception: the NULLARY atoms ``$true`` and
|
|
371
|
+
``$false`` are TPTP's defined propositions (this kit's TPTP reader produces
|
|
372
|
+
them), they are written verbatim and are no symbol of the user's, and
|
|
373
|
+
``to_z3`` reads them as true and false. A VARIABLE that is written as no
|
|
374
|
+
TPTP variable (``ä`` is written ``Ä``, ``x-1``, ``1x``) is refused by name
|
|
375
|
+
too; the problem writers rename a variable, which is bound, without
|
|
376
|
+
recording anything.
|
|
377
|
+
|
|
378
|
+
**What it cannot see, and what is not refused.** It sees ONE formula. A
|
|
379
|
+
problem assembled from several ``to_tptp()`` strings can still merge
|
|
380
|
+
``gaseous`` in one premise with ``Gaseous`` in another, so build a
|
|
381
|
+
problem with :func:`unicode_logic_kit.atp.generate_tptp_problem_with_mapping`
|
|
382
|
+
(or the TF0 / TFA writers), which check every premise and the
|
|
383
|
+
conclusion together. A predicate and a function/constant that share a
|
|
384
|
+
word (the class ``Agent`` and the role function ``agent``) are NOT
|
|
385
|
+
refused here: the text is unambiguous by position and this kit's reader
|
|
386
|
+
reads it back, but a prover may not, so the writers rename the term side
|
|
387
|
+
and return the map.
|
|
388
|
+
|
|
389
|
+
**The asymmetry is deliberate, for this release.** A name TPTP cannot
|
|
390
|
+
spell, or a predicate/term clash, is renamed and recorded in a
|
|
391
|
+
``TptpNameMap`` by the writers; two LEGAL names of one kind that fold
|
|
392
|
+
together are refused by name, never renamed, here and in the writers
|
|
393
|
+
alike. The same-kind refusal predates the name map and stays so that no
|
|
394
|
+
existing caller silently receives a symbol renamed behind its back.
|
|
395
|
+
|
|
396
|
+
Raises:
|
|
397
|
+
NotImplementedError: two symbols written as one word, or a name
|
|
398
|
+
that is not a TPTP word (above), or a construct outside the
|
|
399
|
+
classical first-order fragment (modal, second-order,
|
|
400
|
+
Łukasiewicz, lambda, ...), which names itself.
|
|
401
|
+
"""
|
|
402
|
+
raise NotImplementedError
|
|
403
|
+
|
|
404
|
+
@staticmethod
|
|
405
|
+
def from_dict(d: dict) -> "Node":
|
|
406
|
+
"""Deserialise a node from a dictionary produced by to_dict."""
|
|
407
|
+
t = d["_type"]
|
|
408
|
+
if t not in NODE_CLASSES:
|
|
409
|
+
raise ValueError(f"Unknown type: {t}")
|
|
410
|
+
return NODE_CLASSES[t].from_dict(d)
|
|
411
|
+
|
|
412
|
+
_TREE_LABELS = {
|
|
413
|
+
"And": "∧", "Or": "∨", "Xor": "⊕",
|
|
414
|
+
"Implies": "→", "Iff": "↔", "Not": "¬",
|
|
415
|
+
"Contrast": "Ⓒ",
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
def _tree_parts(self):
|
|
419
|
+
"""Return (label, children) for tree rendering.
|
|
420
|
+
|
|
421
|
+
Leaf terms render their value in the label and have no children.
|
|
422
|
+
Atom and Function render the symbol in the label and expose their
|
|
423
|
+
argument nodes. Quantifier shows its type and bound variable.
|
|
424
|
+
Everything else falls back to its dataclass fields, treating any
|
|
425
|
+
Node-valued field as a child.
|
|
426
|
+
"""
|
|
427
|
+
cls = type(self).__name__
|
|
428
|
+
if cls in ("Variable", "Constant"):
|
|
429
|
+
return f"{cls}: {self.name}", []
|
|
430
|
+
if cls == "Number":
|
|
431
|
+
return f"Number: {self.value}", []
|
|
432
|
+
if cls == "Atom":
|
|
433
|
+
return f"Atom: {self.predicate}", list(self.args)
|
|
434
|
+
if cls == "Function":
|
|
435
|
+
return f"Function: {self.name}", list(self.args)
|
|
436
|
+
if cls == "Quantifier":
|
|
437
|
+
return f"{self.type} {self.variable.name}", [self.formula]
|
|
438
|
+
|
|
439
|
+
label = self._TREE_LABELS.get(cls, cls)
|
|
440
|
+
children = []
|
|
441
|
+
for f in fields(self):
|
|
442
|
+
value = getattr(self, f.name)
|
|
443
|
+
if isinstance(value, Node):
|
|
444
|
+
children.append(value)
|
|
445
|
+
elif isinstance(value, (list, tuple)):
|
|
446
|
+
children.extend(c for c in value if isinstance(c, Node))
|
|
447
|
+
return label, children
|
|
448
|
+
|
|
449
|
+
def to_unicode_str(self) -> str:
|
|
450
|
+
"""Render this node back to a Unicode formula string.
|
|
451
|
+
|
|
452
|
+
The result, re-parsed in the matching MSFLParser mode, yields a
|
|
453
|
+
structurally equal AST (parser round-trip): ``parse(n.to_unicode_str())
|
|
454
|
+
== n`` -- for every node that has a text form (see the last paragraph:
|
|
455
|
+
a node that mixes sorted and unsorted occurrences has none). For the classical FOL fragment (``∀ ∃ ¬ ∧ ∨ → ↔ ⊕`` and
|
|
456
|
+
predicates over constants/variables — no lambda, no modal, no
|
|
457
|
+
second-order) this is the B2 roundtrip guarantee, exercised
|
|
458
|
+
example-by-example in ``tests/test_to_unicode_str.py`` and, starting
|
|
459
|
+
from arbitrary hand-built nodes rather than parser output, in
|
|
460
|
+
``tests/test_fol_fragment_roundtrip_b2.py`` (hand-picked
|
|
461
|
+
parenthesisation edge cases plus a seeded randomized property
|
|
462
|
+
search). This is part of this method's STABLE PUBLIC API contract —
|
|
463
|
+
see the module docstring of ``fol/nodes.py``. The renderer lives in
|
|
464
|
+
_msfl_nodes.py (imported lazily to avoid a circular import) because it
|
|
465
|
+
dispatches over both the FOL nodes here and the MSFL/lambda nodes there.
|
|
466
|
+
|
|
467
|
+
**A node that mixes sorted and unsorted occurrences has no text form.**
|
|
468
|
+
The many-sorted text grammar is all-sorted or all-unsorted: a sorted
|
|
469
|
+
quantifier over an unsorted one (``∀x:A ∃y P(x, y)``), a constant written
|
|
470
|
+
``carl:A`` in one place and plain ``carl`` in another, or an unsorted
|
|
471
|
+
quantifier around a sorted constant prints text that every parser refuses
|
|
472
|
+
(``NamingError``), in every mode. The refusal is loud: such text is never
|
|
473
|
+
read back as a different formula. The node itself is a legitimate formula
|
|
474
|
+
(``c:S`` and plain ``c`` are one constant in the kit's semantics, and an
|
|
475
|
+
unsorted variable ranges over the whole universe), so decide, translate
|
|
476
|
+
and export it as a node, or write the sort on every occurrence (or on none)
|
|
477
|
+
before printing it.
|
|
478
|
+
|
|
479
|
+
**A constant is written bare or in quotes.** ``Constant(name)`` is written
|
|
480
|
+
as the bare name when that text reads back as this constant
|
|
481
|
+
(``socrates``, ``c_k2``) and in single quotes otherwise (``'k2'``,
|
|
482
|
+
``'Alice'``, ``'G-910'``, ``'John Doe'``), with ``'`` written ``\\'`` and
|
|
483
|
+
``\\`` written ``\\\\``; a sorted constant is the same text of its name,
|
|
484
|
+
``:``, and the sort (``'k2':Mountain``). So the text reads back as the node
|
|
485
|
+
for every constant that has a text. A name that has none is refused: the
|
|
486
|
+
empty name, a name with a control character, U+007F, U+0085, U+2028,
|
|
487
|
+
U+2029 or a surrogate (``ValueError``), and a name that is not a string
|
|
488
|
+
(``TypeError``). The names of functions, predicates, variables, sorts,
|
|
489
|
+
the subscript of a modal operator (``K_a``) and nominals have no quoted
|
|
490
|
+
form and are written as they are.
|
|
491
|
+
|
|
492
|
+
A route that uses the printed text of an atom as a KEY (a valuation, a
|
|
493
|
+
table of a model, an order, an identifier of a target) does not use this
|
|
494
|
+
text: it writes every constant by its bare name, as before the quoted
|
|
495
|
+
form existed (``key_text`` in ``_msfl_nodes.py``).
|
|
496
|
+
|
|
497
|
+
Raises:
|
|
498
|
+
ValueError: a constant of the node has a name that has no text.
|
|
499
|
+
TypeError: a constant of the node has a name that is not a string.
|
|
500
|
+
"""
|
|
501
|
+
from ._msfl_nodes import _uni
|
|
502
|
+
return _uni(self)
|
|
503
|
+
|
|
504
|
+
def to_latex(self) -> str:
|
|
505
|
+
"""Render this node as a LaTeX math-mode string (no surrounding $…$).
|
|
506
|
+
|
|
507
|
+
Uses the same precedence-driven parenthesisation as to_unicode_str.
|
|
508
|
+
Symbol/function/predicate names are emitted verbatim (no \\mathrm
|
|
509
|
+
wrapping). The renderer lives in _msfl_nodes.py (imported lazily) so it
|
|
510
|
+
can dispatch over both FOL and MSFL/lambda nodes.
|
|
511
|
+
|
|
512
|
+
A constant is written by its name here, never in quotes: the LaTeX text
|
|
513
|
+
of a formula that holds a constant whose name does not read back bare
|
|
514
|
+
(``k2``, ``Alice``, ``G-910``) does not read back as that constant,
|
|
515
|
+
and ``parse_latex`` does not read a quoted one. Use ``to_unicode_str``
|
|
516
|
+
for text that reads back.
|
|
517
|
+
"""
|
|
518
|
+
from ._msfl_nodes import _latex
|
|
519
|
+
return _latex(self)
|
|
520
|
+
|
|
521
|
+
def to_smtlib(self) -> str:
|
|
522
|
+
"""Render this node as a standalone SMT-LIB2 problem (one ``(assert ...)``).
|
|
523
|
+
|
|
524
|
+
A one-line delegation to :func:`unicode_logic_kit.atp.z3_input.to_smtlib`
|
|
525
|
+
with no premises — the general, multi-premise/sanitisation-correct
|
|
526
|
+
exporter promoted from :class:`~unicode_logic_kit.atp.cvc5_backend
|
|
527
|
+
.Cvc5Backend`'s own already-proven translation; see that function's
|
|
528
|
+
docstring for what "sanitisation-correct" buys over a naive
|
|
529
|
+
``to_z3()`` + ``z3.Solver.to_smt2()`` combination. Imported lazily
|
|
530
|
+
(like :meth:`to_latex`) because ``atp.z3_input`` imports from this
|
|
531
|
+
module's own package at load time — mirrors how :meth:`to_z3`
|
|
532
|
+
already crosses the fol/atp module boundary, just one hop further.
|
|
533
|
+
|
|
534
|
+
Raises:
|
|
535
|
+
NotImplementedError: this node (or a descendant) uses a construct
|
|
536
|
+
with no first-order SMT-LIB2 encoding — the same refusal
|
|
537
|
+
:meth:`to_z3` raises for it.
|
|
538
|
+
"""
|
|
539
|
+
from ..atp.z3_input import to_smtlib as _to_smtlib
|
|
540
|
+
return _to_smtlib(self)
|
|
541
|
+
|
|
542
|
+
def _repr_latex_(self) -> Optional[str]:
|
|
543
|
+
"""Jupyter/IPython rich-display hook: LaTeX math-mode rendering.
|
|
544
|
+
|
|
545
|
+
Wraps :meth:`to_latex` in ``$$...$$`` (display math). MUST NOT raise —
|
|
546
|
+
IPython's formatter machinery treats an exception from a ``_repr_*_``
|
|
547
|
+
method as a hard failure of that cell's output, not as "fall back to
|
|
548
|
+
the next formatter". :meth:`to_latex` refuses loudly (``TypeError`` /
|
|
549
|
+
``NotImplementedError``) for a node it cannot render, e.g. a
|
|
550
|
+
third-party ``Node`` subclass the LaTeX dispatcher has never heard of;
|
|
551
|
+
here that refusal is swallowed and reported as "no LaTeX
|
|
552
|
+
representation" (``None``) instead, so IPython falls back to the
|
|
553
|
+
plain ``repr()`` of the node rather than showing a traceback in a
|
|
554
|
+
notebook cell.
|
|
555
|
+
"""
|
|
556
|
+
try:
|
|
557
|
+
return f"$${self.to_latex()}$$"
|
|
558
|
+
except Exception:
|
|
559
|
+
return None
|
|
560
|
+
|
|
561
|
+
def tree_str(self) -> str:
|
|
562
|
+
"""Render the AST as a multi-line ASCII tree using ├──/└── connectors."""
|
|
563
|
+
label, children = self._tree_parts()
|
|
564
|
+
lines = [label]
|
|
565
|
+
for i, child in enumerate(children):
|
|
566
|
+
last = i == len(children) - 1
|
|
567
|
+
branch = "└── " if last else "├── "
|
|
568
|
+
prefix = " " if last else "│ "
|
|
569
|
+
sub = child.tree_str().split("\n")
|
|
570
|
+
lines.append(branch + sub[0])
|
|
571
|
+
lines.extend(prefix + s for s in sub[1:])
|
|
572
|
+
return "\n".join(lines)
|
|
573
|
+
|
|
574
|
+
def to_msfol(self) -> "Node":
|
|
575
|
+
"""Lower Łukasiewicz operators to classical counterparts; recurse into children.
|
|
576
|
+
|
|
577
|
+
Classical and sort-annotated nodes return a structurally equal copy with
|
|
578
|
+
children recursed. Fuzzy operator subclasses override this to substitute
|
|
579
|
+
the corresponding classical node type.
|
|
580
|
+
"""
|
|
581
|
+
return self.map_children(lambda c: c.to_msfol())
|
|
582
|
+
|
|
583
|
+
def _relativize(self, facts: list) -> "Node":
|
|
584
|
+
"""Replace sorted nodes with plain FOL constructs; collect sort-membership atoms.
|
|
585
|
+
|
|
586
|
+
Classical nodes return a structurally equal copy with children recursed.
|
|
587
|
+
SortedQuantifier and SortedConstant override this with their specific rules.
|
|
588
|
+
Fuzzy operator subclasses override to raise RuntimeError — they must be
|
|
589
|
+
eliminated by to_msfol() before _relativize() is called.
|
|
590
|
+
"""
|
|
591
|
+
return self.map_children(lambda c: c._relativize(facts))
|
|
592
|
+
|
|
593
|
+
# ---------------------------------------------------------------
|
|
594
|
+
# Traversal / inspection API
|
|
595
|
+
# ---------------------------------------------------------------
|
|
596
|
+
|
|
597
|
+
def _child_nodes(self) -> List["Node"]:
|
|
598
|
+
"""Return the immediate Node-valued children, in declaration order.
|
|
599
|
+
|
|
600
|
+
Covers both single Node fields and lists of Nodes. Quantifier exposes
|
|
601
|
+
its bound variable here (it is a Node); for a rendering-oriented child
|
|
602
|
+
view see _tree_parts.
|
|
603
|
+
"""
|
|
604
|
+
result: List["Node"] = []
|
|
605
|
+
for f in fields(self):
|
|
606
|
+
val = getattr(self, f.name)
|
|
607
|
+
if isinstance(val, Node):
|
|
608
|
+
result.append(val)
|
|
609
|
+
elif isinstance(val, (list, tuple)):
|
|
610
|
+
result.extend(c for c in val if isinstance(c, Node))
|
|
611
|
+
return result
|
|
612
|
+
|
|
613
|
+
def map_children(self, fn) -> "Node":
|
|
614
|
+
"""Rebuild this node with ``fn`` applied to each immediate Node child.
|
|
615
|
+
|
|
616
|
+
The single point of structural recursion. Each dataclass field is
|
|
617
|
+
handled by kind: a Node field becomes ``fn(value)``; a list/tuple field
|
|
618
|
+
has ``fn`` mapped over its Node elements (non-Node elements pass
|
|
619
|
+
through, container kind preserved); any other field is copied verbatim.
|
|
620
|
+
The node type and field order are preserved.
|
|
621
|
+
|
|
622
|
+
Binders (Lambda, Quantifier, SortedQuantifier) carry their bound
|
|
623
|
+
variable as a plain Node field, so ``fn`` is applied to it too; callers
|
|
624
|
+
that must treat a binder's scope specially should handle that case
|
|
625
|
+
explicitly before delegating here. This is the shared engine behind the
|
|
626
|
+
purely structural recursions (to_msfol, _relativize, beta/eta reduction,
|
|
627
|
+
scope resolution, …), so a new structural node type is handled
|
|
628
|
+
automatically without touching each traversal.
|
|
629
|
+
"""
|
|
630
|
+
new_kwargs = {}
|
|
631
|
+
for f in fields(self):
|
|
632
|
+
val = getattr(self, f.name)
|
|
633
|
+
if isinstance(val, Node):
|
|
634
|
+
new_kwargs[f.name] = fn(val)
|
|
635
|
+
elif isinstance(val, (list, tuple)):
|
|
636
|
+
new_kwargs[f.name] = type(val)(
|
|
637
|
+
fn(c) if isinstance(c, Node) else c for c in val
|
|
638
|
+
)
|
|
639
|
+
else:
|
|
640
|
+
new_kwargs[f.name] = val
|
|
641
|
+
return type(self)(**new_kwargs)
|
|
642
|
+
|
|
643
|
+
def walk(self):
|
|
644
|
+
"""Yield this node and every descendant in pre-order (depth-first).
|
|
645
|
+
|
|
646
|
+
A node comes before its children and the children come left to right, in the
|
|
647
|
+
order of ``Node._child_nodes``. The traversal keeps its own stack, so a formula
|
|
648
|
+
nested thousands of levels deep is walked as readily as a shallow one: it does
|
|
649
|
+
not depend on the interpreter's recursion limit.
|
|
650
|
+
"""
|
|
651
|
+
stack = [self]
|
|
652
|
+
while stack:
|
|
653
|
+
node = stack.pop()
|
|
654
|
+
yield node
|
|
655
|
+
stack.extend(reversed(node._child_nodes()))
|
|
656
|
+
|
|
657
|
+
def subformulas(self):
|
|
658
|
+
"""Yield every sub-node that is a formula (i.e. not an atomic term).
|
|
659
|
+
|
|
660
|
+
Terms (Variable, Constant, Number, Function, SortedConstant, LambdaVar)
|
|
661
|
+
are excluded; everything else reachable is returned in pre-order.
|
|
662
|
+
"""
|
|
663
|
+
return [n for n in self.walk() if type(n).__name__ not in _TERM_NAMES]
|
|
664
|
+
|
|
665
|
+
def atoms(self):
|
|
666
|
+
"""Return all Atom nodes in pre-order (duplicates kept; comparisons included)."""
|
|
667
|
+
return [n for n in self.walk() if isinstance(n, Atom)]
|
|
668
|
+
|
|
669
|
+
def variables(self):
|
|
670
|
+
"""Return the set of logical Variable nodes occurring anywhere (free and bound)."""
|
|
671
|
+
return {n for n in self.walk() if isinstance(n, Variable)}
|
|
672
|
+
|
|
673
|
+
def count(self, cls=None) -> int:
|
|
674
|
+
"""Count nodes in the tree; if cls is given, only nodes of that type."""
|
|
675
|
+
return sum(1 for n in self.walk() if cls is None or isinstance(n, cls))
|
|
676
|
+
|
|
677
|
+
def depth(self) -> int:
|
|
678
|
+
"""Return the height of the tree; a leaf node has depth 1."""
|
|
679
|
+
children = self._child_nodes()
|
|
680
|
+
return 1 + max((c.depth() for c in children), default=0)
|
|
681
|
+
|
|
682
|
+
def to_dot(self) -> str:
|
|
683
|
+
"""Render the AST as a Graphviz DOT digraph string.
|
|
684
|
+
|
|
685
|
+
Uses the same label/child view as tree_str (the bound variable of a
|
|
686
|
+
quantifier is folded into its node label, not shown as a child), so the
|
|
687
|
+
graph mirrors the ASCII tree. No external dependency: returns the source.
|
|
688
|
+
"""
|
|
689
|
+
lines = ["digraph AST {", " node [shape=box];"]
|
|
690
|
+
counter = [0]
|
|
691
|
+
|
|
692
|
+
def emit(node: "Node") -> int:
|
|
693
|
+
my_id = counter[0]
|
|
694
|
+
counter[0] += 1
|
|
695
|
+
label, children = node._tree_parts()
|
|
696
|
+
safe = label.replace("\\", "\\\\").replace('"', '\\"')
|
|
697
|
+
lines.append(f' n{my_id} [label="{safe}"];')
|
|
698
|
+
for child in children:
|
|
699
|
+
child_id = emit(child)
|
|
700
|
+
lines.append(f" n{my_id} -> n{child_id};")
|
|
701
|
+
return my_id
|
|
702
|
+
|
|
703
|
+
emit(self)
|
|
704
|
+
lines.append("}")
|
|
705
|
+
return "\n".join(lines)
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
# ``__init_subclass__`` guards every SUBCLASS; the base's own ``to_tptp`` (it
|
|
709
|
+
# raises, and a subclass without one inherits it) is guarded here so that every
|
|
710
|
+
# node class, ``Node`` included, answers ``is_guarded`` the same way.
|
|
711
|
+
_guard_to_tptp(Node)
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
# =========================
|
|
715
|
+
# Public tree editing (path-addressed replacement) and PATH CONVENTION
|
|
716
|
+
# =========================
|
|
717
|
+
#
|
|
718
|
+
# replace_at is the PUBLIC, node-type-generic counterpart to the private
|
|
719
|
+
# atp.resolution._replace_at (a term-only helper restricted to Atom/Function
|
|
720
|
+
# argument positions — see replace_at's own docstring for the exact
|
|
721
|
+
# difference). It is built on the same structural machinery Node.map_children
|
|
722
|
+
# already uses for every other whole-tree rewrite in this codebase (to_msfol,
|
|
723
|
+
# _relativize, beta/eta reduction, scope resolution, …).
|
|
724
|
+
#
|
|
725
|
+
# PATH CONVENTION — the one thing traversal (fol.spans.traverse), span lookup
|
|
726
|
+
# (fol.spans.SpanMap) and replace_at/node_at must all agree on (spec item
|
|
727
|
+
# A2). A path is a tuple of non-negative ints, addressing a node relative to
|
|
728
|
+
# some root: () addresses the root itself; (i, *rest) addresses rest inside
|
|
729
|
+
# the root's i-th PATH CHILD. _path_children(node) IS Node._child_nodes()
|
|
730
|
+
# (the SAME child order Node.walk/.map_children/.count/.depth already use)
|
|
731
|
+
# for every node type EXCEPT Quantifier, whose bound `variable` is excluded:
|
|
732
|
+
# a Quantifier's ONLY path child is its `formula`, at index 0.
|
|
733
|
+
#
|
|
734
|
+
# The exclusion is deliberate and spec-driven (fol.spans's module docstring,
|
|
735
|
+
# "WHY THE BOUND VARIABLE IS EXCLUDED"): a quantifier's HEAD span already
|
|
736
|
+
# covers its symbol together with its bound variable as one occurrence
|
|
737
|
+
# ("∀ x"), so exposing the variable AGAIN as a separately path-addressable
|
|
738
|
+
# child would double-count that one piece of source text. It mirrors how
|
|
739
|
+
# Node._tree_parts()/to_dot already fold the bound variable into the node's
|
|
740
|
+
# *label* rather than its *children* — here it is folded into the node's
|
|
741
|
+
# *head span* rather than its *path children*, same idea, different API.
|
|
742
|
+
# Scoped to Quantifier alone (not every binder — Count, Cardinality,
|
|
743
|
+
# SortedQuantifier, Lambda, … keep the default, unexcluded view) because
|
|
744
|
+
# Quantifier is the one binder the span layer's target FOL fragment covers;
|
|
745
|
+
# widening the exclusion to every binder is a separate decision left to
|
|
746
|
+
# whichever future change extends spans past that fragment.
|
|
747
|
+
|
|
748
|
+
def _path_children(node: "Node") -> List["Node"]:
|
|
749
|
+
"""The path-addressable children of ``node``, in path-index order — see
|
|
750
|
+
the PATH CONVENTION comment above."""
|
|
751
|
+
if isinstance(node, Quantifier):
|
|
752
|
+
return [node.formula]
|
|
753
|
+
return node._child_nodes()
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
def node_at(root: "Node", path: Tuple[int, ...]) -> "Node":
|
|
757
|
+
"""Return the node ``path`` addresses in ``root``'s tree (see the PATH
|
|
758
|
+
CONVENTION comment above ``_path_children``).
|
|
759
|
+
|
|
760
|
+
Raises ``IndexError`` if ``path`` does not address a node in this tree —
|
|
761
|
+
an index out of range at some prefix of ``path`` — never returns a guess.
|
|
762
|
+
"""
|
|
763
|
+
node = root
|
|
764
|
+
for depth, idx in enumerate(path):
|
|
765
|
+
children = _path_children(node)
|
|
766
|
+
if not isinstance(idx, int) or not (0 <= idx < len(children)):
|
|
767
|
+
raise IndexError(
|
|
768
|
+
f"node_at: path {path!r} is invalid at position {depth} "
|
|
769
|
+
f"(index {idx!r}) — a {type(node).__name__} node has "
|
|
770
|
+
f"{len(children)} path child(ren) there."
|
|
771
|
+
)
|
|
772
|
+
node = children[idx]
|
|
773
|
+
return node
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
def _replace_child_at(node: "Node", index: int, new_child: "Node") -> "Node":
|
|
777
|
+
"""Rebuild ``node`` with its ``index``-th PATH child (see
|
|
778
|
+
``_path_children``) replaced by ``new_child``; every other field is
|
|
779
|
+
copied verbatim and every other Node child is passed through BY
|
|
780
|
+
REFERENCE — the exact same object, not a copy.
|
|
781
|
+
|
|
782
|
+
A :class:`Quantifier` (whose one path child is its ``formula``, NOT
|
|
783
|
+
``_child_nodes()``'s ``[variable, formula]``) is rebuilt directly via
|
|
784
|
+
``Quantifier(node.type, node.variable, new_child)`` rather than through
|
|
785
|
+
``map_children`` — ``map_children`` is defined over ``_child_nodes()``
|
|
786
|
+
and applies its function to the bound variable too (see its own
|
|
787
|
+
docstring), which is exactly the field this path convention excludes.
|
|
788
|
+
Every other node type's path children ARE ``_child_nodes()``, so
|
|
789
|
+
``map_children`` (with a counting closure that substitutes only the
|
|
790
|
+
targeted slot; every other child call returns its argument unchanged,
|
|
791
|
+
passed through by reference) is both correct and guaranteed consistent
|
|
792
|
+
with ``_path_children``'s own indexing — same field walk, not a separate
|
|
793
|
+
reimplementation that could drift out of sync with it.
|
|
794
|
+
"""
|
|
795
|
+
if isinstance(node, Quantifier):
|
|
796
|
+
if index != 0:
|
|
797
|
+
raise IndexError(
|
|
798
|
+
f"replace_at: Quantifier has exactly one path child (its "
|
|
799
|
+
f"formula, index 0); got index {index}")
|
|
800
|
+
return Quantifier(node.type, node.variable, new_child)
|
|
801
|
+
|
|
802
|
+
seen = 0
|
|
803
|
+
|
|
804
|
+
def fn(child):
|
|
805
|
+
nonlocal seen
|
|
806
|
+
this_index = seen
|
|
807
|
+
seen += 1
|
|
808
|
+
return new_child if this_index == index else child
|
|
809
|
+
|
|
810
|
+
return node.map_children(fn)
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
def replace_at(root: "Node", path: Tuple[int, ...], new_node: "Node") -> "Node":
|
|
814
|
+
"""Return a new tree: ``root`` with the subtree addressed by ``path``
|
|
815
|
+
replaced by ``new_node``. ``root`` and every node reachable from it are
|
|
816
|
+
left untouched — nodes are frozen dataclasses, so in-place mutation is
|
|
817
|
+
not even possible; ``replace_at`` only ever builds new node instances
|
|
818
|
+
along the spine from the root down to the replaced subtree.
|
|
819
|
+
|
|
820
|
+
``path`` uses the PATH CONVENTION documented above ``_path_children``
|
|
821
|
+
(the same one :func:`node_at` and
|
|
822
|
+
:func:`unicode_logic_kit.fol.spans.traverse`/
|
|
823
|
+
:class:`~unicode_logic_kit.fol.spans.SpanMap` use) — in particular, a
|
|
824
|
+
:class:`Quantifier`'s bound variable is NOT reachable via any
|
|
825
|
+
``replace_at`` path; its only path child is its ``formula``, at index 0.
|
|
826
|
+
|
|
827
|
+
STABILITY GUARANTEE (B1). For any path ``q`` that does not run through
|
|
828
|
+
the replaced subtree — ``q`` is not ``path``, not a prefix of ``path``
|
|
829
|
+
(an ancestor), and not extended by ``path`` (a descendant) — the node
|
|
830
|
+
reachable via ``q`` in the result is the SAME object (``is``) as the
|
|
831
|
+
node reachable via ``q`` in the original tree, because every node off
|
|
832
|
+
the root-to-``path`` spine is passed through by reference at each
|
|
833
|
+
rebuilt ancestor (see :func:`_replace_child_at`). Only the spine itself
|
|
834
|
+
— ``root`` and every proper prefix of ``path`` — is rebuilt (new
|
|
835
|
+
objects, since each now contains the replacement somewhere below it);
|
|
836
|
+
everything else in the tree is untouched.
|
|
837
|
+
|
|
838
|
+
Raises ``IndexError`` if any prefix of ``path`` runs off the tree — an
|
|
839
|
+
index that is out of range for the node's path children at that depth
|
|
840
|
+
(this also covers handing a non-empty path to a leaf node, whose path
|
|
841
|
+
children are always empty).
|
|
842
|
+
"""
|
|
843
|
+
path = tuple(path)
|
|
844
|
+
|
|
845
|
+
def rec(node: "Node", depth: int) -> "Node":
|
|
846
|
+
if depth == len(path):
|
|
847
|
+
return new_node
|
|
848
|
+
idx = path[depth]
|
|
849
|
+
children = _path_children(node)
|
|
850
|
+
if not isinstance(idx, int) or not (0 <= idx < len(children)):
|
|
851
|
+
raise IndexError(
|
|
852
|
+
f"replace_at: path {path!r} is invalid at position {depth} "
|
|
853
|
+
f"(index {idx!r}) — a {type(node).__name__} node has "
|
|
854
|
+
f"{len(children)} path child(ren) there."
|
|
855
|
+
)
|
|
856
|
+
new_child = rec(children[idx], depth + 1)
|
|
857
|
+
return _replace_child_at(node, idx, new_child)
|
|
858
|
+
|
|
859
|
+
return rec(root, 0)
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
# Term node class names — used by Node.subformulas to exclude atomic terms.
|
|
863
|
+
# Measure and Cardinality are term-valued (they occur in argument position and
|
|
864
|
+
# are compared with </>); Cardinality additionally carries a formula child, which
|
|
865
|
+
# Node.walk still descends into, so its φ is still reported among subformulas.
|
|
866
|
+
_TERM_NAMES = frozenset({
|
|
867
|
+
"Variable", "Constant", "Number", "Function", "SortedConstant", "LambdaVar",
|
|
868
|
+
"Measure", "Cardinality",
|
|
869
|
+
})
|
|
870
|
+
|
|
871
|
+
|
|
872
|
+
# =========================
|
|
873
|
+
# Term Nodes
|
|
874
|
+
# =========================
|
|
875
|
+
|
|
876
|
+
@dataclass(frozen=True)
|
|
877
|
+
class Variable(Node):
|
|
878
|
+
"""A logical variable, represented by a single lowercase letter in the grammar."""
|
|
879
|
+
|
|
880
|
+
name: str
|
|
881
|
+
|
|
882
|
+
def to_dict(self):
|
|
883
|
+
"""Serialise to dict with type tag and variable name."""
|
|
884
|
+
return {"_type": "Variable", "name": self.name}
|
|
885
|
+
|
|
886
|
+
@staticmethod
|
|
887
|
+
def from_dict(d):
|
|
888
|
+
"""Deserialise a Variable from a dict produced by to_dict."""
|
|
889
|
+
return Variable(d["name"])
|
|
890
|
+
|
|
891
|
+
def to_z3(self, env: Z3Env = None):
|
|
892
|
+
"""Translate to a Z3 constant in the uninterpreted sort S.
|
|
893
|
+
|
|
894
|
+
A variable is a symbol of its own, apart from a constant of the same name (see
|
|
895
|
+
:class:`Z3Env`): the quantifier that binds ``x`` binds no constant named ``x``.
|
|
896
|
+
"""
|
|
897
|
+
return (env or Z3Env()).get_variable(self.name)
|
|
898
|
+
|
|
899
|
+
def to_prover9(self) -> str:
|
|
900
|
+
"""Render the variable name in uppercase.
|
|
901
|
+
|
|
902
|
+
The Prover9 driver enables ``set(prolog_style_variables)``, under which a
|
|
903
|
+
symbol that no quantifier binds is a variable only if it begins with an
|
|
904
|
+
uppercase letter (an underscore does not make one: measured on Prover9
|
|
905
|
+
2026-8A, ``_x`` is a constant with the flag and without it). Grammar variable
|
|
906
|
+
names are always lowercase, so they are uppercased here; constants and
|
|
907
|
+
predicate/function names stay as-is.
|
|
908
|
+
|
|
909
|
+
A name with a non-ASCII character is refused: Prover9 reads ASCII only,
|
|
910
|
+
and upper-casing can give the SAME text for two different variables
|
|
911
|
+
(``ı`` and ``i`` both print as ``I``, ``ſ`` and ``s`` as ``S``, the
|
|
912
|
+
ligatures ``ſt`` and ``st`` as ``ST``), which would silently merge them. A name that
|
|
913
|
+
is no word either (``x-1``, ``1x``, ``x'``, an empty name) is refused as well: written
|
|
914
|
+
bare, Prover9 reads an operator or another symbol in it, and the kit's own reader
|
|
915
|
+
could not read the text back.
|
|
916
|
+
"""
|
|
917
|
+
if not self.name.isascii():
|
|
918
|
+
raise NotImplementedError(
|
|
919
|
+
f"to_prover9: variable {self.name!r} has a non-ASCII character. "
|
|
920
|
+
f"Prover9 reads ASCII only, and upper-casing {self.name!r} gives "
|
|
921
|
+
f"{self.name.upper()!r}, which is either text Prover9 cannot read "
|
|
922
|
+
f"or the same text as another variable (ı and i both print as 'I'). "
|
|
923
|
+
f"Rename the variable to an ASCII letter followed by digits "
|
|
924
|
+
f"(x, y1) before exporting.")
|
|
925
|
+
if not _PROVER9_IDENTIFIER_RE.fullmatch(self.name):
|
|
926
|
+
raise NotImplementedError(
|
|
927
|
+
f"to_prover9: variable {self.name!r} is no word Prover9 reads as one symbol: "
|
|
928
|
+
f"it is written as {self.name.upper()!r}, and only letters, digits and "
|
|
929
|
+
f"underscores, not beginning with a digit, make a word (a '-', a '.', a quote "
|
|
930
|
+
f"or a leading digit is read as an operator or as another symbol). "
|
|
931
|
+
f"Rename the variable to an ASCII letter followed by digits "
|
|
932
|
+
f"(x, y1) before exporting.")
|
|
933
|
+
return self.name.upper()
|
|
934
|
+
|
|
935
|
+
def to_tptp(self) -> str:
|
|
936
|
+
"""Render variable in TPTP syntax. TPTP requires variables to be uppercase; single lowercase letters are capitalized.
|
|
937
|
+
|
|
938
|
+
The outermost :meth:`Node.to_tptp` call refuses a variable whose upper-case
|
|
939
|
+
is no TPTP variable (``ä``, ``x-1``, ``1x``); the problem writers rename it."""
|
|
940
|
+
return self.name.upper()
|
|
941
|
+
|
|
942
|
+
def _tptp_symbol(self):
|
|
943
|
+
"""The TPTP variable :meth:`to_tptp` writes: the upper-case of the name, so ``x`` and ``X`` are one."""
|
|
944
|
+
return (_variable_symbol, self.name)
|
|
945
|
+
|
|
946
|
+
|
|
947
|
+
# --------------------------------------------------------------------------- #
|
|
948
|
+
# Reversible ASCII transliteration for non-ASCII constant names.
|
|
949
|
+
#
|
|
950
|
+
# A constant may carry a non-ASCII (Greek) letter — e.g. a threshold ``θ`` — which
|
|
951
|
+
# the Kripke evaluator and Z3 handle directly (Z3 symbol names are arbitrary
|
|
952
|
+
# strings). The *text*-based first-order back-ends (Prover9 / TPTP) accept only
|
|
953
|
+
# ASCII identifiers, so on export each Greek letter (except the reserved operator
|
|
954
|
+
# glyphs λ / μ) maps to its conventional ASCII name and any other non-ASCII
|
|
955
|
+
# character uses a reversible ``uXXXX`` codepoint escape — a name is never emitted
|
|
956
|
+
# raw. Deterministic; invertible via :func:`constant_name_from_ascii` (exactly for a
|
|
957
|
+
# single-symbol constant, the realistic case).
|
|
958
|
+
# --------------------------------------------------------------------------- #
|
|
959
|
+
|
|
960
|
+
_GREEK_CONST_TO_ASCII = {
|
|
961
|
+
"α": "alpha", "β": "beta", "γ": "gamma", "δ": "delta", "ε": "epsilon",
|
|
962
|
+
"ζ": "zeta", "η": "eta", "θ": "theta", "ι": "iota", "κ": "kappa",
|
|
963
|
+
"ν": "nu", "ξ": "xi", "ο": "omicron", "π": "pi", "ρ": "rho",
|
|
964
|
+
"σ": "sigma", "τ": "tau", "υ": "upsilon", "φ": "phi", "χ": "chi",
|
|
965
|
+
"ψ": "psi", "ω": "omega",
|
|
966
|
+
}
|
|
967
|
+
_ASCII_TO_GREEK_CONST = {v: k for k, v in _GREEK_CONST_TO_ASCII.items()}
|
|
968
|
+
_UESC_RE = re.compile(r"u([0-9a-f]{4})")
|
|
969
|
+
|
|
970
|
+
|
|
971
|
+
def constant_name_to_ascii(name: str) -> str:
|
|
972
|
+
"""Transliterate a (possibly non-ASCII) constant name to a valid ASCII identifier.
|
|
973
|
+
|
|
974
|
+
ASCII characters pass through; a Greek letter maps to its conventional name
|
|
975
|
+
(``θ`` → ``theta``); any other non-ASCII character becomes a reversible ``uXXXX``
|
|
976
|
+
codepoint escape. Deterministic; inverse is :func:`constant_name_from_ascii`.
|
|
977
|
+
"""
|
|
978
|
+
out = []
|
|
979
|
+
for ch in name:
|
|
980
|
+
if ch.isascii():
|
|
981
|
+
out.append(ch)
|
|
982
|
+
elif ch in _GREEK_CONST_TO_ASCII:
|
|
983
|
+
out.append(_GREEK_CONST_TO_ASCII[ch])
|
|
984
|
+
else:
|
|
985
|
+
out.append("u%04x" % ord(ch))
|
|
986
|
+
return "".join(out)
|
|
987
|
+
|
|
988
|
+
|
|
989
|
+
def constant_name_from_ascii(s: str) -> str:
|
|
990
|
+
"""Inverse of :func:`constant_name_to_ascii` for a single transliterated symbol.
|
|
991
|
+
|
|
992
|
+
Recovers the original character when ``s`` is exactly one Greek name (``theta`` →
|
|
993
|
+
``θ``) or a ``uXXXX`` escape; otherwise returns ``s`` unchanged. Multi-symbol
|
|
994
|
+
concatenations are deterministic forward but not uniquely decodable, so only
|
|
995
|
+
single-symbol constants (the realistic case) are guaranteed to round-trip.
|
|
996
|
+
"""
|
|
997
|
+
if s in _ASCII_TO_GREEK_CONST:
|
|
998
|
+
return _ASCII_TO_GREEK_CONST[s]
|
|
999
|
+
m = _UESC_RE.fullmatch(s)
|
|
1000
|
+
if m:
|
|
1001
|
+
return chr(int(m.group(1), 16))
|
|
1002
|
+
return s
|
|
1003
|
+
|
|
1004
|
+
|
|
1005
|
+
def _prover9_reads_as_variable(token: str) -> bool:
|
|
1006
|
+
"""Whether Prover9, under ``set(prolog_style_variables)``, reads the symbol
|
|
1007
|
+
``token`` in TERM position as a VARIABLE rather than as a constant.
|
|
1008
|
+
|
|
1009
|
+
Prover9's own rule (LADR ``ladr/symbols.c``, ``variable_name``): with that
|
|
1010
|
+
flag set, a symbol is a variable iff its first character is ``A``..``Z``
|
|
1011
|
+
(the manual: "If this flag is set, variables in clauses start with (upper
|
|
1012
|
+
case) 'A' through 'Z'"). It applies to every ARITY-0 symbol in term
|
|
1013
|
+
position (``set_vars_recurse`` converts a ``CONSTANT`` term), so a constant
|
|
1014
|
+
is affected; a function or predicate WITH arguments is not (the symbol is
|
|
1015
|
+
not a constant term, only its arguments are examined). The LADR source reads
|
|
1016
|
+
only ``A``..``Z``: measured on Prover9 2026-8A, ``_x`` is a constant with the
|
|
1017
|
+
flag and without it, and this kit's own Prover9 reader
|
|
1018
|
+
(:mod:`~unicode_logic_kit.fol.prover9_input`) reads it as one too. A leading
|
|
1019
|
+
underscore is nevertheless treated like a capital here, because the Prolog
|
|
1020
|
+
convention reads it as a variable and a text that another reader may take
|
|
1021
|
+
for a variable must not carry the name bare; the cost is a rename or a pair of
|
|
1022
|
+
quotes that Prover9 did not need.
|
|
1023
|
+
"""
|
|
1024
|
+
first = token[:1]
|
|
1025
|
+
return first == "_" or ("A" <= first <= "Z")
|
|
1026
|
+
|
|
1027
|
+
|
|
1028
|
+
_PROVER9_IDENTIFIER_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
|
|
1029
|
+
|
|
1030
|
+
#: A name Prover9 reads as ONE symbol when it is written bare: ASCII letters,
|
|
1031
|
+
#: digits and underscores (a digit-leading word is one symbol too: ``2nd(X)`` is
|
|
1032
|
+
#: read, measured on Prover9 2026-8A).
|
|
1033
|
+
_PROVER9_WORD_RE = re.compile(r"[A-Za-z0-9_]+")
|
|
1034
|
+
|
|
1035
|
+
#: The two words LADR reads as quantifiers. As a predicate or a function applied to a
|
|
1036
|
+
#: variable at the start of an operand, ``exists(X) & ...`` is read as ``exists X``
|
|
1037
|
+
#: followed by a stray ``&`` (measured on Prover9 2026-8A: Prover9 echoes
|
|
1038
|
+
#: ``(exists W exists W &(Q(c) & R(c)))`` and ends with ``symbols used with multiple
|
|
1039
|
+
#: arities: &/1, &/2``). A double-quoted symbol is never a keyword, so these are written
|
|
1040
|
+
#: in double quotes at every arity; the problem writer renames them instead.
|
|
1041
|
+
_PROVER9_QUANTIFIERS = frozenset({"all", "exists"})
|
|
1042
|
+
|
|
1043
|
+
#: Symbols, by name AND arity, that Prover9 or this kit's own reader of Prover9 files reads as
|
|
1044
|
+
#: something other than an ordinary symbol (measured on Prover9 2026-8A):
|
|
1045
|
+
#:
|
|
1046
|
+
#: * ``if`` at three arguments: LADR reads the first argument as a FORMULA, so
|
|
1047
|
+
#: ``(all W if(W, a, a))`` is refused ("cannot be used as atomic formulas, because they are
|
|
1048
|
+
#: variables: W") and ``if(a, b, c)`` makes ``a`` a relation symbol, which a constant ``a``
|
|
1049
|
+
#: elsewhere in the file contradicts;
|
|
1050
|
+
#: * ``end_of_list`` with no argument: inside a ``formulas(...)`` list the bare word ends the
|
|
1051
|
+
#: list, so a proposition of that name is "Unrecognized command or list";
|
|
1052
|
+
#: * ``formulas`` at one argument: Prover9 reads ``formulas(alpha)`` in a list as an atom, but a
|
|
1053
|
+
#: file reader that takes it for the header of a nested list cannot read the text back.
|
|
1054
|
+
#:
|
|
1055
|
+
#: A double-quoted symbol is a symbol of its own that none of this applies to, so a single
|
|
1056
|
+
#: renderer writes these in double quotes; the problem writer renames them instead.
|
|
1057
|
+
_PROVER9_RESERVED_SYMBOLS = frozenset({("if", 3), ("end_of_list", 0), ("formulas", 1)})
|
|
1058
|
+
|
|
1059
|
+
|
|
1060
|
+
def _prover9_arity_zero_symbol(token: str) -> Optional[str]:
|
|
1061
|
+
"""The text that makes Prover9 read the arity-0 symbol ``token`` as itself: a
|
|
1062
|
+
constant in term position, a proposition in formula position.
|
|
1063
|
+
|
|
1064
|
+
A name that Prover9 would not read as a variable
|
|
1065
|
+
(:func:`_prover9_reads_as_variable`) is written as it is. One that it would
|
|
1066
|
+
is written in double quotes. LADR stores a double-quoted symbol WITH its
|
|
1067
|
+
quote characters, so the first character it tests for the variable rule is
|
|
1068
|
+
the quote, in every variable style; ``"Gaseous"`` is a constant and
|
|
1069
|
+
``"Rain"`` a proposition, each distinct from the bare word of the same
|
|
1070
|
+
letters (measured on Prover9 2026-8A, with and without
|
|
1071
|
+
``prolog_style_variables``). The quantifier words ``all`` and ``exists`` are
|
|
1072
|
+
written in double quotes too (:data:`_PROVER9_QUANTIFIERS`), and so is a word that is
|
|
1073
|
+
reserved at no argument (:data:`_PROVER9_RESERVED_SYMBOLS`). LADR has no escape
|
|
1074
|
+
inside quotes, so a name can be quoted only when it is a plain word (ASCII
|
|
1075
|
+
letters, digits and underscore, which excludes the quote itself). ``None`` says
|
|
1076
|
+
the name is no such word (it holds a space, a punctuation mark, a non-ASCII
|
|
1077
|
+
letter, or nothing at all): it can be written neither bare nor quoted, and the
|
|
1078
|
+
caller refuses it by name (:func:`_prover9_name_refusal`).
|
|
1079
|
+
"""
|
|
1080
|
+
if _PROVER9_WORD_RE.fullmatch(token) is None:
|
|
1081
|
+
return None
|
|
1082
|
+
if (token in _PROVER9_QUANTIFIERS or (token, 0) in _PROVER9_RESERVED_SYMBOLS
|
|
1083
|
+
or _prover9_reads_as_variable(token)):
|
|
1084
|
+
return '"' + token + '"'
|
|
1085
|
+
return token
|
|
1086
|
+
|
|
1087
|
+
|
|
1088
|
+
def _prover9_name_refusal(what: str, name: str, written: Optional[str] = None
|
|
1089
|
+
) -> NotImplementedError:
|
|
1090
|
+
"""The refusal for a symbol name that can be written neither bare nor in quotes.
|
|
1091
|
+
|
|
1092
|
+
``what`` says which symbol (``"the predicate"``, ``"the constant"``), ``name``
|
|
1093
|
+
is the kit name and ``written`` the text it was reduced to when that differs
|
|
1094
|
+
(a constant is transliterated first). A name that begins with ``$`` gets the
|
|
1095
|
+
reason that is its own: Prover9 keeps those words for itself.
|
|
1096
|
+
"""
|
|
1097
|
+
if name.startswith("$"):
|
|
1098
|
+
return NotImplementedError(
|
|
1099
|
+
f"to_prover9: {what} {name!r} is a '$'-word. Prover9 keeps the words that begin "
|
|
1100
|
+
f"with '$' for itself (its truth constants are $T and $F), so a symbol spelled "
|
|
1101
|
+
f"like that is not a name of the user's, and the text would say something else; "
|
|
1102
|
+
f"the kit writes only the nullary atoms $true and $false, as $T and $F. Rename "
|
|
1103
|
+
f"the symbol before exporting it.")
|
|
1104
|
+
shown = f" (written {written!r})" if written is not None and written != name else ""
|
|
1105
|
+
return NotImplementedError(
|
|
1106
|
+
f"to_prover9: {what} {name!r}{shown} cannot be written for Prover9. Prover9 reads a "
|
|
1107
|
+
f"bare word of ASCII letters, digits and underscores as one symbol and a "
|
|
1108
|
+
f"double-quoted word as another, and this name is neither: it holds a space, a "
|
|
1109
|
+
f"punctuation mark or a non-ASCII letter, or it is empty, so any text for it would "
|
|
1110
|
+
f"be read as several symbols or refused. Build the problem with "
|
|
1111
|
+
f"unicode_logic_kit.atp.prover9_entailment.generate_prover9_input_with_mapping, which "
|
|
1112
|
+
f"writes such a name under an ASCII replacement and returns the map, or rename it.")
|
|
1113
|
+
|
|
1114
|
+
|
|
1115
|
+
def _prover9_word(name: str, what: str, arity: Optional[int] = None) -> str:
|
|
1116
|
+
"""``name`` as the word of a predicate or function that has ``arity`` arguments, or a
|
|
1117
|
+
refusal by name. Such a symbol is never read as a variable, whatever its first
|
|
1118
|
+
letter, so only its shape matters; the quantifier words ``all`` and ``exists``, and a
|
|
1119
|
+
word reserved at this number of arguments (:data:`_PROVER9_RESERVED_SYMBOLS`), are the
|
|
1120
|
+
exception, which are written in double quotes (:data:`_PROVER9_QUANTIFIERS`)."""
|
|
1121
|
+
if _PROVER9_WORD_RE.fullmatch(name) is None:
|
|
1122
|
+
raise _prover9_name_refusal(what, name)
|
|
1123
|
+
if name in _PROVER9_QUANTIFIERS or (name, arity) in _PROVER9_RESERVED_SYMBOLS:
|
|
1124
|
+
return '"' + name + '"'
|
|
1125
|
+
return name
|
|
1126
|
+
|
|
1127
|
+
|
|
1128
|
+
#: True while the OUTERMOST ``to_prover9`` of a node writes the tree :func:`_prover9_prepared` made of
|
|
1129
|
+
#: it, and in every call nested below it: such a call writes its node as it is.
|
|
1130
|
+
_PROVER9_PREPARED: "contextvars.ContextVar[bool]" = contextvars.ContextVar(
|
|
1131
|
+
"unicode_logic_kit_prover9_prepared", default=False)
|
|
1132
|
+
|
|
1133
|
+
|
|
1134
|
+
def _prover9_scope_scan(node: "Node") -> Tuple[bool, frozenset]:
|
|
1135
|
+
"""Whether a binder of ``node`` is read by Prover9 as one that re-binds a name, and the upper-case
|
|
1136
|
+
names of the free variables of ``node``. Iterative: it costs no recursion depth.
|
|
1137
|
+
|
|
1138
|
+
A binder re-binds when it sits inside the scope of a binder of its own name, or when a variable of
|
|
1139
|
+
its name is free somewhere in the node (Prover9 closes a formula universally, so the closure is a
|
|
1140
|
+
binder that every other one sits in). Names are compared as Prover9 reads them, in upper case.
|
|
1141
|
+
"""
|
|
1142
|
+
free: set = set()
|
|
1143
|
+
binders: list = []
|
|
1144
|
+
rebound = False
|
|
1145
|
+
stack: list = [(node, frozenset())]
|
|
1146
|
+
while stack:
|
|
1147
|
+
current, bound = stack.pop()
|
|
1148
|
+
if isinstance(current, Variable):
|
|
1149
|
+
if current.name.upper() not in bound:
|
|
1150
|
+
free.add(current.name.upper())
|
|
1151
|
+
elif isinstance(current, Quantifier):
|
|
1152
|
+
name = current.variable.name.upper()
|
|
1153
|
+
rebound = rebound or name in bound
|
|
1154
|
+
binders.append(name)
|
|
1155
|
+
stack.append((current.formula, bound | {name}))
|
|
1156
|
+
else:
|
|
1157
|
+
stack.extend((child, bound) for child in current._child_nodes())
|
|
1158
|
+
return rebound or any(name in free for name in binders), frozenset(free)
|
|
1159
|
+
|
|
1160
|
+
|
|
1161
|
+
def _prover9_merged_variables(node: "Node") -> Optional[Tuple[str, str]]:
|
|
1162
|
+
"""Two variables of ``node`` that its text would read as ONE, or ``None``. Iterative.
|
|
1163
|
+
|
|
1164
|
+
Prover9 reads a variable by the upper-case of its name, so ``x`` and ``X`` are one variable in the
|
|
1165
|
+
text. That is harmless where the two are bound apart (``(all X P(X)) & (all X Q(X))``), and a binder
|
|
1166
|
+
inside the scope of another of the same upper-case name is renamed (:func:`_prover9_prepared`). What
|
|
1167
|
+
is left, and what no renaming of a binder can repair, is an occurrence that the binder of ITS name
|
|
1168
|
+
does not enclose but a binder of the same upper-case name does (``∀X P(x)`` with a free ``x``: the
|
|
1169
|
+
text binds it), and two free variables of one upper-case name (``P(x) ∧ Q(X)``: two parameters
|
|
1170
|
+
become one). Call it on a tree whose re-bound binders are already renamed.
|
|
1171
|
+
"""
|
|
1172
|
+
free: dict = {}
|
|
1173
|
+
stack: list = [(node, {})]
|
|
1174
|
+
while stack:
|
|
1175
|
+
current, binders = stack.pop()
|
|
1176
|
+
if isinstance(current, Variable):
|
|
1177
|
+
upper = current.name.upper()
|
|
1178
|
+
binder = binders.get(upper)
|
|
1179
|
+
if binder is None:
|
|
1180
|
+
first = free.setdefault(upper, current.name)
|
|
1181
|
+
if first != current.name:
|
|
1182
|
+
return first, current.name
|
|
1183
|
+
elif binder != current.name:
|
|
1184
|
+
return binder, current.name
|
|
1185
|
+
elif isinstance(current, Quantifier):
|
|
1186
|
+
stack.append((current.formula, {**binders, current.variable.name.upper(): current.variable.name}))
|
|
1187
|
+
else:
|
|
1188
|
+
stack.extend((child, binders) for child in current._child_nodes())
|
|
1189
|
+
return None
|
|
1190
|
+
|
|
1191
|
+
|
|
1192
|
+
def _prover9_refuse_merged_variables(node: "Node") -> None:
|
|
1193
|
+
"""Refuse ``node`` by name when two of its variables would be read as one (see
|
|
1194
|
+
:func:`_prover9_merged_variables`); the refusal is the one the problem writer makes."""
|
|
1195
|
+
pair = _prover9_merged_variables(node)
|
|
1196
|
+
if pair is not None:
|
|
1197
|
+
_check_variable_names(Atom("variables", tuple(Variable(name) for name in pair)),
|
|
1198
|
+
where="Node.to_prover9", subject="formula", dialect="prover9")
|
|
1199
|
+
|
|
1200
|
+
|
|
1201
|
+
def _prover9_prepared(node: "Node") -> "Node":
|
|
1202
|
+
"""The tree whose text means what ``node`` means: ``node`` with its binders made safe for Prover9.
|
|
1203
|
+
|
|
1204
|
+
Prover9 reads the names of a text, not the tree, and two things make it read another formula than
|
|
1205
|
+
the node whose text it is. LADR renames a variable that a quantifier binds inside the scope of a
|
|
1206
|
+
quantifier of the same name, to ``x0``, ``x1``, ... (the first that is no variable in scope), and
|
|
1207
|
+
takes a constant of that spelling for it: ``(all W (all W P(W, x0)))`` is clausified to
|
|
1208
|
+
``P(A, A)``. And a counting witness is a name minted for the text: Prover9 writes a variable in
|
|
1209
|
+
upper case, so a witness ``x0`` next to a variable ``X0`` is ONE variable.
|
|
1210
|
+
|
|
1211
|
+
This is the preparation the problem writer makes of every formula of a problem, with the same two
|
|
1212
|
+
functions (``_lower_for_prover9`` and ``_rename_rebound_binders`` of
|
|
1213
|
+
``unicode_logic_kit.atp.prover9_entailment``): sorted nodes are lowered, the witnesses of a counting
|
|
1214
|
+
quantifier are fresh against every name of the whole node, of every kind, compared case-folded, and
|
|
1215
|
+
a binder that re-binds a name (see :func:`_prover9_scope_scan`) is renamed to a fresh variable. A
|
|
1216
|
+
node that needs none of this is returned as it is, so a formula that holds no re-bound binder and
|
|
1217
|
+
no counting quantifier is written exactly as deep as it always was, and so is a node that holds a
|
|
1218
|
+
Lukasiewicz connective (the connective refuses itself when it is written).
|
|
1219
|
+
|
|
1220
|
+
Two variables that differ only in case and that no renaming of a binder can tell apart are refused by
|
|
1221
|
+
name (:func:`_prover9_merged_variables`), as the problem writer refuses them.
|
|
1222
|
+
|
|
1223
|
+
Raises:
|
|
1224
|
+
NotImplementedError: two variables of ``node`` would be written as one.
|
|
1225
|
+
"""
|
|
1226
|
+
from ..atp.prover9_entailment import _LUKASIEWICZ_NODES, _lower_for_prover9, _rename_rebound_binders
|
|
1227
|
+
nodes = list(node.walk())
|
|
1228
|
+
names = {n.name for n in nodes if isinstance(n, Variable)}
|
|
1229
|
+
merged = len({name.upper() for name in names}) < len(names)
|
|
1230
|
+
if not any(getattr(n, "variable", None) is not None for n in nodes):
|
|
1231
|
+
if merged:
|
|
1232
|
+
_prover9_refuse_merged_variables(node)
|
|
1233
|
+
return node
|
|
1234
|
+
if any(isinstance(n, _LUKASIEWICZ_NODES) for n in nodes):
|
|
1235
|
+
return node
|
|
1236
|
+
if any(isinstance(n, Variable) and not n.name.isascii() for n in nodes):
|
|
1237
|
+
return node # a variable that Prover9 cannot read is refused by name when it is written
|
|
1238
|
+
lowering = any(isinstance(n, Count) or type(n).__name__ in ("SortedQuantifier", "SortedCount")
|
|
1239
|
+
for n in nodes)
|
|
1240
|
+
if not lowering and not _prover9_scope_scan(node)[0]:
|
|
1241
|
+
if merged:
|
|
1242
|
+
_prover9_refuse_merged_variables(node)
|
|
1243
|
+
return node
|
|
1244
|
+
avoid = set(_identifiers.symbol_names(node, fold=str.casefold))
|
|
1245
|
+
lowered = node
|
|
1246
|
+
if lowering:
|
|
1247
|
+
try:
|
|
1248
|
+
lowered = _lower_for_prover9(node, avoid)
|
|
1249
|
+
except NotImplementedError:
|
|
1250
|
+
return node # the node that cannot be written says so itself, when it is written
|
|
1251
|
+
rebinds, free = _prover9_scope_scan(lowered)
|
|
1252
|
+
prepared = _rename_rebound_binders(lowered, avoid, free) if rebinds else lowered
|
|
1253
|
+
if merged:
|
|
1254
|
+
_prover9_refuse_merged_variables(prepared)
|
|
1255
|
+
return prepared
|
|
1256
|
+
|
|
1257
|
+
|
|
1258
|
+
def _prover9_write_outermost(node: "Node") -> str:
|
|
1259
|
+
"""Write the text of ``node`` from the tree :func:`_prover9_prepared` makes of it."""
|
|
1260
|
+
prepared = _prover9_prepared(node)
|
|
1261
|
+
token = _PROVER9_PREPARED.set(True)
|
|
1262
|
+
try:
|
|
1263
|
+
return prepared.to_prover9()
|
|
1264
|
+
finally:
|
|
1265
|
+
_PROVER9_PREPARED.reset(token)
|
|
1266
|
+
|
|
1267
|
+
|
|
1268
|
+
class _Prover9Entry:
|
|
1269
|
+
"""The descriptor :func:`_prover9_outermost` puts in place of a ``to_prover9`` method.
|
|
1270
|
+
|
|
1271
|
+
``Class.to_prover9`` is a function of the node. ``node.to_prover9`` is the method that writes the
|
|
1272
|
+
prepared tree (:func:`_prover9_prepared`) when no ``to_prover9`` is in progress in this context, and
|
|
1273
|
+
the ORIGINAL bound method when one is: a nested call costs no stack frame of its own, so a deep
|
|
1274
|
+
formula is written as deep as it was before. ``__wrapped__`` is the original function.
|
|
1275
|
+
"""
|
|
1276
|
+
|
|
1277
|
+
def __init__(self, function: Callable[..., str]) -> None:
|
|
1278
|
+
for attribute in ("__module__", "__name__", "__qualname__", "__doc__"):
|
|
1279
|
+
setattr(self, attribute, getattr(function, attribute))
|
|
1280
|
+
self.__wrapped__ = function
|
|
1281
|
+
self._function = function
|
|
1282
|
+
|
|
1283
|
+
@functools.wraps(function)
|
|
1284
|
+
def outermost(node: Any) -> str:
|
|
1285
|
+
return function(node) if _PROVER9_PREPARED.get() else _prover9_write_outermost(node)
|
|
1286
|
+
|
|
1287
|
+
self._outermost = outermost
|
|
1288
|
+
|
|
1289
|
+
def __get__(self, node: Any, owner: Any = None) -> Any:
|
|
1290
|
+
write = self._function if _PROVER9_PREPARED.get() else self._outermost
|
|
1291
|
+
return write if node is None else types.MethodType(write, node)
|
|
1292
|
+
|
|
1293
|
+
|
|
1294
|
+
_F = TypeVar("_F", bound=Callable[..., str])
|
|
1295
|
+
|
|
1296
|
+
|
|
1297
|
+
def _prover9_outermost(function: _F) -> _F:
|
|
1298
|
+
"""Mark the ``to_prover9`` of a node class whose text can hold a binder, or stand around one:
|
|
1299
|
+
the outermost call prepares the whole node once (:func:`_prover9_prepared`), the calls nested in
|
|
1300
|
+
it write their nodes as they are."""
|
|
1301
|
+
return cast(_F, _Prover9Entry(function))
|
|
1302
|
+
|
|
1303
|
+
|
|
1304
|
+
# --------------------------------------------------------------------------- #
|
|
1305
|
+
# TPTP name folding — the exact mirror of tptp_input.py's ``_cap()``.
|
|
1306
|
+
#
|
|
1307
|
+
# TPTP requires an unquoted identifier to start with a lower-case letter
|
|
1308
|
+
# (``lower_word: [a-z][A-Za-z0-9_]*``), while this kit's own Atom-predicate
|
|
1309
|
+
# convention requires an upper-case first letter (grammar token
|
|
1310
|
+
# ``PREDICATE: /[A-Z][a-zA-Z0-9]*/``). On import, ``tptp_input.py``'s
|
|
1311
|
+
# ``_cap()`` bridges that gap by capitalising ONLY the first character of a
|
|
1312
|
+
# parsed predicate name (``hasBond`` → ``HasBond``) and leaving every other
|
|
1313
|
+
# character untouched — never a whole-string case fold. Exporting therefore
|
|
1314
|
+
# has to invert exactly that: fold only the first character back to
|
|
1315
|
+
# lower-case, not `.lower()` the entire name. An earlier version of
|
|
1316
|
+
# Atom/Function/Constant.to_tptp did the latter, which is wrong two ways:
|
|
1317
|
+
#
|
|
1318
|
+
# 1. **Not the true inverse of `_cap()`.** ``_cap()`` only ever touches
|
|
1319
|
+
# position 0, so re-exporting a mixed-case name via whole-string
|
|
1320
|
+
# `.lower()` does not reproduce the original: a chemistry predicate like
|
|
1321
|
+
# ``BDouble`` would round-trip as ``bdouble`` → (re-imported, `_cap()`
|
|
1322
|
+
# applied) → ``Bdouble`` — a DIFFERENT symbol, silently.
|
|
1323
|
+
# 2. **Loses information `_cap()` never touched at all for Function/Constant
|
|
1324
|
+
# names.** Those are never case-folded on import (`_functor_name` only
|
|
1325
|
+
# strips quotes), so a mixed-case function/constant name such as
|
|
1326
|
+
# ``hasBond`` needs NO folding whatsoever — the first character is
|
|
1327
|
+
# already lower-case per this kit's own NAME-token convention — yet the
|
|
1328
|
+
# old whole-string `.lower()` mangled it to ``hasbond`` anyway.
|
|
1329
|
+
#
|
|
1330
|
+
# Folding only the first character does NOT by itself make the export
|
|
1331
|
+
# injective: ``Foo`` and ``foo`` still both fold to ``foo``. A collision is a
|
|
1332
|
+
# property of the whole SET of symbols a text contains, never of one node, so
|
|
1333
|
+
# it is caught where a set of symbols is known:
|
|
1334
|
+
#
|
|
1335
|
+
# * for ONE formula, by the OUTERMOST ``to_tptp()`` call — every node class's
|
|
1336
|
+
# ``to_tptp`` is guarded (``Node.__init_subclass__``, see
|
|
1337
|
+
# :mod:`unicode_logic_kit.fol._tptp_symbols`), the guard records each name the
|
|
1338
|
+
# render actually writes and, when the outermost call returns, refuses with
|
|
1339
|
+
# ``NotImplementedError`` if two DISTINCT names of one kind (predicates; or
|
|
1340
|
+
# functions and constants together) were written as one word. It reads what
|
|
1341
|
+
# is rendered, not what is in the source tree, so a name a reduction
|
|
1342
|
+
# introduces (the sort guard predicate of a ``SortedQuantifier``) is seen;
|
|
1343
|
+
# * for a PROBLEM made of several formulas, by the checked writers —
|
|
1344
|
+
# :func:`unicode_logic_kit.atp._tptp_problem.generate_tptp_problem` (the
|
|
1345
|
+
# external-prover backends), the TF0/TFA writers, and
|
|
1346
|
+
# :func:`unicode_logic_kit.atp.tptp_ncl.to_tptp_ncl` (the NXF modal export) —
|
|
1347
|
+
# which check every premise and the conclusion TOGETHER. A caller that
|
|
1348
|
+
# joins ``to_tptp()`` strings itself is outside every check: ``gaseous`` in
|
|
1349
|
+
# one premise and ``Gaseous`` in another reach the prover as one symbol, and
|
|
1350
|
+
# no per-formula check can see it.
|
|
1351
|
+
#
|
|
1352
|
+
# Both use the SAME check (``_tptp_symbols.check_symbols``), reading the same
|
|
1353
|
+
# per-node hook (``Node._tptp_symbol``), so they cannot disagree about what a
|
|
1354
|
+
# node writes.
|
|
1355
|
+
#
|
|
1356
|
+
# What is refused and what is renamed differ, deliberately, for this release.
|
|
1357
|
+
# Two LEGAL names of one kind that fold together (``Foo``/``foo``) are refused
|
|
1358
|
+
# by name, by both the guard and the writers: the refusal predates the writers'
|
|
1359
|
+
# name map, and stays so that no existing caller silently receives a symbol
|
|
1360
|
+
# renamed behind its back. A name TPTP cannot spell, and a predicate that
|
|
1361
|
+
# shares its word with a function/constant (the class ``Agent`` and the role
|
|
1362
|
+
# function ``agent``), are renamed by the WRITERS and recorded in the returned
|
|
1363
|
+
# ``TptpNameMap``. The guard does not refuse the cross-kind case: the text of
|
|
1364
|
+
# one formula is unambiguous by position and this kit's reader reads it back,
|
|
1365
|
+
# and only a writer has a map to hand back. A name TPTP cannot spell is another
|
|
1366
|
+
# matter for ONE formula: the guard cannot rename it and will not write it, so
|
|
1367
|
+
# it refuses it by name (an illegal word is not a rendering).
|
|
1368
|
+
# --------------------------------------------------------------------------- #
|
|
1369
|
+
|
|
1370
|
+
def tptp_fold_first_letter(name: str) -> str:
|
|
1371
|
+
"""Fold ``name``'s first character to lower-case for TPTP export; leave the rest untouched.
|
|
1372
|
+
|
|
1373
|
+
The exact mirror of :func:`tptp_input._cap`, which capitalises only the
|
|
1374
|
+
first character of a parsed predicate name on import — see the module
|
|
1375
|
+
comment above this function for why a whole-string ``.lower()`` is wrong.
|
|
1376
|
+
Used by :meth:`Atom.to_tptp`, :meth:`Function.to_tptp`, and
|
|
1377
|
+
:meth:`Constant.to_tptp` for their predicate/function/constant name.
|
|
1378
|
+
"""
|
|
1379
|
+
return (name[:1].lower() + name[1:]) if name else name
|
|
1380
|
+
|
|
1381
|
+
|
|
1382
|
+
# The resolvers behind ``Node._tptp_symbol``: kit name -> (namespace, word, kind),
|
|
1383
|
+
# exactly the word the matching ``to_tptp`` writes for it; ``None`` for a token
|
|
1384
|
+
# that is not an identifier. Predicates are one namespace; functions and
|
|
1385
|
+
# constants share the other.
|
|
1386
|
+
|
|
1387
|
+
def _predicate_symbol(name: str):
|
|
1388
|
+
# Equality and the comparisons are written with a token of their own
|
|
1389
|
+
# (``=``, ``!=``, ``$less`` ...). Such a token is not a name, but a user's
|
|
1390
|
+
# predicate named ``$less`` would be written as the same word.
|
|
1391
|
+
if name in Atom.INFIX_PREDS_TPTP:
|
|
1392
|
+
return ("predicate", Atom.INFIX_PREDS_TPTP[name], "reserved predicate")
|
|
1393
|
+
if name in Atom.PREFIX_PREDS_TPTP:
|
|
1394
|
+
return ("predicate", Atom.PREFIX_PREDS_TPTP[name], "reserved predicate")
|
|
1395
|
+
return ("predicate", tptp_fold_first_letter(name), "predicate")
|
|
1396
|
+
|
|
1397
|
+
|
|
1398
|
+
def _function_symbol(name: str):
|
|
1399
|
+
if name in Function.TPTP_ARITH_OPS:
|
|
1400
|
+
return ("term", Function.TPTP_ARITH_OPS[name], "reserved function")
|
|
1401
|
+
return ("term", tptp_fold_first_letter(name), "function")
|
|
1402
|
+
|
|
1403
|
+
|
|
1404
|
+
def _constant_symbol(name: str):
|
|
1405
|
+
return ("term", tptp_fold_first_letter(constant_name_to_ascii(name)), "constant/function")
|
|
1406
|
+
|
|
1407
|
+
|
|
1408
|
+
def _measure_symbol(name: str):
|
|
1409
|
+
return ("term", name, "function")
|
|
1410
|
+
|
|
1411
|
+
|
|
1412
|
+
def _variable_symbol(name: str):
|
|
1413
|
+
return ("variable", name.upper(), "variable")
|
|
1414
|
+
|
|
1415
|
+
|
|
1416
|
+
def _numeral_symbol(value):
|
|
1417
|
+
# The word is the text the number is written as, which is also what a number
|
|
1418
|
+
# is called in a refusal. A value has one spelling (``Number(1.0)`` is
|
|
1419
|
+
# ``Number(1)``), so the value alone keys the entry of the render log.
|
|
1420
|
+
text = _number_text(value)
|
|
1421
|
+
return ("term", text, "numeral", text)
|
|
1422
|
+
|
|
1423
|
+
|
|
1424
|
+
@dataclass(frozen=True)
|
|
1425
|
+
class Constant(Node):
|
|
1426
|
+
"""A ground constant, produced by a bare NAME, a ``c_``-prefixed CONSTANT, a
|
|
1427
|
+
non-ASCII (Greek, e.g. ``θ``) CONSTANT token, or a quoted name (``'k2'``).
|
|
1428
|
+
|
|
1429
|
+
The name may contain non-ASCII letters; the Kripke evaluator and Z3 use them
|
|
1430
|
+
directly, while the ASCII-only Prover9 / TPTP exporters transliterate them via
|
|
1431
|
+
:func:`constant_name_to_ascii` (``θ`` → ``theta``).
|
|
1432
|
+
|
|
1433
|
+
Every constant has a text of its own in the kit's syntax, whatever its name
|
|
1434
|
+
(the empty name and a name with a control character excepted: those are refused
|
|
1435
|
+
when printed). ``to_unicode_str`` writes the name bare when the bare word reads
|
|
1436
|
+
back as this constant (``socrates``, ``c_k2``, ``θ``) and in single quotes when
|
|
1437
|
+
it does not: ``'k2'`` (a bare ``k2`` is a variable), ``'Alice'``, ``'G-910'``,
|
|
1438
|
+
``'John Doe'``, ``'1'`` (a bare ``1`` is a number). Inside the quotes ``'`` is
|
|
1439
|
+
written ``\\'`` and ``\\`` is written ``\\\\``. So the text of a formula reads
|
|
1440
|
+
back as the formula, for a hand-built constant as for a parsed one. The text a
|
|
1441
|
+
route uses as a KEY (a valuation, a model table) writes the bare name instead,
|
|
1442
|
+
as it always did. In the text of a formula the constant ``'a'`` and the
|
|
1443
|
+
variable ``a`` are therefore told apart."""
|
|
1444
|
+
|
|
1445
|
+
name: str
|
|
1446
|
+
|
|
1447
|
+
def to_dict(self):
|
|
1448
|
+
"""Serialise to dict with type tag and constant name."""
|
|
1449
|
+
return {"_type": "Constant", "name": self.name}
|
|
1450
|
+
|
|
1451
|
+
@staticmethod
|
|
1452
|
+
def from_dict(d):
|
|
1453
|
+
"""Deserialise a Constant from a dict produced by to_dict."""
|
|
1454
|
+
return Constant(d["name"])
|
|
1455
|
+
|
|
1456
|
+
def to_z3(self, env: Z3Env = None):
|
|
1457
|
+
"""Translate to a Z3 constant in the uninterpreted sort S (Z3 accepts the raw name)."""
|
|
1458
|
+
return (env or Z3Env()).get_symbol(self.name)
|
|
1459
|
+
|
|
1460
|
+
def to_prover9(self) -> str:
|
|
1461
|
+
"""Render the constant name, transliterating any non-ASCII to ASCII (Prover9 is ASCII-only).
|
|
1462
|
+
|
|
1463
|
+
A name that begins with an upper-case letter or an underscore
|
|
1464
|
+
(:func:`_prover9_reads_as_variable`) is written in double quotes:
|
|
1465
|
+
``Gaseous`` is written ``"Gaseous"``. Every Prover9 file this kit writes
|
|
1466
|
+
sets ``prolog_style_variables``, under which the bare word in term
|
|
1467
|
+
position is a VARIABLE when it begins with an upper-case letter, so
|
|
1468
|
+
``P(Gaseous)`` would read as ``∀X P(X)`` and the text would denote a
|
|
1469
|
+
different formula (an underscore-initial name is a constant to Prover9
|
|
1470
|
+
itself, and is quoted all the same, because the Prolog convention reads
|
|
1471
|
+
it as a variable); a double-quoted symbol is
|
|
1472
|
+
never a variable and is a symbol of its own, distinct from the bare word
|
|
1473
|
+
of the same letters. The kit's own Prover9 reader reads it back as this
|
|
1474
|
+
constant.
|
|
1475
|
+
|
|
1476
|
+
Raises:
|
|
1477
|
+
NotImplementedError: the name would be read as a variable and cannot
|
|
1478
|
+
be quoted, because it holds a character other than a letter, a
|
|
1479
|
+
digit or an underscore (LADR has no escape for a double quote
|
|
1480
|
+
inside quotes). A single node cannot rename (a rename must be the
|
|
1481
|
+
same in every formula of the problem and stay injective), so the
|
|
1482
|
+
refusal points at the problem writer, which does. A name that is
|
|
1483
|
+
no word at all (a space, a dot, a ``$``-word, empty) is refused the
|
|
1484
|
+
same way, whatever its first letter: written bare it would be read
|
|
1485
|
+
as several symbols or as one of Prover9's own.
|
|
1486
|
+
"""
|
|
1487
|
+
text = constant_name_to_ascii(self.name)
|
|
1488
|
+
quoted = _prover9_arity_zero_symbol(text)
|
|
1489
|
+
if quoted is not None:
|
|
1490
|
+
return quoted
|
|
1491
|
+
if _prover9_reads_as_variable(text):
|
|
1492
|
+
raise NotImplementedError(
|
|
1493
|
+
f"to_prover9: constant {self.name!r} cannot be written for Prover9 on its "
|
|
1494
|
+
f"own: it would be read as a variable, and it cannot be put in double "
|
|
1495
|
+
f"quotes (which Prover9 never reads as a variable) because it holds a "
|
|
1496
|
+
f"character other than a letter, a digit or an underscore. Every "
|
|
1497
|
+
f"Prover9 file this kit writes sets prolog_style_variables, "
|
|
1498
|
+
f"under which a term-position symbol that begins with an upper-case "
|
|
1499
|
+
f"letter or an underscore is a VARIABLE: the text {text!r} would read "
|
|
1500
|
+
f"as a variable, and the formula around it would say something else "
|
|
1501
|
+
f"('P({text})' reads as 'for all X, P(X)'). Build the problem with "
|
|
1502
|
+
f"unicode_logic_kit.atp.prover9_entailment.generate_prover9_input_with_mapping "
|
|
1503
|
+
f"(check_logical_entailment and the Prover9 backend use it), which renames "
|
|
1504
|
+
f"such a constant to a lower-case token and returns the mapping, or name "
|
|
1505
|
+
f"the constant with a lower-case first letter.")
|
|
1506
|
+
raise _prover9_name_refusal("the constant", self.name, text)
|
|
1507
|
+
|
|
1508
|
+
def to_tptp(self) -> str:
|
|
1509
|
+
"""Render constant in TPTP syntax (ASCII, lowercase-initial): transliterate, then fold the first letter.
|
|
1510
|
+
|
|
1511
|
+
Only the first character is folded to lower-case (see
|
|
1512
|
+
:func:`tptp_fold_first_letter`) — everything from the second character
|
|
1513
|
+
on is emitted verbatim, so a mixed-case constant name (e.g. a
|
|
1514
|
+
chemistry identifier like ``hasBond``) survives export unmangled.
|
|
1515
|
+
"""
|
|
1516
|
+
return tptp_fold_first_letter(constant_name_to_ascii(self.name))
|
|
1517
|
+
|
|
1518
|
+
def _tptp_symbol(self):
|
|
1519
|
+
"""The constant word :meth:`to_tptp` writes (shares the term namespace with functions)."""
|
|
1520
|
+
return (_constant_symbol, self.name)
|
|
1521
|
+
|
|
1522
|
+
|
|
1523
|
+
def _number_text(value) -> str:
|
|
1524
|
+
"""The text a :class:`Number` prints as in every textual syntax of the kit
|
|
1525
|
+
(unicode, LaTeX, TPTP, Prover9), and that the kit's own readers read back as
|
|
1526
|
+
the SAME value.
|
|
1527
|
+
|
|
1528
|
+
The NUMBER terminal of every reader is ``-?[0-9]+(\\.[0-9]+)?``: digits, an
|
|
1529
|
+
optional fractional part, no exponent. Python's ``str(1e-07)`` is ``'1e-07'``,
|
|
1530
|
+
which the unicode reader reads as the subtraction ``1e - 07`` and
|
|
1531
|
+
``str(1.5e-05)`` is not text it can read at all. So a float whose ``repr`` is
|
|
1532
|
+
in exponent form is written in plain positional notation instead, from the
|
|
1533
|
+
digits of that same ``repr`` (the shortest string that round-trips) with
|
|
1534
|
+
decimal arithmetic, never a rounding format: ``float(text) == value`` exactly.
|
|
1535
|
+
A float always keeps a ``.`` so it reads back as a float, not an int
|
|
1536
|
+
(``1e16`` is ``10000000000000000.0``). An int, and every float whose ``repr``
|
|
1537
|
+
is already positional (``2.5``, ``12345.678``, ``0.0001``), prints exactly as
|
|
1538
|
+
before. A :class:`Number` never holds a float with a whole value (it stores the
|
|
1539
|
+
integer it equals, so ``Number(1e16)`` prints ``10000000000000000``); the point
|
|
1540
|
+
of such a raw float is kept only for a caller that hands a bare float to this
|
|
1541
|
+
function.
|
|
1542
|
+
|
|
1543
|
+
Raises:
|
|
1544
|
+
ValueError: ``value`` is ``inf``, ``-inf`` or ``nan``. No syntax of the
|
|
1545
|
+
kit has a literal for it, and the word ``inf`` would read back as a
|
|
1546
|
+
CONSTANT of that name, a different formula.
|
|
1547
|
+
"""
|
|
1548
|
+
if isinstance(value, float):
|
|
1549
|
+
if value != value or value in (float("inf"), float("-inf")):
|
|
1550
|
+
raise ValueError(
|
|
1551
|
+
f"Number({value!r}) has no literal: a non-finite float cannot be "
|
|
1552
|
+
f"written in the unicode, LaTeX, TPTP or Prover9 syntax, and the "
|
|
1553
|
+
f"word {str(value)!r} would read back as a constant of that name, "
|
|
1554
|
+
f"not as a number. Use a finite value (or a constant such as "
|
|
1555
|
+
f"'infinity' for a symbolic bound).")
|
|
1556
|
+
text = repr(float(value))
|
|
1557
|
+
if "e" in text or "E" in text:
|
|
1558
|
+
text = format(Decimal(text), "f")
|
|
1559
|
+
if "." not in text:
|
|
1560
|
+
text += ".0"
|
|
1561
|
+
return text
|
|
1562
|
+
return str(value)
|
|
1563
|
+
|
|
1564
|
+
|
|
1565
|
+
#: The most significant digits a decimal text may have and still be read as the float it spells.
|
|
1566
|
+
_DECIMAL_DIGITS_READ_EXACTLY = 15
|
|
1567
|
+
|
|
1568
|
+
#: The smallest positive normal double (``sys.float_info.min``): below it the doubles carry fewer
|
|
1569
|
+
#: than 53 significant bits.
|
|
1570
|
+
_SMALLEST_NORMAL_DOUBLE = 2.2250738585072014e-308
|
|
1571
|
+
|
|
1572
|
+
|
|
1573
|
+
def _numeral_from_text(text: str) -> Union[int, float]:
|
|
1574
|
+
"""The value of a decimal numeral as it is written, ``-?[0-9]+(\\.[0-9]+)?``: read exactly or refused.
|
|
1575
|
+
|
|
1576
|
+
This is the ONE reading every text reader of the kit gives a NUMBER token, the inverse of
|
|
1577
|
+
:func:`_number_text`. A text without a point is the ``int`` of its digits, and so is a text
|
|
1578
|
+
with a point whose fractional digits are all zero, whatever its size
|
|
1579
|
+
(``100000000000000000000000.0`` is 10**23, where ``float`` would give the nearest double,
|
|
1580
|
+
``99999999999999991611392``). Any other decimal is the ``float`` it spells when it has at most
|
|
1581
|
+
15 significant digits, counted after the sign, the leading zeros and the trailing zeros of the
|
|
1582
|
+
fraction are dropped (``0.1`` and ``0.10`` are one numeral, ``3.14159265358979`` is read), and
|
|
1583
|
+
is refused when it has more (``3.141592653589793``).
|
|
1584
|
+
|
|
1585
|
+
Fifteen is the bound because two different decimals of at most 15 significant digits differ by
|
|
1586
|
+
at least 1e-15 of the larger one, while two numbers that are one double differ by at most
|
|
1587
|
+
2**-52 (about 2.2e-16) of it, so no two such decimals are one float. With 16 digits the gap can
|
|
1588
|
+
be 1e-16 of the number, below the spacing of the doubles, and two decimals can be one float
|
|
1589
|
+
(``8.000000000000001`` and ``8.000000000000002``, ``0.30000000000000004`` and
|
|
1590
|
+
``0.30000000000000005``).
|
|
1591
|
+
|
|
1592
|
+
Raises:
|
|
1593
|
+
ValueError: the decimal has more than 15 significant digits (two different decimals of that
|
|
1594
|
+
length can be one float, and a numeral is identified by its value, so reading it as a
|
|
1595
|
+
float could make two numerals one), or is nearer to zero than the smallest normal
|
|
1596
|
+
double (2.2250738585072014e-308), where a double holds fewer than 15 digits and a
|
|
1597
|
+
different decimal can be the same double, or zero. The message names the numeral and
|
|
1598
|
+
says why; a numeral is never read as another one.
|
|
1599
|
+
"""
|
|
1600
|
+
whole, point, fraction = text.partition(".")
|
|
1601
|
+
if not point:
|
|
1602
|
+
return int(text)
|
|
1603
|
+
if not fraction.strip("0"):
|
|
1604
|
+
return int(whole)
|
|
1605
|
+
digits = len((whole.lstrip("+-") + fraction.rstrip("0")).lstrip("0"))
|
|
1606
|
+
if digits > _DECIMAL_DIGITS_READ_EXACTLY:
|
|
1607
|
+
raise ValueError(
|
|
1608
|
+
f"the numeral {text} has {digits} significant digits, more than the "
|
|
1609
|
+
f"{_DECIMAL_DIGITS_READ_EXACTLY} that a floating-point number tells apart: two different "
|
|
1610
|
+
f"decimals of that length can be one float (0.30000000000000004 and 0.30000000000000005 "
|
|
1611
|
+
f"are), and a numeral is identified by its value, so reading it as a float could make "
|
|
1612
|
+
f"two numerals one. Write it with at most {_DECIMAL_DIGITS_READ_EXACTLY} significant "
|
|
1613
|
+
f"digits, or as an integer")
|
|
1614
|
+
value = float(text)
|
|
1615
|
+
if abs(value) < _SMALLEST_NORMAL_DOUBLE:
|
|
1616
|
+
raise ValueError(
|
|
1617
|
+
f"the numeral {text} is so close to zero that a floating-point number cannot hold "
|
|
1618
|
+
f"{_DECIMAL_DIGITS_READ_EXACTLY} digits of it (the nearest float is {value!r}), and "
|
|
1619
|
+
f"reading it as that number could make two different numerals one. Write it as 0, or "
|
|
1620
|
+
f"with a larger magnitude")
|
|
1621
|
+
return value
|
|
1622
|
+
|
|
1623
|
+
|
|
1624
|
+
class NumeralTextError(ParsingError):
|
|
1625
|
+
"""A numeral the unicode reader cannot read as the number it was written as.
|
|
1626
|
+
|
|
1627
|
+
A :class:`~unicode_logic_kit.fol.naming.ParsingError`, so the CLI, ``api.parse_any`` and every
|
|
1628
|
+
caller that catches the parser's error type report it as the one-line SYNTAX_ERROR it is. It
|
|
1629
|
+
is constructed directly from the message of :func:`_numeral_from_text`, not from a Lark
|
|
1630
|
+
exception, so it sets its own message.
|
|
1631
|
+
"""
|
|
1632
|
+
|
|
1633
|
+
def __init__(self, message: str):
|
|
1634
|
+
self.args = (f"SYNTAX_ERROR: {message}",)
|
|
1635
|
+
|
|
1636
|
+
def __str__(self):
|
|
1637
|
+
return self.args[0]
|
|
1638
|
+
|
|
1639
|
+
|
|
1640
|
+
@dataclass(frozen=True)
|
|
1641
|
+
class Number(Node):
|
|
1642
|
+
"""A numeral, produced by the NUMBER terminal of the grammar: a constant identified by its VALUE.
|
|
1643
|
+
|
|
1644
|
+
On every route that was not asked for arithmetic by name a numeral is an ordinary constant
|
|
1645
|
+
and nothing else is known about it: two numerals of different value may denote the same
|
|
1646
|
+
element (``1 ≠ 2`` is not valid), ``+ - * /`` are uninterpreted function symbols and
|
|
1647
|
+
``< > ≤ ≥`` uninterpreted predicates. The arithmetic reading is asked for by name (the
|
|
1648
|
+
``*_arith`` functions and ``sort="int"`` / ``sort="real"``); there ``Number(3)`` is the
|
|
1649
|
+
integer 3, or the real 3.0 under ``sort="real"``.
|
|
1650
|
+
|
|
1651
|
+
There is ONE constant per value and ONE spelling per value. A float whose value is a whole
|
|
1652
|
+
number is stored as the ``int`` it equals: ``Number(1.0)`` IS ``Number(1)``, with the same
|
|
1653
|
+
``value``, the same ``repr``, the same ``to_dict`` and the same printed text (``1``) in every
|
|
1654
|
+
syntax, and ``Number(-0.0)`` is ``Number(0)``. A value that is no whole number keeps its type
|
|
1655
|
+
(``Number(2.5)`` is a float). A float too large to have a fractional part is the integer it
|
|
1656
|
+
exactly is (the double nearest ``1e23`` is ``99999999999999991611392``).
|
|
1657
|
+
|
|
1658
|
+
The readers read a decimal text exactly or refuse it. One whose fractional digits are all zero
|
|
1659
|
+
is the integer it spells (``100000000000000000000000.0`` is ``10**23``, not that double), any
|
|
1660
|
+
other is the float it spells when it has at most 15 significant digits (``0.1`` and ``0.10``
|
|
1661
|
+
are one numeral), and one with more is refused by name, because two different decimals of 16
|
|
1662
|
+
digits or more can be one float (``0.30000000000000004`` and ``0.30000000000000005`` are) and
|
|
1663
|
+
a numeral is identified by its value.
|
|
1664
|
+
|
|
1665
|
+
Numerals of equal value are equal nodes and print alike, so a route that keys an atom by its
|
|
1666
|
+
printed text reads ``P(1)`` and ``P(1.0)`` as the one atom they are. A ``bool`` is no number:
|
|
1667
|
+
it is kept as it is, not read as ``1`` or ``0``, and the routes that need an integer refuse
|
|
1668
|
+
it by name.
|
|
1669
|
+
|
|
1670
|
+
Fields:
|
|
1671
|
+
|
|
1672
|
+
* ``value`` -- the number: an ``int``, or a ``float`` that is not a whole number (a
|
|
1673
|
+
non-finite float is stored as it is; no syntax of the kit has a literal for it).
|
|
1674
|
+
"""
|
|
1675
|
+
|
|
1676
|
+
value: Union[int, float]
|
|
1677
|
+
|
|
1678
|
+
def __post_init__(self):
|
|
1679
|
+
"""Store a float with a whole value as the ``int`` it equals: one numeral, one spelling."""
|
|
1680
|
+
value = self.value
|
|
1681
|
+
if isinstance(value, float) and value.is_integer():
|
|
1682
|
+
object.__setattr__(self, "value", int(value))
|
|
1683
|
+
|
|
1684
|
+
def to_dict(self):
|
|
1685
|
+
"""Serialise to dict with type tag and numeric value (an integral value is an ``int``)."""
|
|
1686
|
+
return {"_type": "Number", "value": self.value}
|
|
1687
|
+
|
|
1688
|
+
@staticmethod
|
|
1689
|
+
def from_dict(d):
|
|
1690
|
+
"""Deserialise a Number from a dict produced by to_dict."""
|
|
1691
|
+
return Number(d["value"])
|
|
1692
|
+
|
|
1693
|
+
def to_z3(self, env: Z3Env = None):
|
|
1694
|
+
"""Encode the number as a named constant in the uninterpreted sort S.
|
|
1695
|
+
|
|
1696
|
+
The symbol is named by the VALUE of the number (:func:`numeral_key`), so
|
|
1697
|
+
``Number(1)`` and ``Number(1.0)`` are one constant, and a constant of that very
|
|
1698
|
+
name would be the same symbol; the environment refuses the pair (see
|
|
1699
|
+
:class:`Z3Env`) with a ``NotImplementedError``. Nothing else is known about a
|
|
1700
|
+
numeral: ``1`` and ``2`` may denote the same element.
|
|
1701
|
+
"""
|
|
1702
|
+
return (env or Z3Env()).get_symbol(numeral_key(self.value), numeral=True)
|
|
1703
|
+
|
|
1704
|
+
def to_prover9(self) -> str:
|
|
1705
|
+
"""Render the numeral as ONE Prover9 constant: its value in double quotes.
|
|
1706
|
+
|
|
1707
|
+
A numeral is a constant identified by its VALUE, so ``Number(1)`` and
|
|
1708
|
+
``Number(1.0)`` -- equal nodes -- are one symbol, ``"1"``; ``2.5`` is
|
|
1709
|
+
``"2.5"`` and ``-1`` is ``"-1"``, the positional text of the number (see
|
|
1710
|
+
:func:`~unicode_logic_kit.fol._numeral_symbols.numeral_name`: an integral
|
|
1711
|
+
float is spelled as the integer, nothing is written in exponent form).
|
|
1712
|
+
The kit's Prover9 reader reads that quoted text back as the
|
|
1713
|
+
:class:`Number`.
|
|
1714
|
+
|
|
1715
|
+
Every numeral is quoted, also a non-negative integer, for three reasons
|
|
1716
|
+
measured on Prover9 and Mace4 2026-8A. Bare, ``2.5`` ends the statement
|
|
1717
|
+
(Prover9 refuses the file) and ``-1`` is the function ``-`` applied to the
|
|
1718
|
+
constant ``1``. And Mace4 reads a bare integer as a domain element of its own,
|
|
1719
|
+
all of them pairwise distinct (``1 != 2`` has no countermodel at any size it
|
|
1720
|
+
searched, and the smallest model of ``P(1) & P(2)`` has three elements), whereas
|
|
1721
|
+
the kit's numerals are ordinary constants that may denote one thing (``⊢ 1 ≠ 2``
|
|
1722
|
+
is not valid); a quoted symbol is a plain constant to both tools. Prover9 has no arithmetic: ``+ - * /`` and
|
|
1723
|
+
``< > ≤ ≥`` are uninterpreted symbols, as they are for the Z3 route.
|
|
1724
|
+
"""
|
|
1725
|
+
from ._numeral_symbols import numeral_name
|
|
1726
|
+
return '"' + numeral_name(self.value) + '"'
|
|
1727
|
+
|
|
1728
|
+
def to_tptp(self) -> str:
|
|
1729
|
+
"""Render number in TPTP syntax as an integer or rational literal, a float in positional notation (see :func:`_number_text`).
|
|
1730
|
+
|
|
1731
|
+
This is the ARITHMETIC spelling: a bare TPTP number is a literal of the prover's own
|
|
1732
|
+
arithmetic (Vampire and E type ``1`` as ``$int``, so ``p(1)`` is a type error for a
|
|
1733
|
+
predicate over individuals, and ``1 != 2`` is a theorem). On every route that was not
|
|
1734
|
+
asked for arithmetic the kit reads a numeral as a CONSTANT identified by its value, and a
|
|
1735
|
+
PROBLEM for a prover is written by the checked writers
|
|
1736
|
+
(:func:`~unicode_logic_kit.atp._tptp_problem.generate_tptp_problem_with_mapping`,
|
|
1737
|
+
:func:`~unicode_logic_kit.atp.tptp_tff.generate_tff_problem_with_mapping`), which write the
|
|
1738
|
+
numeral as an ordinary constant of a word of their own and record it in the name map.
|
|
1739
|
+
Use this method for the text of ONE formula, never to assemble a problem.
|
|
1740
|
+
"""
|
|
1741
|
+
return _number_text(self.value)
|
|
1742
|
+
|
|
1743
|
+
def _tptp_symbol(self):
|
|
1744
|
+
"""The numeral :meth:`to_tptp` writes, which is also the word of a constant spelled like it."""
|
|
1745
|
+
return (_numeral_symbol, self.value)
|
|
1746
|
+
|
|
1747
|
+
|
|
1748
|
+
@dataclass(frozen=True)
|
|
1749
|
+
class Function(Node):
|
|
1750
|
+
"""A function application node, covering both named functions and arithmetic operators."""
|
|
1751
|
+
|
|
1752
|
+
name: str
|
|
1753
|
+
args: Tuple[Node, ...]
|
|
1754
|
+
|
|
1755
|
+
def __post_init__(self):
|
|
1756
|
+
"""Coerce args to a tuple so this frozen node is hashable."""
|
|
1757
|
+
if not isinstance(self.args, tuple):
|
|
1758
|
+
object.__setattr__(self, "args", tuple(self.args))
|
|
1759
|
+
|
|
1760
|
+
INFIX_OPS = {"+", "-", "*", "/"}
|
|
1761
|
+
|
|
1762
|
+
def to_dict(self):
|
|
1763
|
+
"""Serialise to dict with type tag, function name, and recursively serialised arguments."""
|
|
1764
|
+
return {
|
|
1765
|
+
"_type": "Function",
|
|
1766
|
+
"name": self.name,
|
|
1767
|
+
"args": [a.to_dict() for a in self.args]
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
@staticmethod
|
|
1771
|
+
def from_dict(d):
|
|
1772
|
+
"""Deserialise a Function from a dict produced by to_dict."""
|
|
1773
|
+
return Function(d["name"], [Node.from_dict(a) for a in d["args"]])
|
|
1774
|
+
|
|
1775
|
+
def to_z3(self, env: Z3Env = None):
|
|
1776
|
+
"""Translate to an uninterpreted Z3 function application in sort S."""
|
|
1777
|
+
env = env or Z3Env()
|
|
1778
|
+
z3_args = [a.to_z3(env) for a in self.args]
|
|
1779
|
+
func = env.get_func(self.name, len(self.args))
|
|
1780
|
+
return func(*z3_args)
|
|
1781
|
+
|
|
1782
|
+
def to_prover9(self) -> str:
|
|
1783
|
+
"""Render in Prover9 syntax, using infix notation for ``+``, ``*`` and ``/``.
|
|
1784
|
+
|
|
1785
|
+
Prover9 has no infix minus: ``(a - b)`` is a syntax error there (measured on
|
|
1786
|
+
2026-8A), so a binary ``-`` is written in functional notation, ``-(a, b)``,
|
|
1787
|
+
the same symbol that ``-(a)`` is at one argument. None of these symbols is
|
|
1788
|
+
interpreted by Prover9, as none is by the Z3 route: they are uninterpreted
|
|
1789
|
+
functions. A function with no arguments is a constant (see
|
|
1790
|
+
:meth:`Constant.to_prover9`).
|
|
1791
|
+
|
|
1792
|
+
Raises:
|
|
1793
|
+
NotImplementedError: the name is no word Prover9 reads as one symbol (a
|
|
1794
|
+
space, a dot, a non-ASCII letter, a ``$``-word, an arithmetic
|
|
1795
|
+
symbol at a number of arguments it has no notation for); the problem
|
|
1796
|
+
writer renames such a name.
|
|
1797
|
+
"""
|
|
1798
|
+
if self.name in self.INFIX_OPS and len(self.args) == 2:
|
|
1799
|
+
left = self.args[0].to_prover9()
|
|
1800
|
+
right = self.args[1].to_prover9()
|
|
1801
|
+
if self.name == "-":
|
|
1802
|
+
return f"-({left}, {right})"
|
|
1803
|
+
return f"({left} {self.name} {right})"
|
|
1804
|
+
if self.name == "-" and len(self.args) == 1:
|
|
1805
|
+
return f"-({self.args[0].to_prover9()})"
|
|
1806
|
+
if not self.args:
|
|
1807
|
+
return Constant(self.name).to_prover9()
|
|
1808
|
+
|
|
1809
|
+
name = _prover9_word(self.name, "the function", len(self.args))
|
|
1810
|
+
args_str = ", ".join(a.to_prover9() for a in self.args)
|
|
1811
|
+
return f"{name}({args_str})"
|
|
1812
|
+
|
|
1813
|
+
TPTP_ARITH_OPS = {
|
|
1814
|
+
"+": "$sum",
|
|
1815
|
+
"-": "$difference",
|
|
1816
|
+
"*": "$product",
|
|
1817
|
+
"/": "$quotient",
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
def to_tptp(self) -> str:
|
|
1821
|
+
"""Render function application in TPTP syntax.
|
|
1822
|
+
|
|
1823
|
+
Arithmetic operators (``+``, ``-``, ``*``, ``/``) are mapped to their
|
|
1824
|
+
TPTP dollar-word equivalents (``$sum``, ``$difference``, ``$product``,
|
|
1825
|
+
``$quotient``) and emitted in
|
|
1826
|
+
prefix notation. All other functions are emitted as identifiers with
|
|
1827
|
+
a parenthesised argument list, with only the first character folded
|
|
1828
|
+
to lower-case (see :func:`tptp_fold_first_letter`) — a mixed-case
|
|
1829
|
+
function name is otherwise preserved verbatim.
|
|
1830
|
+
|
|
1831
|
+
The dollar-words are the ARITHMETIC spelling: a prover reads ``$sum(1,1) = 2`` as a
|
|
1832
|
+
theorem of its own arithmetic. On every route that was not asked for arithmetic the kit
|
|
1833
|
+
reads ``+ - * /`` as uninterpreted function symbols, and a PROBLEM for a prover is
|
|
1834
|
+
written by the checked writers
|
|
1835
|
+
(:func:`~unicode_logic_kit.atp._tptp_problem.generate_tptp_problem_with_mapping`,
|
|
1836
|
+
:func:`~unicode_logic_kit.atp.tptp_tff.generate_tff_problem_with_mapping`), which write
|
|
1837
|
+
the operator as an ordinary function of a word of their own and record it in the name
|
|
1838
|
+
map; the typed arithmetic writer
|
|
1839
|
+
(:func:`~unicode_logic_kit.atp._tff_problem.generate_tff_arith_problem`) keeps the
|
|
1840
|
+
dollar-words, typed ``$int`` or ``$real``. Use this method for the text of ONE formula,
|
|
1841
|
+
never to assemble a problem.
|
|
1842
|
+
|
|
1843
|
+
A function with no arguments is the constant of its name, and is written as that
|
|
1844
|
+
constant (:meth:`Constant.to_tptp`): TPTP has no empty argument list, ``f()`` is no
|
|
1845
|
+
term, and a prover stops at it with a parse error. The refusals of the constant apply to
|
|
1846
|
+
it, so an arithmetic symbol with no argument (``+``) is refused by name rather than
|
|
1847
|
+
written as ``$sum()``.
|
|
1848
|
+
"""
|
|
1849
|
+
if not self.args:
|
|
1850
|
+
return Constant(self.name).to_tptp()
|
|
1851
|
+
args_str = ",".join(a.to_tptp() for a in self.args)
|
|
1852
|
+
tptp_name = self.TPTP_ARITH_OPS.get(self.name, tptp_fold_first_letter(self.name))
|
|
1853
|
+
return f"{tptp_name}({args_str})"
|
|
1854
|
+
|
|
1855
|
+
def _tptp_symbol(self):
|
|
1856
|
+
"""The word :meth:`to_tptp` writes: the function word, or for a function with no arguments the word of the constant of its name (an arithmetic operator is a fixed ``$``-word)."""
|
|
1857
|
+
if not self.args:
|
|
1858
|
+
return (_constant_symbol, self.name)
|
|
1859
|
+
return (_function_symbol, self.name)
|
|
1860
|
+
|
|
1861
|
+
|
|
1862
|
+
# =========================
|
|
1863
|
+
# Formula Nodes
|
|
1864
|
+
# =========================
|
|
1865
|
+
|
|
1866
|
+
@dataclass(frozen=True)
|
|
1867
|
+
class Atom(Node):
|
|
1868
|
+
"""An atomic formula: either a named predicate application or an infix comparison."""
|
|
1869
|
+
|
|
1870
|
+
predicate: str
|
|
1871
|
+
args: Tuple[Node, ...]
|
|
1872
|
+
|
|
1873
|
+
def __post_init__(self):
|
|
1874
|
+
"""Coerce args to a tuple so this frozen node is hashable."""
|
|
1875
|
+
if not isinstance(self.args, tuple):
|
|
1876
|
+
object.__setattr__(self, "args", tuple(self.args))
|
|
1877
|
+
|
|
1878
|
+
INFIX_PREDS_P9 = {
|
|
1879
|
+
"=": "=", "<": "<", ">": ">",
|
|
1880
|
+
"≤": "<=", "≥": ">=", "≠": "!=",
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
def to_dict(self):
|
|
1884
|
+
"""Serialise to dict with type tag, predicate name, and recursively serialised arguments."""
|
|
1885
|
+
return {
|
|
1886
|
+
"_type": "Atom",
|
|
1887
|
+
"predicate": self.predicate,
|
|
1888
|
+
"args": [a.to_dict() for a in self.args]
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
@staticmethod
|
|
1892
|
+
def from_dict(d):
|
|
1893
|
+
"""Deserialise an Atom from a dict produced by to_dict."""
|
|
1894
|
+
return Atom(d["predicate"], [Node.from_dict(a) for a in d["args"]])
|
|
1895
|
+
|
|
1896
|
+
def to_z3(self, env: Z3Env = None):
|
|
1897
|
+
"""Translate to a Z3 boolean expression.
|
|
1898
|
+
|
|
1899
|
+
Equality and disequality map to native Z3 operators; all other
|
|
1900
|
+
predicates become uninterpreted Z3 functions returning Bool. The nullary
|
|
1901
|
+
atoms ``$true`` and ``$false`` (TPTP's defined propositions, which this
|
|
1902
|
+
kit's TPTP reader produces) are the constants true and false, so z3 and a
|
|
1903
|
+
TPTP prover answer the question the TPTP text asks; the nullary atoms named
|
|
1904
|
+
``⊤`` and ``⊥`` are the same two constants.
|
|
1905
|
+
"""
|
|
1906
|
+
env = env or Z3Env()
|
|
1907
|
+
if _is_tptp_boolean_atom(self):
|
|
1908
|
+
return z3.BoolVal(_truth_constant_word(self) == "$true")
|
|
1909
|
+
z3_args = [a.to_z3(env) for a in self.args]
|
|
1910
|
+
|
|
1911
|
+
if self.predicate == "=" and len(self.args) == 2:
|
|
1912
|
+
return z3_args[0] == z3_args[1]
|
|
1913
|
+
if self.predicate == "≠" and len(self.args) == 2:
|
|
1914
|
+
return z3_args[0] != z3_args[1]
|
|
1915
|
+
|
|
1916
|
+
pred = env.get_pred(self.predicate, len(self.args))
|
|
1917
|
+
return pred(*z3_args)
|
|
1918
|
+
|
|
1919
|
+
def to_prover9(self) -> str:
|
|
1920
|
+
"""Render in Prover9 syntax, using infix notation for comparison predicates.
|
|
1921
|
+
|
|
1922
|
+
A nullary predicate renders as a propositional atom without an argument
|
|
1923
|
+
list; Prover9 rejects an empty one (``P()``). The nullary atoms ``$true``
|
|
1924
|
+
and ``$false`` (TPTP's defined propositions, see :meth:`to_z3`) are
|
|
1925
|
+
Prover9's constants ``$T`` and ``$F``.
|
|
1926
|
+
|
|
1927
|
+
A predicate whose name is no word (a space, a dot, a non-ASCII letter, a
|
|
1928
|
+
``$``-word, empty) is refused by name, at every arity: written bare it would
|
|
1929
|
+
be read as several symbols or as one of Prover9's own, and it cannot be
|
|
1930
|
+
quoted. A comparison symbol at a number of arguments other than two is
|
|
1931
|
+
refused the same way.
|
|
1932
|
+
|
|
1933
|
+
A nullary predicate that begins with an upper-case letter or an underscore
|
|
1934
|
+
— ``Rain``, the usual spelling of a proposition in this kit — is written in
|
|
1935
|
+
double quotes, ``"Rain"``. Every Prover9 file this kit writes sets
|
|
1936
|
+
``prolog_style_variables``, under which an arity-0 symbol that begins with
|
|
1937
|
+
an upper-case letter is a VARIABLE, an atom with no arguments included
|
|
1938
|
+
(measured on Prover9 2026-8A: the bare ``Rain`` is refused as "cannot be
|
|
1939
|
+
used as atomic formulas, because they are variables"). A double-quoted
|
|
1940
|
+
symbol is never a variable and is a symbol of its own, distinct from the
|
|
1941
|
+
bare word of the same letters; the kit's own Prover9 reader reads it back
|
|
1942
|
+
as this atom. A lower-case nullary predicate is written bare. The same
|
|
1943
|
+
word used both as a proposition and as a constant, or as a predicate of
|
|
1944
|
+
two arities, is one symbol to Prover9,
|
|
1945
|
+
which refuses the file; this method has no view of the other formulas and
|
|
1946
|
+
cannot see that. The problem writer (:func:`unicode_logic_kit.atp
|
|
1947
|
+
.prover9_entailment.generate_prover9_input_with_mapping`) renames such
|
|
1948
|
+
symbols to lower-case tokens, one per role, and records the renaming; text
|
|
1949
|
+
for Prover9 is built with the writer, not by joining ``to_prover9()``
|
|
1950
|
+
strings.
|
|
1951
|
+
"""
|
|
1952
|
+
if self.predicate in self.INFIX_PREDS_P9 and len(self.args) == 2:
|
|
1953
|
+
left = self.args[0].to_prover9()
|
|
1954
|
+
right = self.args[1].to_prover9()
|
|
1955
|
+
op = self.INFIX_PREDS_P9[self.predicate]
|
|
1956
|
+
return f"({left} {op} {right})"
|
|
1957
|
+
|
|
1958
|
+
if _is_tptp_boolean_atom(self):
|
|
1959
|
+
return "$T" if _truth_constant_word(self) == "$true" else "$F"
|
|
1960
|
+
|
|
1961
|
+
if not self.args:
|
|
1962
|
+
quoted = _prover9_arity_zero_symbol(self.predicate)
|
|
1963
|
+
if quoted is None:
|
|
1964
|
+
raise _prover9_name_refusal("the proposition", self.predicate)
|
|
1965
|
+
return quoted
|
|
1966
|
+
|
|
1967
|
+
name = _prover9_word(self.predicate, "the predicate", len(self.args))
|
|
1968
|
+
args_str = ", ".join(a.to_prover9() for a in self.args)
|
|
1969
|
+
return f"{name}({args_str})"
|
|
1970
|
+
|
|
1971
|
+
# The only genuine infix predicates in TPTP are equality and disequality.
|
|
1972
|
+
INFIX_PREDS_TPTP = {
|
|
1973
|
+
"=": "=",
|
|
1974
|
+
"≠": "!=",
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
# Arithmetic comparisons are TPTP dollar-word predicates, applied in
|
|
1978
|
+
# prefix/functor form ($less(a, b)) — they are NOT infix operators.
|
|
1979
|
+
PREFIX_PREDS_TPTP = {
|
|
1980
|
+
"<": "$less",
|
|
1981
|
+
">": "$greater",
|
|
1982
|
+
"≤": "$lesseq",
|
|
1983
|
+
"≥": "$greatereq",
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
def to_tptp(self) -> str:
|
|
1987
|
+
"""Render an atom in TPTP syntax.
|
|
1988
|
+
|
|
1989
|
+
Equality (=) and disequality (!=) are emitted infix — the only genuine
|
|
1990
|
+
infix predicates in TPTP. The arithmetic comparisons (<, >, ≤, ≥) are
|
|
1991
|
+
TPTP dollar-word predicates and are emitted in prefix/functor form
|
|
1992
|
+
($less(a, b), $greater(a, b), $lesseq(a, b), $greatereq(a, b)). All
|
|
1993
|
+
other predicates are emitted as identifiers with a parenthesised
|
|
1994
|
+
argument list, with only the first character folded to lower-case
|
|
1995
|
+
(see :func:`tptp_fold_first_letter`) — the exact mirror of
|
|
1996
|
+
``tptp_input.py``'s ``_cap()``, which capitalises only the first
|
|
1997
|
+
character of a parsed predicate name on import. A nullary predicate
|
|
1998
|
+
becomes a bare propositional atom.
|
|
1999
|
+
|
|
2000
|
+
The four comparisons are the ARITHMETIC spelling: a prover proves
|
|
2001
|
+
``$less(1,2)`` from its own arithmetic. On every route that was not asked for
|
|
2002
|
+
arithmetic the kit reads ``< > ≤ ≥`` as uninterpreted binary predicates, and a
|
|
2003
|
+
PROBLEM for a prover is written by the checked writers
|
|
2004
|
+
(:func:`~unicode_logic_kit.atp._tptp_problem.generate_tptp_problem_with_mapping`,
|
|
2005
|
+
:func:`~unicode_logic_kit.atp.tptp_tff.generate_tff_problem_with_mapping`), which write a
|
|
2006
|
+
comparison as an ordinary predicate of a word of its own and record it in the name
|
|
2007
|
+
map; the typed arithmetic writer
|
|
2008
|
+
(:func:`~unicode_logic_kit.atp._tff_problem.generate_tff_arith_problem`) keeps the
|
|
2009
|
+
dollar-words. Use this method for the text of ONE formula, never to assemble a
|
|
2010
|
+
problem.
|
|
2011
|
+
|
|
2012
|
+
This first-letter fold is NOT injective on its own — ``Foo`` and
|
|
2013
|
+
``foo`` both render as ``foo`` — so two distinct predicates that
|
|
2014
|
+
differ only in their first letter's case collide. Inside ONE formula
|
|
2015
|
+
the outermost ``to_tptp()`` call refuses that (see
|
|
2016
|
+
:meth:`Node.to_tptp`); across several formulas only the checked
|
|
2017
|
+
problem writers can, since a single formula has no visibility into its
|
|
2018
|
+
siblings elsewhere in the problem.
|
|
2019
|
+
|
|
2020
|
+
The two truth constants are TPTP's own words: the nullary atoms ``$true``
|
|
2021
|
+
and ``⊤`` are written ``$true``, ``$false`` and ``⊥`` are written ``$false``.
|
|
2022
|
+
"""
|
|
2023
|
+
truth_word = _truth_constant_word(self)
|
|
2024
|
+
if truth_word is not None:
|
|
2025
|
+
return truth_word
|
|
2026
|
+
|
|
2027
|
+
if self.predicate in self.INFIX_PREDS_TPTP and len(self.args) == 2:
|
|
2028
|
+
left = self.args[0].to_tptp()
|
|
2029
|
+
right = self.args[1].to_tptp()
|
|
2030
|
+
op = self.INFIX_PREDS_TPTP[self.predicate]
|
|
2031
|
+
return f"({left} {op} {right})"
|
|
2032
|
+
|
|
2033
|
+
if self.predicate in self.PREFIX_PREDS_TPTP and len(self.args) == 2:
|
|
2034
|
+
left = self.args[0].to_tptp()
|
|
2035
|
+
right = self.args[1].to_tptp()
|
|
2036
|
+
op = self.PREFIX_PREDS_TPTP[self.predicate]
|
|
2037
|
+
return f"{op}({left},{right})"
|
|
2038
|
+
|
|
2039
|
+
if not self.args:
|
|
2040
|
+
return tptp_fold_first_letter(self.predicate)
|
|
2041
|
+
|
|
2042
|
+
args_str = ",".join(a.to_tptp() for a in self.args)
|
|
2043
|
+
return f"{tptp_fold_first_letter(self.predicate)}({args_str})"
|
|
2044
|
+
|
|
2045
|
+
def _tptp_symbol(self):
|
|
2046
|
+
"""The predicate word :meth:`to_tptp` writes (none for equality and the arithmetic comparisons: fixed tokens).
|
|
2047
|
+
|
|
2048
|
+
None for the nullary atoms ``$true`` / ``$false`` either: they are TPTP's own
|
|
2049
|
+
propositions, written verbatim, and no symbol of the user's."""
|
|
2050
|
+
if _is_tptp_boolean_atom(self):
|
|
2051
|
+
return None
|
|
2052
|
+
return (_predicate_symbol, self.predicate)
|
|
2053
|
+
|
|
2054
|
+
|
|
2055
|
+
@dataclass(frozen=True)
|
|
2056
|
+
class Not(Node):
|
|
2057
|
+
"""Logical negation of a formula."""
|
|
2058
|
+
|
|
2059
|
+
formula: Node
|
|
2060
|
+
|
|
2061
|
+
def to_dict(self):
|
|
2062
|
+
"""Serialise to dict with type tag and recursively serialised subformula."""
|
|
2063
|
+
return {"_type": "Not", "formula": self.formula.to_dict()}
|
|
2064
|
+
|
|
2065
|
+
@staticmethod
|
|
2066
|
+
def from_dict(d):
|
|
2067
|
+
"""Deserialise a Not from a dict produced by to_dict."""
|
|
2068
|
+
return Not(Node.from_dict(d["formula"]))
|
|
2069
|
+
|
|
2070
|
+
def to_z3(self, env: Z3Env = None):
|
|
2071
|
+
"""Translate to a Z3 Not expression."""
|
|
2072
|
+
return z3.Not(self.formula.to_z3(env or Z3Env()))
|
|
2073
|
+
|
|
2074
|
+
@_prover9_outermost
|
|
2075
|
+
def to_prover9(self) -> str:
|
|
2076
|
+
"""Render negation in Prover9 syntax using the dash operator."""
|
|
2077
|
+
return f"-({self.formula.to_prover9()})"
|
|
2078
|
+
|
|
2079
|
+
def to_tptp(self) -> str:
|
|
2080
|
+
"""Render negation in TPTP syntax using the tilde operator."""
|
|
2081
|
+
return f"~({self.formula.to_tptp()})"
|
|
2082
|
+
|
|
2083
|
+
|
|
2084
|
+
@dataclass(frozen=True)
|
|
2085
|
+
class And(Node):
|
|
2086
|
+
"""Conjunction of two formulas."""
|
|
2087
|
+
|
|
2088
|
+
left: Node
|
|
2089
|
+
right: Node
|
|
2090
|
+
|
|
2091
|
+
def to_dict(self):
|
|
2092
|
+
"""Serialise to dict with type tag and recursively serialised operands."""
|
|
2093
|
+
return {"_type": "And", "left": self.left.to_dict(), "right": self.right.to_dict()}
|
|
2094
|
+
|
|
2095
|
+
@staticmethod
|
|
2096
|
+
def from_dict(d):
|
|
2097
|
+
"""Deserialise an And from a dict produced by to_dict."""
|
|
2098
|
+
return And(Node.from_dict(d["left"]), Node.from_dict(d["right"]))
|
|
2099
|
+
|
|
2100
|
+
def to_z3(self, env: Z3Env = None):
|
|
2101
|
+
"""Translate to a Z3 And expression."""
|
|
2102
|
+
env = env or Z3Env()
|
|
2103
|
+
return z3.And(self.left.to_z3(env), self.right.to_z3(env))
|
|
2104
|
+
|
|
2105
|
+
@_prover9_outermost
|
|
2106
|
+
def to_prover9(self) -> str:
|
|
2107
|
+
"""Render conjunction in Prover9 syntax using the ampersand operator."""
|
|
2108
|
+
return f"({self.left.to_prover9()} & {self.right.to_prover9()})"
|
|
2109
|
+
|
|
2110
|
+
def to_tptp(self) -> str:
|
|
2111
|
+
"""Render conjunction in TPTP syntax using the ampersand operator."""
|
|
2112
|
+
return f"({self.left.to_tptp()} & {self.right.to_tptp()})"
|
|
2113
|
+
|
|
2114
|
+
|
|
2115
|
+
@dataclass(frozen=True)
|
|
2116
|
+
class Or(Node):
|
|
2117
|
+
"""Disjunction of two formulas."""
|
|
2118
|
+
|
|
2119
|
+
left: Node
|
|
2120
|
+
right: Node
|
|
2121
|
+
|
|
2122
|
+
def to_dict(self):
|
|
2123
|
+
"""Serialise to dict with type tag and recursively serialised operands."""
|
|
2124
|
+
return {"_type": "Or", "left": self.left.to_dict(), "right": self.right.to_dict()}
|
|
2125
|
+
|
|
2126
|
+
@staticmethod
|
|
2127
|
+
def from_dict(d):
|
|
2128
|
+
"""Deserialise an Or from a dict produced by to_dict."""
|
|
2129
|
+
return Or(Node.from_dict(d["left"]), Node.from_dict(d["right"]))
|
|
2130
|
+
|
|
2131
|
+
def to_z3(self, env: Z3Env = None):
|
|
2132
|
+
"""Translate to a Z3 Or expression."""
|
|
2133
|
+
env = env or Z3Env()
|
|
2134
|
+
return z3.Or(self.left.to_z3(env), self.right.to_z3(env))
|
|
2135
|
+
|
|
2136
|
+
@_prover9_outermost
|
|
2137
|
+
def to_prover9(self) -> str:
|
|
2138
|
+
"""Render disjunction in Prover9 syntax using the pipe operator."""
|
|
2139
|
+
return f"({self.left.to_prover9()} | {self.right.to_prover9()})"
|
|
2140
|
+
|
|
2141
|
+
def to_tptp(self) -> str:
|
|
2142
|
+
"""Render disjunction in TPTP syntax using the pipe operator."""
|
|
2143
|
+
return f"({self.left.to_tptp()} | {self.right.to_tptp()})"
|
|
2144
|
+
|
|
2145
|
+
|
|
2146
|
+
@dataclass(frozen=True)
|
|
2147
|
+
class Xor(Node):
|
|
2148
|
+
"""Exclusive disjunction of two formulas."""
|
|
2149
|
+
|
|
2150
|
+
left: Node
|
|
2151
|
+
right: Node
|
|
2152
|
+
|
|
2153
|
+
def to_dict(self):
|
|
2154
|
+
"""Serialise to dict with type tag and recursively serialised operands."""
|
|
2155
|
+
return {"_type": "Xor", "left": self.left.to_dict(), "right": self.right.to_dict()}
|
|
2156
|
+
|
|
2157
|
+
@staticmethod
|
|
2158
|
+
def from_dict(d):
|
|
2159
|
+
"""Deserialise an Xor from a dict produced by to_dict."""
|
|
2160
|
+
return Xor(Node.from_dict(d["left"]), Node.from_dict(d["right"]))
|
|
2161
|
+
|
|
2162
|
+
def to_z3(self, env: Z3Env = None):
|
|
2163
|
+
"""Translate to a Z3 Xor expression."""
|
|
2164
|
+
env = env or Z3Env()
|
|
2165
|
+
return z3.Xor(self.left.to_z3(env), self.right.to_z3(env))
|
|
2166
|
+
|
|
2167
|
+
@_prover9_outermost
|
|
2168
|
+
def to_prover9(self) -> str:
|
|
2169
|
+
"""Render exclusive or in Prover9 syntax by expanding to (l | r) & -(l & r)."""
|
|
2170
|
+
l = self.left.to_prover9()
|
|
2171
|
+
r = self.right.to_prover9()
|
|
2172
|
+
return f"(({l} | {r}) & -(({l}) & ({r})))"
|
|
2173
|
+
|
|
2174
|
+
def to_tptp(self) -> str:
|
|
2175
|
+
"""Render exclusive or in TPTP syntax using the non-equivalence operator (<~>).
|
|
2176
|
+
|
|
2177
|
+
In TPTP ``~|`` is NOR, not XOR; ``<~>`` (non-equivalence) is the operator
|
|
2178
|
+
truth-functionally equal to exclusive or.
|
|
2179
|
+
"""
|
|
2180
|
+
return f"({self.left.to_tptp()} <~> {self.right.to_tptp()})"
|
|
2181
|
+
|
|
2182
|
+
|
|
2183
|
+
@dataclass(frozen=True)
|
|
2184
|
+
class Implies(Node):
|
|
2185
|
+
"""Material implication from left to right."""
|
|
2186
|
+
|
|
2187
|
+
left: Node
|
|
2188
|
+
right: Node
|
|
2189
|
+
|
|
2190
|
+
def to_dict(self):
|
|
2191
|
+
"""Serialise to dict with type tag and recursively serialised operands."""
|
|
2192
|
+
return {"_type": "Implies", "left": self.left.to_dict(), "right": self.right.to_dict()}
|
|
2193
|
+
|
|
2194
|
+
@staticmethod
|
|
2195
|
+
def from_dict(d):
|
|
2196
|
+
"""Deserialise an Implies from a dict produced by to_dict."""
|
|
2197
|
+
return Implies(Node.from_dict(d["left"]), Node.from_dict(d["right"]))
|
|
2198
|
+
|
|
2199
|
+
def to_z3(self, env: Z3Env = None):
|
|
2200
|
+
"""Translate to a Z3 Implies expression."""
|
|
2201
|
+
env = env or Z3Env()
|
|
2202
|
+
return z3.Implies(self.left.to_z3(env), self.right.to_z3(env))
|
|
2203
|
+
|
|
2204
|
+
@_prover9_outermost
|
|
2205
|
+
def to_prover9(self) -> str:
|
|
2206
|
+
"""Render implication in Prover9 syntax using the -> operator."""
|
|
2207
|
+
return f"({self.left.to_prover9()} -> {self.right.to_prover9()})"
|
|
2208
|
+
|
|
2209
|
+
def to_tptp(self) -> str:
|
|
2210
|
+
"""Render implication in TPTP syntax using the => operator."""
|
|
2211
|
+
return f"({self.left.to_tptp()} => {self.right.to_tptp()})"
|
|
2212
|
+
|
|
2213
|
+
|
|
2214
|
+
@dataclass(frozen=True)
|
|
2215
|
+
class Iff(Node):
|
|
2216
|
+
"""Biconditional (if and only if) between two formulas."""
|
|
2217
|
+
|
|
2218
|
+
left: Node
|
|
2219
|
+
right: Node
|
|
2220
|
+
|
|
2221
|
+
def to_dict(self):
|
|
2222
|
+
"""Serialise to dict with type tag and recursively serialised operands."""
|
|
2223
|
+
return {"_type": "Iff", "left": self.left.to_dict(), "right": self.right.to_dict()}
|
|
2224
|
+
|
|
2225
|
+
@staticmethod
|
|
2226
|
+
def from_dict(d):
|
|
2227
|
+
"""Deserialise an Iff from a dict produced by to_dict."""
|
|
2228
|
+
return Iff(Node.from_dict(d["left"]), Node.from_dict(d["right"]))
|
|
2229
|
+
|
|
2230
|
+
def to_z3(self, env: Z3Env = None):
|
|
2231
|
+
"""Translate to Z3 equality of the two boolean subexpressions."""
|
|
2232
|
+
env = env or Z3Env()
|
|
2233
|
+
return self.left.to_z3(env) == self.right.to_z3(env)
|
|
2234
|
+
|
|
2235
|
+
@_prover9_outermost
|
|
2236
|
+
def to_prover9(self) -> str:
|
|
2237
|
+
"""Render biconditional in Prover9 syntax using the <-> operator."""
|
|
2238
|
+
return f"({self.left.to_prover9()} <-> {self.right.to_prover9()})"
|
|
2239
|
+
|
|
2240
|
+
def to_tptp(self) -> str:
|
|
2241
|
+
"""Render biconditional in TPTP syntax using the <=> operator."""
|
|
2242
|
+
return f"({self.left.to_tptp()} <=> {self.right.to_tptp()})"
|
|
2243
|
+
|
|
2244
|
+
|
|
2245
|
+
@dataclass(frozen=True)
|
|
2246
|
+
class Quantifier(Node):
|
|
2247
|
+
"""A universally or existentially quantified formula over a single variable."""
|
|
2248
|
+
|
|
2249
|
+
type: str
|
|
2250
|
+
variable: Variable
|
|
2251
|
+
formula: Node
|
|
2252
|
+
|
|
2253
|
+
def to_dict(self):
|
|
2254
|
+
"""Serialise to dict with type tag, quantifier type, variable, and recursively serialised body."""
|
|
2255
|
+
return {
|
|
2256
|
+
"_type": "Quantifier",
|
|
2257
|
+
"type": self.type,
|
|
2258
|
+
"variable": self.variable.to_dict(),
|
|
2259
|
+
"formula": self.formula.to_dict()
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
@staticmethod
|
|
2263
|
+
def from_dict(d):
|
|
2264
|
+
"""Deserialise a Quantifier from a dict produced by to_dict."""
|
|
2265
|
+
return Quantifier(d["type"], Node.from_dict(d["variable"]), Node.from_dict(d["formula"]))
|
|
2266
|
+
|
|
2267
|
+
def to_z3(self, env: Z3Env = None):
|
|
2268
|
+
"""Translate to a Z3 ForAll or Exists expression over the bound variable."""
|
|
2269
|
+
env = env or Z3Env()
|
|
2270
|
+
z3_var = self.variable.to_z3(env)
|
|
2271
|
+
body = self.formula.to_z3(env)
|
|
2272
|
+
|
|
2273
|
+
if self.type in ("forall", "∀"):
|
|
2274
|
+
return z3.ForAll([z3_var], body)
|
|
2275
|
+
elif self.type in ("exists", "∃"):
|
|
2276
|
+
return z3.Exists([z3_var], body)
|
|
2277
|
+
raise ValueError(f"Unknown quantifier: {self.type}")
|
|
2278
|
+
|
|
2279
|
+
@_prover9_outermost
|
|
2280
|
+
def to_prover9(self) -> str:
|
|
2281
|
+
"""Render the quantified formula in Prover9 syntax using all/exists keywords.
|
|
2282
|
+
|
|
2283
|
+
A binder that sits inside the scope of a binder of its own name is written under a fresh
|
|
2284
|
+
variable, so that Prover9 has nothing to rename (see :meth:`Node.to_prover9`).
|
|
2285
|
+
"""
|
|
2286
|
+
var = self.variable.to_prover9()
|
|
2287
|
+
body = self.formula.to_prover9()
|
|
2288
|
+
|
|
2289
|
+
if self.type in ("forall", "∀"):
|
|
2290
|
+
return f"(all {var} {body})"
|
|
2291
|
+
elif self.type in ("exists", "∃"):
|
|
2292
|
+
return f"(exists {var} {body})"
|
|
2293
|
+
raise ValueError(f"Unknown quantifier: {self.type}")
|
|
2294
|
+
|
|
2295
|
+
def to_tptp(self) -> str:
|
|
2296
|
+
"""Render a quantified formula in TPTP syntax.
|
|
2297
|
+
|
|
2298
|
+
Universal quantification uses ! and existential uses ?,
|
|
2299
|
+
with the bound variable listed in brackets: ![X]: body or ?[X]: body.
|
|
2300
|
+
"""
|
|
2301
|
+
var = self.variable.to_tptp()
|
|
2302
|
+
body = self.formula.to_tptp()
|
|
2303
|
+
|
|
2304
|
+
if self.type in ("forall", "∀"):
|
|
2305
|
+
return f"(![{var}]: {body})"
|
|
2306
|
+
elif self.type in ("exists", "∃"):
|
|
2307
|
+
return f"(?[{var}]: {body})"
|
|
2308
|
+
raise ValueError(f"Unknown quantifier: {self.type}")
|
|
2309
|
+
|
|
2310
|
+
|
|
2311
|
+
# =========================
|
|
2312
|
+
# Counting quantifier, measure / cardinality terms, concessive connective
|
|
2313
|
+
# =========================
|
|
2314
|
+
#
|
|
2315
|
+
# Classical, non-modal extensions used by natural-language → logic front-ends
|
|
2316
|
+
# (e.g. CCG pipelines) to translate cardinal determiners, degree comparatives,
|
|
2317
|
+
# counting comparisons, and concessive coordination:
|
|
2318
|
+
#
|
|
2319
|
+
# * Count — a cardinality quantifier ∃≥n / ∃≤n / ∃=n carrying its bound n
|
|
2320
|
+
# SYMBOLICALLY (a Number, not expanded into single-letter
|
|
2321
|
+
# variables), so an arbitrarily large n is represented exactly.
|
|
2322
|
+
# It is first-order expressible; the exports lower it to the
|
|
2323
|
+
# standard distinct-witnesses encoding via _expand().
|
|
2324
|
+
# * Measure — a degree/measure term μ(entity, dimension) for bare quantity
|
|
2325
|
+
# comparatives (μ(x, height) > μ(y, height)); an uninterpreted
|
|
2326
|
+
# binary function on export.
|
|
2327
|
+
# * Cardinality — a set-cardinality term |{v : φ}| for counting comparisons
|
|
2328
|
+
# (|{v : Votes(x, v)}| > |{v : Votes(y, v)}|). Set cardinality is
|
|
2329
|
+
# a second-order notion, so it has NO first-order export.
|
|
2330
|
+
# * Contrast — a concessive connective (whereas / although / but) that is
|
|
2331
|
+
# truth-functionally ∧, but kept as a distinct node so the
|
|
2332
|
+
# concession survives translation instead of flattening to ∧.
|
|
2333
|
+
#
|
|
2334
|
+
# Count and Cardinality BIND their variable, so the binder-aware passes in
|
|
2335
|
+
# _msfl_nodes.py (free_variables / substitute / resolve_lambda_scope) special-case
|
|
2336
|
+
# them alongside Quantifier, and the renderers special-case Count / Measure /
|
|
2337
|
+
# Cardinality (binders/terms are not regular operators). Contrast IS a regular
|
|
2338
|
+
# level2 operator and is driven entirely by the operator registry — it needs no
|
|
2339
|
+
# renderer branch and no binder handling.
|
|
2340
|
+
|
|
2341
|
+
# op code -> the ∃-prefixed surface glyph (and its inverse, used by the parser).
|
|
2342
|
+
_COUNT_OPS = {"ge": "∃≥", "le": "∃≤", "eq": "∃="}
|
|
2343
|
+
_COUNT_TOKEN_TO_OP = {glyph: op for op, glyph in _COUNT_OPS.items()}
|
|
2344
|
+
|
|
2345
|
+
# Largest witness count Count._expand() will materialise. The distinct-witnesses
|
|
2346
|
+
# encoding is O(n²) constraints under n nested quantifiers, so a large n produces a
|
|
2347
|
+
# tree that is both huge and deeper than Python's recursion limit; the Count node
|
|
2348
|
+
# itself keeps n symbolically and round-trips for ANY n, so only the to_z3 / to_prover9
|
|
2349
|
+
# / to_tptp *expansion* is bounded.
|
|
2350
|
+
_COUNT_EXPAND_MAX = 500
|
|
2351
|
+
_COUNT_TOO_LARGE = (
|
|
2352
|
+
"Count.to_z3/to_prover9/to_tptp: n={n} is too large to expand to first-order "
|
|
2353
|
+
"(the distinct-witnesses encoding materialises O(n²) constraints under n nested "
|
|
2354
|
+
"quantifiers; the limit is n<={limit}). The Count node keeps n symbolically and "
|
|
2355
|
+
"round-trips via to_unicode_str / to_dict for any n — only this first-order "
|
|
2356
|
+
"lowering is bounded."
|
|
2357
|
+
)
|
|
2358
|
+
|
|
2359
|
+
|
|
2360
|
+
def _balanced_and(parts):
|
|
2361
|
+
"""Fold a non-empty list of formulas into a *balanced* And tree (shallow depth).
|
|
2362
|
+
|
|
2363
|
+
A left-associative fold would make an n-element conjunction n deep, so an
|
|
2364
|
+
O(n²)-conjunct counting expansion overflows Python's recursion limit on
|
|
2365
|
+
traversal; a balanced tree is only O(log n) deep.
|
|
2366
|
+
"""
|
|
2367
|
+
while len(parts) > 1:
|
|
2368
|
+
merged = [And(parts[i], parts[i + 1]) for i in range(0, len(parts) - 1, 2)]
|
|
2369
|
+
if len(parts) % 2:
|
|
2370
|
+
merged.append(parts[-1])
|
|
2371
|
+
parts = merged
|
|
2372
|
+
return parts[0]
|
|
2373
|
+
|
|
2374
|
+
|
|
2375
|
+
@dataclass(frozen=True)
|
|
2376
|
+
class Count(Node):
|
|
2377
|
+
"""A counting (cardinality) quantifier ∃≥n / ∃≤n / ∃=n over a single variable.
|
|
2378
|
+
|
|
2379
|
+
``op`` is ``"ge"`` / ``"le"`` / ``"eq"`` (at least / at most / exactly); ``n``
|
|
2380
|
+
is a :class:`Number` wrapping a non-negative integer bound — kept SYMBOLIC, not
|
|
2381
|
+
expanded into single-letter variables, so an arbitrarily large bound (e.g. 500)
|
|
2382
|
+
is represented exactly. ``variable`` is the bound counting variable and
|
|
2383
|
+
``formula`` its matrix. Semantics: ``∃≥n x φ`` is true iff at least ``n``
|
|
2384
|
+
DISTINCT individuals satisfy ``φ`` (``∃≤n`` at most, ``∃=n`` exactly). The
|
|
2385
|
+
counting quantifier is first-order expressible; :meth:`to_z3` / :meth:`to_prover9`
|
|
2386
|
+
/ :meth:`to_tptp` lower it to the standard distinct-witnesses encoding (see
|
|
2387
|
+
:meth:`_expand`).
|
|
2388
|
+
"""
|
|
2389
|
+
|
|
2390
|
+
op: str
|
|
2391
|
+
n: Number
|
|
2392
|
+
variable: Variable
|
|
2393
|
+
formula: Node
|
|
2394
|
+
|
|
2395
|
+
def __post_init__(self):
|
|
2396
|
+
"""Validate the op code and that n is a non-negative integer Number."""
|
|
2397
|
+
if self.op not in _COUNT_OPS:
|
|
2398
|
+
raise ValueError(
|
|
2399
|
+
f"Count: unknown op {self.op!r}; expected one of 'ge', 'le', 'eq'.")
|
|
2400
|
+
if not (isinstance(self.n, Number) and isinstance(self.n.value, int)
|
|
2401
|
+
and self.n.value >= 0):
|
|
2402
|
+
raise ValueError(
|
|
2403
|
+
"Count: n must be a Number wrapping a non-negative integer.")
|
|
2404
|
+
|
|
2405
|
+
def _tree_parts(self):
|
|
2406
|
+
"""Return the ∃≥n / ∃≤n / ∃=n label (with the bound variable) and the matrix."""
|
|
2407
|
+
return (f"{_COUNT_OPS[self.op]}{self.n.value} {self.variable.name}",
|
|
2408
|
+
[self.formula])
|
|
2409
|
+
|
|
2410
|
+
def to_dict(self):
|
|
2411
|
+
"""Serialise to dict with op, n, bound variable, and serialised matrix."""
|
|
2412
|
+
return {"_type": "Count", "op": self.op, "n": self.n.to_dict(),
|
|
2413
|
+
"variable": self.variable.to_dict(),
|
|
2414
|
+
"formula": self.formula.to_dict()}
|
|
2415
|
+
|
|
2416
|
+
@staticmethod
|
|
2417
|
+
def from_dict(d):
|
|
2418
|
+
"""Deserialise a Count from a dict produced by to_dict."""
|
|
2419
|
+
return Count(d["op"], Node.from_dict(d["n"]),
|
|
2420
|
+
Node.from_dict(d["variable"]), Node.from_dict(d["formula"]))
|
|
2421
|
+
|
|
2422
|
+
def _expand(self, avoid_names=None) -> "Node":
|
|
2423
|
+
"""Lower to plain FOL via the standard distinct-witnesses counting encoding.
|
|
2424
|
+
|
|
2425
|
+
``avoid_names`` is a set the caller owns: every name in it is avoided as a
|
|
2426
|
+
witness too (the problem writers pass every variable name of the whole
|
|
2427
|
+
problem), and every witness minted is added to it, so that a second
|
|
2428
|
+
expansion with the same set mints other names.
|
|
2429
|
+
|
|
2430
|
+
``∃≥m x φ`` becomes ``∃x0 … ∃x{m-1} (⋀ φ[x_i] ∧ ⋀_{i<j} x_i ≠ x_j)``;
|
|
2431
|
+
``∃≤n`` is ``¬(∃≥n+1)``; ``∃=n`` is ``∃≥n ∧ ¬(∃≥n+1)``. The witnesses are
|
|
2432
|
+
named like the counting variable, one letter and digits (``x0``, ``x1``, …:
|
|
2433
|
+
the shape the VARIABLE terminal reads back, so the printed expansion parses),
|
|
2434
|
+
and they avoid EVERY name in the matrix, bound ones included, so the
|
|
2435
|
+
substitution below never has to rename an inner binder. The bound variable
|
|
2436
|
+
is substituted out capture-avoidingly, so the result is a closed,
|
|
2437
|
+
meaning-preserving classical formula.
|
|
2438
|
+
|
|
2439
|
+
"Every name" means a name of EVERY kind that the matrix holds
|
|
2440
|
+
(:func:`~unicode_logic_kit.fol._identifiers.symbol_names`): the constants,
|
|
2441
|
+
and also the functions (the nullary one is a constant), the predicates and
|
|
2442
|
+
the sorts. A constant may be spelled like a variable — the grammar writes
|
|
2443
|
+
one in quotes (``'y0'``), a caller who builds nodes can make one, and so
|
|
2444
|
+
do the description-logic image (an individual named ``y0``) and the TPTP
|
|
2445
|
+
reader (``p(x0)``). ``Variable("y0")`` and ``Constant("y0")`` are two
|
|
2446
|
+
nodes, and the Unicode text (``y0`` against ``'y0'``) and Z3 keep them
|
|
2447
|
+
apart; a target that writes both as one symbol does not, and there a
|
|
2448
|
+
witness named ``y0`` would CAPTURE that constant: ``∃≥1 y1 r(y0, y1)``
|
|
2449
|
+
used to expand to an ``∃y0`` over ``r`` of the constant ``y0`` and the
|
|
2450
|
+
variable ``y0``, which such a target reads as ``∃y0 r(y0, y0)`` and an
|
|
2451
|
+
irreflexive ``r`` contradicts — a consistent knowledge base came out
|
|
2452
|
+
inconsistent. So a witness avoids every constant, for every target. A
|
|
2453
|
+
witness named like a predicate or
|
|
2454
|
+
a function is the same defect in SMT-LIB text, where a bound variable and
|
|
2455
|
+
the symbol it shadows are one identifier (``(exists ((x0 S)) (x0 x0))``).
|
|
2456
|
+
What the matrix does not hold is the caller's to pass: the other formulas
|
|
2457
|
+
of the problem, in ``avoid_names``.
|
|
2458
|
+
"""
|
|
2459
|
+
from ._msfl_nodes import substitute # lazy: avoid import cycle
|
|
2460
|
+
if self.n.value > _COUNT_EXPAND_MAX:
|
|
2461
|
+
raise NotImplementedError(
|
|
2462
|
+
_COUNT_TOO_LARGE.format(n=self.n.value, limit=_COUNT_EXPAND_MAX))
|
|
2463
|
+
var, phi = self.variable, self.formula
|
|
2464
|
+
avoid = set(_identifiers.symbol_names(phi)) | {var.name}
|
|
2465
|
+
if avoid_names is not None:
|
|
2466
|
+
avoid |= avoid_names
|
|
2467
|
+
|
|
2468
|
+
def fresh(k):
|
|
2469
|
+
"""Return k fresh Variables not clashing with the matrix or each other."""
|
|
2470
|
+
out = []
|
|
2471
|
+
for _ in range(k):
|
|
2472
|
+
name = _identifiers.fresh_variable_like(var.name, avoid)
|
|
2473
|
+
avoid.add(name)
|
|
2474
|
+
if avoid_names is not None:
|
|
2475
|
+
avoid_names.add(name)
|
|
2476
|
+
out.append(Variable(name))
|
|
2477
|
+
return out
|
|
2478
|
+
|
|
2479
|
+
def at_least(m):
|
|
2480
|
+
"""Build the plain-FOL 'at least m distinct φ' formula."""
|
|
2481
|
+
if m <= 0:
|
|
2482
|
+
# '≥ 0' is vacuously true: ∃x (φ ∨ ¬φ), valid on a non-empty domain.
|
|
2483
|
+
w = fresh(1)[0]
|
|
2484
|
+
g = substitute(phi, var, w)
|
|
2485
|
+
return Quantifier("∃", w, Or(g, Not(g)))
|
|
2486
|
+
ws = fresh(m)
|
|
2487
|
+
conjuncts = [substitute(phi, var, w) for w in ws]
|
|
2488
|
+
conjuncts += [Atom("≠", [ws[i], ws[j]])
|
|
2489
|
+
for i in range(m) for j in range(i + 1, m)]
|
|
2490
|
+
body = _balanced_and(conjuncts) # balanced ⇒ shallow recursion
|
|
2491
|
+
for w in reversed(ws):
|
|
2492
|
+
body = Quantifier("∃", w, body)
|
|
2493
|
+
return body
|
|
2494
|
+
|
|
2495
|
+
if self.op == "ge":
|
|
2496
|
+
return at_least(self.n.value)
|
|
2497
|
+
if self.op == "le":
|
|
2498
|
+
return Not(at_least(self.n.value + 1))
|
|
2499
|
+
return And(at_least(self.n.value), Not(at_least(self.n.value + 1)))
|
|
2500
|
+
|
|
2501
|
+
def to_z3(self, env: Z3Env = None):
|
|
2502
|
+
"""Lower to the distinct-witnesses encoding, then translate to Z3."""
|
|
2503
|
+
return self._expand().to_z3(env)
|
|
2504
|
+
|
|
2505
|
+
@_prover9_outermost
|
|
2506
|
+
def to_prover9(self) -> str:
|
|
2507
|
+
"""Lower to the distinct-witnesses encoding, then render Prover9 syntax.
|
|
2508
|
+
|
|
2509
|
+
The witnesses are fresh against every name of the whole node that is written, of every
|
|
2510
|
+
kind, compared case-folded, because Prover9 writes a variable in upper case: ``x0`` and
|
|
2511
|
+
``X0`` are one variable there (see :meth:`Node.to_prover9`).
|
|
2512
|
+
"""
|
|
2513
|
+
return self._expand().to_prover9()
|
|
2514
|
+
|
|
2515
|
+
def to_tptp(self) -> str:
|
|
2516
|
+
"""Lower to the distinct-witnesses encoding, then render TPTP syntax."""
|
|
2517
|
+
return self._expand().to_tptp()
|
|
2518
|
+
|
|
2519
|
+
|
|
2520
|
+
@dataclass(frozen=True)
|
|
2521
|
+
class Measure(Node):
|
|
2522
|
+
"""A degree/measure term μ(entity, dimension): the degree to which ``entity`` has
|
|
2523
|
+
the gradable dimension ``dimension``.
|
|
2524
|
+
|
|
2525
|
+
A first-class measure-function term, the clean argument for bare (standard-less)
|
|
2526
|
+
quantity comparatives — ``more water`` / ``taller`` become ``μ(x, dim) > μ(y, dim)``
|
|
2527
|
+
rather than a thin relational ``More(x, c)``. Both children are terms. On export it
|
|
2528
|
+
is the uninterpreted binary function ``measure(entity, dimension)`` (the ``μ`` glyph
|
|
2529
|
+
is ASCII-folded to ``measure`` so the first-order back-ends accept it), and ``>`` / ``<``
|
|
2530
|
+
over the resulting degrees use the back-end's ordering.
|
|
2531
|
+
"""
|
|
2532
|
+
|
|
2533
|
+
entity: Node
|
|
2534
|
+
dimension: Node
|
|
2535
|
+
|
|
2536
|
+
def _tree_parts(self):
|
|
2537
|
+
"""Return the μ label and the entity/dimension children."""
|
|
2538
|
+
return "μ", [self.entity, self.dimension]
|
|
2539
|
+
|
|
2540
|
+
def to_dict(self):
|
|
2541
|
+
"""Serialise to dict with serialised entity and dimension terms."""
|
|
2542
|
+
return {"_type": "Measure", "entity": self.entity.to_dict(),
|
|
2543
|
+
"dimension": self.dimension.to_dict()}
|
|
2544
|
+
|
|
2545
|
+
@staticmethod
|
|
2546
|
+
def from_dict(d):
|
|
2547
|
+
"""Deserialise a Measure from a dict produced by to_dict."""
|
|
2548
|
+
return Measure(Node.from_dict(d["entity"]), Node.from_dict(d["dimension"]))
|
|
2549
|
+
|
|
2550
|
+
def to_z3(self, env: Z3Env = None):
|
|
2551
|
+
"""Translate to an uninterpreted binary Z3 function ``measure`` in sort S."""
|
|
2552
|
+
env = env or Z3Env()
|
|
2553
|
+
return env.get_func("measure", 2)(self.entity.to_z3(env), self.dimension.to_z3(env))
|
|
2554
|
+
|
|
2555
|
+
def to_prover9(self) -> str:
|
|
2556
|
+
"""Render as the Prover9 function ``measure(entity, dimension)``."""
|
|
2557
|
+
return f"measure({self.entity.to_prover9()}, {self.dimension.to_prover9()})"
|
|
2558
|
+
|
|
2559
|
+
def to_tptp(self) -> str:
|
|
2560
|
+
"""Render as the TPTP function ``measure(entity, dimension)``."""
|
|
2561
|
+
return f"measure({self.entity.to_tptp()},{self.dimension.to_tptp()})"
|
|
2562
|
+
|
|
2563
|
+
def _tptp_symbol(self):
|
|
2564
|
+
"""The function ``measure`` this node writes — the very symbol ``Function('measure', ...)`` is,
|
|
2565
|
+
so it collides with a differently-spelled ``Function('Measure', ...)`` and with nothing else."""
|
|
2566
|
+
return (_measure_symbol, "measure")
|
|
2567
|
+
|
|
2568
|
+
|
|
2569
|
+
# Shared rejection message: a set-cardinality term is not first-order definable.
|
|
2570
|
+
_NO_CARDINALITY_EXPORT = (
|
|
2571
|
+
"Cardinality terms (|{v : φ}|) denote set cardinality, a second-order notion "
|
|
2572
|
+
"with no first-order counterpart — counting comparisons such as 'more … than …' "
|
|
2573
|
+
"are not first-order definable. Keep the term at the AST level, or express a "
|
|
2574
|
+
"fixed-bound count with the Count quantifier (∃≥n / ∃≤n / ∃=n)."
|
|
2575
|
+
)
|
|
2576
|
+
|
|
2577
|
+
|
|
2578
|
+
@dataclass(frozen=True)
|
|
2579
|
+
class Cardinality(Node):
|
|
2580
|
+
"""A set-cardinality term ``|{v : φ}|``: how many ``v`` satisfy ``φ``.
|
|
2581
|
+
|
|
2582
|
+
The first-class ``|S|`` term behind faithful counting comparisons — ``more votes
|
|
2583
|
+
than`` becomes ``|{v : Votes(x, v)}| > |{v : Votes(y, v)}|``. It BINDS ``variable``
|
|
2584
|
+
over the matrix ``formula``. Set cardinality is genuinely second-order, so it has
|
|
2585
|
+
no first-order export: :meth:`to_z3` / :meth:`to_prover9` / :meth:`to_tptp` reject.
|
|
2586
|
+
"""
|
|
2587
|
+
|
|
2588
|
+
variable: Variable
|
|
2589
|
+
formula: Node
|
|
2590
|
+
|
|
2591
|
+
def _tree_parts(self):
|
|
2592
|
+
"""Return the |v| cardinality label (with the bound variable) and the matrix."""
|
|
2593
|
+
return f"|{self.variable.name}|", [self.formula]
|
|
2594
|
+
|
|
2595
|
+
def to_dict(self):
|
|
2596
|
+
"""Serialise to dict with the bound variable and serialised matrix."""
|
|
2597
|
+
return {"_type": "Cardinality", "variable": self.variable.to_dict(),
|
|
2598
|
+
"formula": self.formula.to_dict()}
|
|
2599
|
+
|
|
2600
|
+
@staticmethod
|
|
2601
|
+
def from_dict(d):
|
|
2602
|
+
"""Deserialise a Cardinality from a dict produced by to_dict."""
|
|
2603
|
+
return Cardinality(Node.from_dict(d["variable"]), Node.from_dict(d["formula"]))
|
|
2604
|
+
|
|
2605
|
+
def to_z3(self, env: Z3Env = None):
|
|
2606
|
+
"""Reject Z3 export: set cardinality has no first-order counterpart."""
|
|
2607
|
+
raise NotImplementedError(_NO_CARDINALITY_EXPORT)
|
|
2608
|
+
|
|
2609
|
+
def to_prover9(self) -> str:
|
|
2610
|
+
"""Reject Prover9 export: set cardinality has no first-order counterpart."""
|
|
2611
|
+
raise NotImplementedError(_NO_CARDINALITY_EXPORT)
|
|
2612
|
+
|
|
2613
|
+
def to_tptp(self) -> str:
|
|
2614
|
+
"""Reject TPTP export: set cardinality has no first-order counterpart."""
|
|
2615
|
+
raise NotImplementedError(_NO_CARDINALITY_EXPORT)
|
|
2616
|
+
|
|
2617
|
+
|
|
2618
|
+
@dataclass(frozen=True)
|
|
2619
|
+
class Contrast(Node):
|
|
2620
|
+
"""A concessive (contrastive) connective ``P Ⓒ Q`` — whereas / although / but.
|
|
2621
|
+
|
|
2622
|
+
Truth-functionally identical to classical conjunction (concession is a discourse
|
|
2623
|
+
relation, not a truth-functional one), but kept as a distinct node so a front-end
|
|
2624
|
+
can preserve the contrast rather than flattening it to ∧. Exports therefore behave
|
|
2625
|
+
exactly like :class:`And`.
|
|
2626
|
+
"""
|
|
2627
|
+
|
|
2628
|
+
left: Node
|
|
2629
|
+
right: Node
|
|
2630
|
+
|
|
2631
|
+
def to_dict(self):
|
|
2632
|
+
"""Serialise to dict with type tag and recursively serialised operands."""
|
|
2633
|
+
return {"_type": "Contrast", "left": self.left.to_dict(), "right": self.right.to_dict()}
|
|
2634
|
+
|
|
2635
|
+
@staticmethod
|
|
2636
|
+
def from_dict(d):
|
|
2637
|
+
"""Deserialise a Contrast from a dict produced by to_dict."""
|
|
2638
|
+
return Contrast(Node.from_dict(d["left"]), Node.from_dict(d["right"]))
|
|
2639
|
+
|
|
2640
|
+
def to_z3(self, env: Z3Env = None):
|
|
2641
|
+
"""Translate like And (concession is truth-functionally conjunction)."""
|
|
2642
|
+
env = env or Z3Env()
|
|
2643
|
+
return z3.And(self.left.to_z3(env), self.right.to_z3(env))
|
|
2644
|
+
|
|
2645
|
+
@_prover9_outermost
|
|
2646
|
+
def to_prover9(self) -> str:
|
|
2647
|
+
"""Render like And, using the Prover9 ampersand operator."""
|
|
2648
|
+
return f"({self.left.to_prover9()} & {self.right.to_prover9()})"
|
|
2649
|
+
|
|
2650
|
+
def to_tptp(self) -> str:
|
|
2651
|
+
"""Render like And, using the TPTP ampersand operator."""
|
|
2652
|
+
return f"({self.left.to_tptp()} & {self.right.to_tptp()})"
|
|
2653
|
+
|
|
2654
|
+
|
|
2655
|
+
# =========================
|
|
2656
|
+
# Registry
|
|
2657
|
+
# =========================
|
|
2658
|
+
|
|
2659
|
+
NODE_CLASSES = {
|
|
2660
|
+
"Variable": Variable, "Constant": Constant, "Number": Number,
|
|
2661
|
+
"Function": Function, "Atom": Atom, "Not": Not, "And": And,
|
|
2662
|
+
"Or": Or, "Xor": Xor, "Implies": Implies, "Iff": Iff,
|
|
2663
|
+
"Quantifier": Quantifier,
|
|
2664
|
+
"Count": Count, "Measure": Measure, "Cardinality": Cardinality,
|
|
2665
|
+
}
|
|
2666
|
+
|
|
2667
|
+
|
|
2668
|
+
# =========================
|
|
2669
|
+
# Operator registry (self-registration)
|
|
2670
|
+
# =========================
|
|
2671
|
+
#
|
|
2672
|
+
# A formula operator (a connective/modal that the precedence-driven renderers in
|
|
2673
|
+
# _msfl_nodes.py format) registers ONE OperatorSpec here next to its class
|
|
2674
|
+
# definition. The renderers then drive every regular operator from this registry,
|
|
2675
|
+
# so adding an operator no longer means editing the central rendering tables.
|
|
2676
|
+
#
|
|
2677
|
+
# Each spec records, byte-for-byte, what the renderer emits:
|
|
2678
|
+
# - unicode: the glyph or prefix string for to_unicode_str (e.g. '¬', '□', 'K_').
|
|
2679
|
+
# - latex: the LaTeX markup for to_latex, including any trailing space that
|
|
2680
|
+
# the current renderer emits (e.g. '\\lnot ', '\\Box ', 'K').
|
|
2681
|
+
# - fixity: how the renderer arranges the operand(s) and the glyph.
|
|
2682
|
+
# - precedence: the formula precedence (higher binds tighter): 4 for prefix /
|
|
2683
|
+
# agent_prefix, 1 for binary_iff, 2 for binary_implies, 2.5 for
|
|
2684
|
+
# binary_until, 3 for level2.
|
|
2685
|
+
#
|
|
2686
|
+
# The "binders" (Quantifier, SortedQuantifier, SecondOrderQuantifier), Lambda and
|
|
2687
|
+
# Application keep their explicit handling in the renderers and a small static
|
|
2688
|
+
# precedence table — they are NOT regular operators and do NOT register here.
|
|
2689
|
+
|
|
2690
|
+
_VALID_FIXITIES = frozenset({
|
|
2691
|
+
"prefix", "agent_prefix",
|
|
2692
|
+
"binary_iff", "binary_implies", "binary_until",
|
|
2693
|
+
"level2",
|
|
2694
|
+
})
|
|
2695
|
+
|
|
2696
|
+
|
|
2697
|
+
@dataclass(frozen=True)
|
|
2698
|
+
class OperatorSpec:
|
|
2699
|
+
"""A renderer-facing description of one formula operator.
|
|
2700
|
+
|
|
2701
|
+
name is the node class ``__name__`` (the renderers dispatch by class name).
|
|
2702
|
+
fixity is one of 'prefix', 'agent_prefix', 'binary_iff', 'binary_implies',
|
|
2703
|
+
'binary_until', 'level2'. unicode/latex are the EXACT strings the
|
|
2704
|
+
to_unicode_str / to_latex renderers emit for the operator's glyph or prefix
|
|
2705
|
+
(latex includes any trailing space). precedence is the formula precedence
|
|
2706
|
+
used for parenthesisation (a float; .5 values let an operator sit between two
|
|
2707
|
+
integer levels, as Until does at 2.5).
|
|
2708
|
+
"""
|
|
2709
|
+
|
|
2710
|
+
name: str
|
|
2711
|
+
fixity: str
|
|
2712
|
+
unicode: str
|
|
2713
|
+
latex: str
|
|
2714
|
+
precedence: float
|
|
2715
|
+
|
|
2716
|
+
|
|
2717
|
+
# name -> OperatorSpec. Populated by register_operator as each node module is
|
|
2718
|
+
# imported. The renderers in _msfl_nodes.py read this dict directly.
|
|
2719
|
+
OPERATORS: Dict[str, OperatorSpec] = {}
|
|
2720
|
+
|
|
2721
|
+
|
|
2722
|
+
def register_operator(node_class, fixity: str, unicode: str, latex: str,
|
|
2723
|
+
precedence: float) -> OperatorSpec:
|
|
2724
|
+
"""Register ``node_class`` as a renderable formula operator.
|
|
2725
|
+
|
|
2726
|
+
Records an OperatorSpec under ``node_class.__name__`` in OPERATORS and adds
|
|
2727
|
+
the class to NODE_CLASSES (so from_dict/serialisation see it too). Safe to
|
|
2728
|
+
call more than once for the same class — the latest call overwrites the
|
|
2729
|
+
previous spec (idempotent / overwrite-safe). Returns the stored OperatorSpec.
|
|
2730
|
+
|
|
2731
|
+
A node registered here is driven entirely by the central renderers via its
|
|
2732
|
+
spec, so no edit to _msfl_nodes.py is needed to render a new operator.
|
|
2733
|
+
"""
|
|
2734
|
+
if fixity not in _VALID_FIXITIES:
|
|
2735
|
+
raise ValueError(
|
|
2736
|
+
f"register_operator: unknown fixity {fixity!r}; "
|
|
2737
|
+
f"expected one of {sorted(_VALID_FIXITIES)}"
|
|
2738
|
+
)
|
|
2739
|
+
name = node_class.__name__
|
|
2740
|
+
spec = OperatorSpec(name, fixity, unicode, latex, float(precedence))
|
|
2741
|
+
OPERATORS[name] = spec
|
|
2742
|
+
NODE_CLASSES[name] = node_class
|
|
2743
|
+
return spec
|
|
2744
|
+
|
|
2745
|
+
|
|
2746
|
+
# Register the classical operators next to their class definitions above.
|
|
2747
|
+
register_operator(Not, "prefix", "¬", "\\lnot ", 4)
|
|
2748
|
+
register_operator(And, "level2", "∧", "\\land", 3)
|
|
2749
|
+
register_operator(Or, "level2", "∨", "\\lor", 3)
|
|
2750
|
+
register_operator(Xor, "level2", "⊕", "\\oplus", 3)
|
|
2751
|
+
register_operator(Implies, "binary_implies", "→", "\\rightarrow", 2)
|
|
2752
|
+
register_operator(Iff, "binary_iff", "↔", "\\leftrightarrow", 1)
|
|
2753
|
+
|
|
2754
|
+
|
|
2755
|
+
# =========================
|
|
2756
|
+
# Parser registry (self-assembling grammar)
|
|
2757
|
+
# =========================
|
|
2758
|
+
#
|
|
2759
|
+
# A SECOND, parser-facing registry sits alongside the renderer registry above.
|
|
2760
|
+
# Where OperatorSpec records how a node is *rendered*, ParserOp records how an
|
|
2761
|
+
# operator is *parsed*: which grammar mode it belongs to, which precedence level
|
|
2762
|
+
# it slots into, the grammar fragment it contributes, and the transform that
|
|
2763
|
+
# turns the matched tokens into a Node. MSFLParser reads this registry per mode
|
|
2764
|
+
# to build BOTH the Lark grammar string and the Transformer — so adding an
|
|
2765
|
+
# operator is a registry entry in the operator's own module, with no edit to
|
|
2766
|
+
# msflparser.py or the grammar skeleton.
|
|
2767
|
+
#
|
|
2768
|
+
# The same glyph maps to different nodes in different modes (∧ → And in FOL but
|
|
2769
|
+
# WeakConjunction in MSFL), so registration is PER (mode, operator): a node may
|
|
2770
|
+
# register several ParserOps, one per mode it appears in.
|
|
2771
|
+
#
|
|
2772
|
+
# Levels mirror the grammar's precedence layering (loosest first):
|
|
2773
|
+
# biimplication the right-assoc ↔ rule (one op per mode)
|
|
2774
|
+
# implication the right-assoc → rule (one op per mode)
|
|
2775
|
+
# until the right-assoc Ⓤ rule (modal only) (one op per mode)
|
|
2776
|
+
# level2 the no-mixing same-level group ∧∨⊕⊗ (one only_X rule each)
|
|
2777
|
+
# prefix ¬ and the prefix modal/temporal ops (the prefix rule's alts)
|
|
2778
|
+
# quantifier ∀/∃ over a variable or predicate (the quantifier alts)
|
|
2779
|
+
#
|
|
2780
|
+
# The shared term/atom/lambda/application layer is NOT registry-driven; it lives
|
|
2781
|
+
# verbatim in the base template, identical across every mode (the only term-layer
|
|
2782
|
+
# variation, sorted vs. plain constants, is selected by the SORTED flag below).
|
|
2783
|
+
|
|
2784
|
+
_VALID_PARSE_LEVELS = frozenset({
|
|
2785
|
+
"prefix", "level2", "implication", "biimplication", "until", "quantifier",
|
|
2786
|
+
})
|
|
2787
|
+
|
|
2788
|
+
_VALID_MODES = frozenset({"fol", "msfol", "msfl", "fl", "modal", "second_order",
|
|
2789
|
+
"higher_order",
|
|
2790
|
+
"dependence", "linear", "lambek"})
|
|
2791
|
+
|
|
2792
|
+
|
|
2793
|
+
@dataclass(frozen=True)
|
|
2794
|
+
class ParserOp:
|
|
2795
|
+
"""A parser-facing description of how one operator is parsed in one mode.
|
|
2796
|
+
|
|
2797
|
+
mode : the grammar mode this binding applies to (one of _VALID_MODES).
|
|
2798
|
+
level : the precedence level it slots into (one of _VALID_PARSE_LEVELS).
|
|
2799
|
+
terminal_name : name of a named terminal to declare, or "" if the operator
|
|
2800
|
+
uses an inline string literal (the common case for the glyph
|
|
2801
|
+
connectives — kept inline so the error-path terminal patterns
|
|
2802
|
+
match the legacy grammars byte-for-byte).
|
|
2803
|
+
terminal_def : the full terminal declaration line (e.g. 'BOX: "□"' or
|
|
2804
|
+
'KNOWS.5: /K_.../'), or "" when terminal_name is "".
|
|
2805
|
+
grammar : the right-hand side of the grammar alternative this op contributes,
|
|
2806
|
+
already referencing the shared rule names (e.g. '"¬" prefix',
|
|
2807
|
+
'BOX prefix', or '(FORALL | EXISTS) VARIABLE prefix'). For a
|
|
2808
|
+
level2 op this is instead the glyph literal (e.g. '"∧"'), since
|
|
2809
|
+
level2 ops are spliced into the generated only_X / same_level_ops
|
|
2810
|
+
rules rather than contributing a free-standing alternative.
|
|
2811
|
+
rule_alias : the Lark rule alias (-> rule_alias) that names the parse node;
|
|
2812
|
+
the matching transform is attached to the assembled Transformer
|
|
2813
|
+
under this same name.
|
|
2814
|
+
transform : function(items) -> Node implementing the alias's handler.
|
|
2815
|
+
node_class : the Node subclass produced (recorded for introspection/tests).
|
|
2816
|
+
only_name : for level2 ops only, the generated only_X rule name (e.g.
|
|
2817
|
+
"only_and"); "" for every other level.
|
|
2818
|
+
"""
|
|
2819
|
+
|
|
2820
|
+
mode: str
|
|
2821
|
+
level: str
|
|
2822
|
+
terminal_name: str
|
|
2823
|
+
terminal_def: str
|
|
2824
|
+
grammar: str
|
|
2825
|
+
rule_alias: str
|
|
2826
|
+
transform: object
|
|
2827
|
+
node_class: object
|
|
2828
|
+
only_name: str = ""
|
|
2829
|
+
|
|
2830
|
+
|
|
2831
|
+
# Append-only list of every parser binding, populated by register_parser_op as
|
|
2832
|
+
# each node module imports. MSFLParser filters it by mode at construction time.
|
|
2833
|
+
PARSER_OPS: List[ParserOp] = []
|
|
2834
|
+
|
|
2835
|
+
|
|
2836
|
+
def register_parser_op(node_class, mode: str, level: str, rule_alias: str,
|
|
2837
|
+
grammar: str, transform, *,
|
|
2838
|
+
terminal_name: str = "", terminal_def: str = "",
|
|
2839
|
+
only_name: str = "") -> ParserOp:
|
|
2840
|
+
"""Register one parser binding for ``node_class`` in grammar mode ``mode``.
|
|
2841
|
+
|
|
2842
|
+
Appends a ParserOp to PARSER_OPS. ``transform(items) -> Node`` is the handler
|
|
2843
|
+
Lark calls for the ``rule_alias`` reduction; ``grammar`` is the alternative's
|
|
2844
|
+
right-hand side (or, for a level2 op, the bare glyph literal). Named terminals
|
|
2845
|
+
are declared via ``terminal_name``/``terminal_def``; inline string operators
|
|
2846
|
+
leave both empty. Returns the stored ParserOp.
|
|
2847
|
+
|
|
2848
|
+
This is additive to register_operator (which handles rendering): a fully
|
|
2849
|
+
self-describing operator calls both — register_operator for the renderers,
|
|
2850
|
+
register_parser_op (once per mode) for the parser.
|
|
2851
|
+
"""
|
|
2852
|
+
if mode not in _VALID_MODES:
|
|
2853
|
+
raise ValueError(
|
|
2854
|
+
f"register_parser_op: unknown mode {mode!r}; "
|
|
2855
|
+
f"expected one of {sorted(_VALID_MODES)}"
|
|
2856
|
+
)
|
|
2857
|
+
if level not in _VALID_PARSE_LEVELS:
|
|
2858
|
+
raise ValueError(
|
|
2859
|
+
f"register_parser_op: unknown level {level!r}; "
|
|
2860
|
+
f"expected one of {sorted(_VALID_PARSE_LEVELS)}"
|
|
2861
|
+
)
|
|
2862
|
+
op = ParserOp(mode, level, terminal_name, terminal_def, grammar,
|
|
2863
|
+
rule_alias, transform, node_class, only_name)
|
|
2864
|
+
PARSER_OPS.append(op)
|
|
2865
|
+
return op
|
|
2866
|
+
|
|
2867
|
+
|
|
2868
|
+
def parser_ops_for_mode(mode: str) -> List[ParserOp]:
|
|
2869
|
+
"""Return every registered ParserOp whose mode matches ``mode`` (in order)."""
|
|
2870
|
+
return [op for op in PARSER_OPS if op.mode == mode]
|
|
2871
|
+
|
|
2872
|
+
|
|
2873
|
+
# ---------------------------------------------------------------------------
|
|
2874
|
+
# Base grammar template
|
|
2875
|
+
# ---------------------------------------------------------------------------
|
|
2876
|
+
#
|
|
2877
|
+
# ONE skeleton shared by every mode. The %%MARKERS%% are filled by
|
|
2878
|
+
# build_grammar() from the mode's ParserOps. The structure: right-assoc ↔ and →,
|
|
2879
|
+
# the optional Until sub-level, the no-mixing only_X same_level_ops group, the
|
|
2880
|
+
# prefix level
|
|
2881
|
+
# (¬ plus any prefix modal ops, then quantifier / atom / grouping), the tight
|
|
2882
|
+
# quantifier binding (body is the prefix level), and the verbatim
|
|
2883
|
+
# term/atom/lambda/application layer.
|
|
2884
|
+
#
|
|
2885
|
+
# Normalised internal rule names: the legacy grammars used negation /
|
|
2886
|
+
# luk_negation / modal for the prefix level and biimplication / luk_biimplication
|
|
2887
|
+
# (etc.) for the binary levels; because Lark inlines ?-rules and only the ->
|
|
2888
|
+
# aliases name tree nodes, these internal names are irrelevant to the produced
|
|
2889
|
+
# AST, so the template uses single uniform names (prefix, biimplication,
|
|
2890
|
+
# implication, until, same_level_ops). The %%...%% markers:
|
|
2891
|
+
# %%TERMINAL_IMPORTS%% the (...) list imported from .terminals (NUMBER,
|
|
2892
|
+
# FORALL, EXISTS, LAMBDA — the identifier terminals
|
|
2893
|
+
# PREDICATE/CONSTANT/NAME/VARIABLE/SORT are generated
|
|
2894
|
+
# by fol/_identifiers.py and land in TERMINAL_DEFS
|
|
2895
|
+
# instead; see that module's docstring for why)
|
|
2896
|
+
# %%TERMINAL_DEFS%% the generated identifier terminals, followed by any
|
|
2897
|
+
# extra named-terminal declarations (modal ops)
|
|
2898
|
+
# %%BIIMPL_OPS%% the ↔ alternative(s)
|
|
2899
|
+
# %%IMPL_OPS%% the → alternative(s)
|
|
2900
|
+
# %%IMPL_BODY%% rule the → level reduces to: "until" or "same_level_ops"
|
|
2901
|
+
# %%UNTIL_BLOCK%% the whole until rule (modal) or empty
|
|
2902
|
+
# %%LEVEL2_ALTS%% the same_level_ops alternation members (only_X | … )
|
|
2903
|
+
# %%ONLY_RULES%% the only_X rule definitions
|
|
2904
|
+
# %%PREFIX_OPS%% the prefix alternatives contributed by ops (¬, modal …)
|
|
2905
|
+
# %%QUANT_OPS%% the quantifier alternative(s)
|
|
2906
|
+
# %%CONST_ALTS%% the atom_term constant rules (plain vs. sorted)
|
|
2907
|
+
|
|
2908
|
+
_BASE_GRAMMAR_TEMPLATE = '''\
|
|
2909
|
+
%import .terminals (%%TERMINAL_IMPORTS%%)
|
|
2910
|
+
%import common.WS
|
|
2911
|
+
%%TERMINAL_DEFS%%
|
|
2912
|
+
?start: formula
|
|
2913
|
+
|
|
2914
|
+
?formula: biimplication
|
|
2915
|
+
| lambda_
|
|
2916
|
+
| application_
|
|
2917
|
+
|
|
2918
|
+
?biimplication: implication
|
|
2919
|
+
%%BIIMPL_OPS%%
|
|
2920
|
+
|
|
2921
|
+
?implication: %%IMPL_BODY%%
|
|
2922
|
+
%%IMPL_OPS%%
|
|
2923
|
+
%%UNTIL_BLOCK%%
|
|
2924
|
+
?same_level_ops: %%LEVEL2_ALTS%%
|
|
2925
|
+
%%ONLY_RULES%%
|
|
2926
|
+
?prefix: %%PREFIX_OPS%%
|
|
2927
|
+
| quantifier
|
|
2928
|
+
| atom
|
|
2929
|
+
| "(" formula ")"
|
|
2930
|
+
| "[" formula "]"
|
|
2931
|
+
|
|
2932
|
+
?quantifier: %%QUANT_OPS%%
|
|
2933
|
+
|
|
2934
|
+
?atom: infix_predicate
|
|
2935
|
+
| PREDICATE "(" %%ATOM_ARGS%% ")" -> atom_
|
|
2936
|
+
| PREDICATE -> atom0_
|
|
2937
|
+
%%TRUTH_ATOMS%%
|
|
2938
|
+
%%ATOM_EXTRA%%
|
|
2939
|
+
|
|
2940
|
+
?infix_predicate: term "<" term -> lt_
|
|
2941
|
+
| term ">" term -> gt_
|
|
2942
|
+
| term "=" term -> eq_
|
|
2943
|
+
| term "≤" term -> le_
|
|
2944
|
+
| term "≥" term -> ge_
|
|
2945
|
+
| term "≠" term -> ne_
|
|
2946
|
+
|
|
2947
|
+
?termlist: term ("," term)*
|
|
2948
|
+
|
|
2949
|
+
?term: sum
|
|
2950
|
+
|
|
2951
|
+
?sum: product
|
|
2952
|
+
| sum "+" product -> add_
|
|
2953
|
+
| sum "-" product -> sub_
|
|
2954
|
+
|
|
2955
|
+
?product: atom_term
|
|
2956
|
+
| product "*" atom_term -> mul_
|
|
2957
|
+
| product "/" atom_term -> div_
|
|
2958
|
+
|
|
2959
|
+
?atom_term: VARIABLE
|
|
2960
|
+
| NAME "(" termlist ")" -> function_
|
|
2961
|
+
%%CONST_ALTS%%
|
|
2962
|
+
| NUMBER -> number_
|
|
2963
|
+
%%TERM_EXTRA%%
|
|
2964
|
+
| "(" term ")"
|
|
2965
|
+
|
|
2966
|
+
lambda_: LAMBDA (VARIABLE | NAME | PREDICATE) "." formula
|
|
2967
|
+
?app_arg: formula | atom_term
|
|
2968
|
+
application_: "(" formula ")" "(" app_arg ")"
|
|
2969
|
+
|
|
2970
|
+
%ignore WS
|
|
2971
|
+
'''
|
|
2972
|
+
|
|
2973
|
+
|
|
2974
|
+
# The two constant-handling variants for the atom_term layer. Plain (FOL / FL /
|
|
2975
|
+
# modal / second-order) treats a bare NAME as a Constant, c_-constants via
|
|
2976
|
+
# const_, and a quoted name (QUOTED_NAME, whose token handler builds the
|
|
2977
|
+
# Constant) as the constant of exactly that name; sorted (MSFOL / MSFL) requires a
|
|
2978
|
+
# SORT annotation on each, so a quoted constant there is ``'k2':Mountain`` and
|
|
2979
|
+
# never bare. The head line ``?atom_term: VARIABLE`` stays where it is:
|
|
2980
|
+
# msflparser patches the assembled grammar by that exact line.
|
|
2981
|
+
_CONST_ALTS_PLAIN = (
|
|
2982
|
+
" | NAME\n"
|
|
2983
|
+
" | CONSTANT -> const_\n"
|
|
2984
|
+
" | QUOTED_NAME"
|
|
2985
|
+
)
|
|
2986
|
+
_CONST_ALTS_SORTED = (
|
|
2987
|
+
" | NAME SORT -> sorted_const_\n"
|
|
2988
|
+
" | CONSTANT SORT -> sorted_const_\n"
|
|
2989
|
+
" | QUOTED_NAME SORT -> sorted_const_"
|
|
2990
|
+
)
|
|
2991
|
+
|
|
2992
|
+
|
|
2993
|
+
# Per-mode grammar configuration that is NOT operator-specific: the terminal
|
|
2994
|
+
# import list and whether constants are sorted. (The operators themselves come
|
|
2995
|
+
# from the registry.)
|
|
2996
|
+
#
|
|
2997
|
+
# PREDICATE, CONSTANT, NAME, VARIABLE, and (for the sorted modes) SORT used to
|
|
2998
|
+
# be listed here for %import, like NUMBER/FORALL/EXISTS/LAMBDA still are —
|
|
2999
|
+
# they are generated instead now (see fol/_identifiers.py's module docstring
|
|
3000
|
+
# for why) and spliced into %%TERMINAL_DEFS%% by build_grammar below, so this
|
|
3001
|
+
# import list carries only the terminals that are still declared verbatim in
|
|
3002
|
+
# fol/grammars/terminals.lark.
|
|
3003
|
+
_MODE_TERMINAL_IMPORTS = {
|
|
3004
|
+
"fol": "NUMBER, FORALL, EXISTS, LAMBDA",
|
|
3005
|
+
"msfol": "NUMBER, FORALL, EXISTS, LAMBDA",
|
|
3006
|
+
"msfl": "NUMBER, FORALL, EXISTS, LAMBDA",
|
|
3007
|
+
"fl": "NUMBER, FORALL, EXISTS, LAMBDA",
|
|
3008
|
+
"modal": "NUMBER, FORALL, EXISTS, LAMBDA",
|
|
3009
|
+
"second_order": "NUMBER, FORALL, EXISTS, LAMBDA",
|
|
3010
|
+
"third_order": "NUMBER, FORALL, EXISTS, LAMBDA",
|
|
3011
|
+
"third_order_modal": "NUMBER, FORALL, EXISTS, LAMBDA",
|
|
3012
|
+
"dependence": "NUMBER, FORALL, EXISTS, LAMBDA",
|
|
3013
|
+
"linear": "NUMBER, FORALL, EXISTS, LAMBDA",
|
|
3014
|
+
"lambek": "NUMBER, FORALL, EXISTS, LAMBDA",
|
|
3015
|
+
}
|
|
3016
|
+
|
|
3017
|
+
_SORTED_MODES = frozenset({"msfol", "msfl"})
|
|
3018
|
+
|
|
3019
|
+
|
|
3020
|
+
# Per-mode atom_term extensions that are NOT registry-driven (the term layer is the
|
|
3021
|
+
# one hand-written part of the template). The classical unsorted modes (fol, modal,
|
|
3022
|
+
# second_order) gain the measure term μ(entity, dimension) and the set-cardinality
|
|
3023
|
+
# term |{v : φ}|; the matching FOLTransformer.measure_ / .cardinality_ handlers
|
|
3024
|
+
# (base-class methods, so available in every mode) turn them into Measure /
|
|
3025
|
+
# Cardinality nodes. All three share the plain (unsorted) term layer, so the same
|
|
3026
|
+
# fragment applies verbatim; modal / second-order are included so a measure or
|
|
3027
|
+
# cardinality term can appear under their operators (e.g. ◇(μ(x, height) > μ(y,
|
|
3028
|
+
# height)) or ∃P (|{v : P(v)}| > c)). The SORTED modes msfol/msfl are absent because
|
|
3029
|
+
# the |{v : φ}| binder would need a sort annotation; a mode absent from this map
|
|
3030
|
+
# gets no extra term form.
|
|
3031
|
+
_TERM_EXTRA_CLASSICAL = (
|
|
3032
|
+
' | "μ" "(" termlist ")" -> measure_\n'
|
|
3033
|
+
' | "|" "{" VARIABLE ":" formula "}" "|" -> cardinality_'
|
|
3034
|
+
)
|
|
3035
|
+
# Sorted variant (MSFOL): the measure term is unchanged (its args are the mode's
|
|
3036
|
+
# termlist), but the cardinality binder carries a sort annotation on the bound
|
|
3037
|
+
# variable — |{v:Sort : φ}| → SortedCardinality — to stay consistent with MSFOL's
|
|
3038
|
+
# rule that every binder is sorted.
|
|
3039
|
+
_TERM_EXTRA_SORTED = (
|
|
3040
|
+
' | "μ" "(" termlist ")" -> measure_\n'
|
|
3041
|
+
' | "|" "{" VARIABLE SORT ":" formula "}" "|" -> sorted_cardinality_'
|
|
3042
|
+
)
|
|
3043
|
+
_MODE_TERM_EXTRA = {
|
|
3044
|
+
"fol": _TERM_EXTRA_CLASSICAL,
|
|
3045
|
+
"modal": _TERM_EXTRA_CLASSICAL,
|
|
3046
|
+
"second_order": _TERM_EXTRA_CLASSICAL,
|
|
3047
|
+
"third_order": _TERM_EXTRA_CLASSICAL,
|
|
3048
|
+
"third_order_modal": _TERM_EXTRA_CLASSICAL,
|
|
3049
|
+
"msfol": _TERM_EXTRA_SORTED,
|
|
3050
|
+
}
|
|
3051
|
+
|
|
3052
|
+
|
|
3053
|
+
# Per-mode ARGUMENT layer for a predicate application. Every mode but the
|
|
3054
|
+
# third-order ones takes an ordinary ``termlist``: a predicate's arguments
|
|
3055
|
+
# are INDIVIDUALS, so ``P(x)`` is the whole story and a predicate name in
|
|
3056
|
+
# argument position is a syntax error — which is exactly right for first-
|
|
3057
|
+
# and second-order syntax, where ``∀P φ`` binds P as the HEAD of an
|
|
3058
|
+
# application and never as an argument of one.
|
|
3059
|
+
#
|
|
3060
|
+
# The third-order modes widen that one position, and only that one: an
|
|
3061
|
+
# argument may also be a PREDICATE name or a λ-abstraction, i.e. a PROPERTY.
|
|
3062
|
+
# That is the whole syntactic content of "third order" — a predicate whose
|
|
3063
|
+
# argument is itself a predicate (``Positive(G)``, ``Essence(G, x)``,
|
|
3064
|
+
# ``Positive(λx. ¬G(x))``) — and it is why the level is not reachable by
|
|
3065
|
+
# adding another quantifier to second-order syntax. The matching handlers are
|
|
3066
|
+
# ``FOLTransformer.hoarglist`` (returns the argument list, exactly like
|
|
3067
|
+
# ``termlist``) and ``LambdaTransformer.pred_arg_`` (builds the PredicateTerm;
|
|
3068
|
+
# that one lives in msflparser.py because PredicateTerm is defined downstream
|
|
3069
|
+
# of this module).
|
|
3070
|
+
_MODE_ATOM_ARGS = {
|
|
3071
|
+
"third_order": "hoarglist",
|
|
3072
|
+
"third_order_modal": "hoarglist",
|
|
3073
|
+
}
|
|
3074
|
+
_ATOM_EXTRA_THIRD_ORDER = (
|
|
3075
|
+
'hoarglist: hoarg ("," hoarg)*\n'
|
|
3076
|
+
'\n'
|
|
3077
|
+
'?hoarg: term\n'
|
|
3078
|
+
' | PREDICATE -> pred_arg_\n'
|
|
3079
|
+
' | lambda_'
|
|
3080
|
+
)
|
|
3081
|
+
_MODE_ATOM_EXTRA = {
|
|
3082
|
+
"third_order": _ATOM_EXTRA_THIRD_ORDER,
|
|
3083
|
+
"third_order_modal": _ATOM_EXTRA_THIRD_ORDER,
|
|
3084
|
+
}
|
|
3085
|
+
|
|
3086
|
+
|
|
3087
|
+
# The two truth constants as atoms: ``⊤`` is the nullary atom ``$true`` and ``⊥`` the
|
|
3088
|
+
# nullary atom ``$false`` (the atoms the TPTP reader builds), which is also what
|
|
3089
|
+
# ``Node.to_unicode_str`` prints them as, so a formula that contains one reads back to
|
|
3090
|
+
# itself. Every mode that has propositional atoms reads them. The two modes that do
|
|
3091
|
+
# not are left alone: ``linear`` already gives the glyph ``⊤`` a meaning of its own
|
|
3092
|
+
# (the additive unit of ``&``, a ``Top`` node, registered in ``_linear_nodes``) and
|
|
3093
|
+
# ``lambek`` is a calculus of category types, with no propositional constants.
|
|
3094
|
+
_TRUTH_ATOM_ALTS = (
|
|
3095
|
+
' | "⊤" -> true_\n'
|
|
3096
|
+
' | "⊥" -> false_\n'
|
|
3097
|
+
)
|
|
3098
|
+
_NO_TRUTH_ATOM_MODES = frozenset({"linear", "lambek"})
|
|
3099
|
+
|
|
3100
|
+
|
|
3101
|
+
def build_grammar(mode: str) -> str:
|
|
3102
|
+
"""Assemble the Lark grammar STRING for ``mode`` from the registry + template.
|
|
3103
|
+
|
|
3104
|
+
Splices the mode's ParserOps into the base template's markers, preserving the
|
|
3105
|
+
exact precedence structure of the legacy hand-written grammar for that mode.
|
|
3106
|
+
Pure string assembly: no Lark object is built here (MSFLParser does that).
|
|
3107
|
+
"""
|
|
3108
|
+
# Handler-only ops (empty grammar, e.g. sorted_const_ whose alternative lives
|
|
3109
|
+
# in the template's CONST_ALTS block) contribute a transform but no grammar
|
|
3110
|
+
# alternative, so they are excluded from every grammar-fragment join below.
|
|
3111
|
+
ops = [op for op in parser_ops_for_mode(mode) if op.grammar]
|
|
3112
|
+
|
|
3113
|
+
# --- named-terminal declarations (modal operators; dedup, preserve order) ---
|
|
3114
|
+
seen_terms = set()
|
|
3115
|
+
term_defs = []
|
|
3116
|
+
for op in ops:
|
|
3117
|
+
if op.terminal_def and op.terminal_name not in seen_terms:
|
|
3118
|
+
seen_terms.add(op.terminal_name)
|
|
3119
|
+
term_defs.append(op.terminal_def)
|
|
3120
|
+
# The generated identifier terminals (PREDICATE, CONSTANT, NAME, VARIABLE,
|
|
3121
|
+
# and SORT for the sorted modes) go first, ahead of any modal/temporal
|
|
3122
|
+
# operator terminal — see fol/_identifiers.py's module docstring for why
|
|
3123
|
+
# they are generated rather than %import'd from terminals.lark.
|
|
3124
|
+
identifier_defs = _identifiers.terminal_block(include_sort=mode in _SORTED_MODES)
|
|
3125
|
+
terminal_defs = identifier_defs + (("\n".join(term_defs) + "\n") if term_defs else "")
|
|
3126
|
+
|
|
3127
|
+
# --- until sub-level (modal only) ----------------------------------------
|
|
3128
|
+
# Determined first because it sets the implication body rule. ``op.grammar``
|
|
3129
|
+
# for an until op is just the operator glyph (literal or named terminal).
|
|
3130
|
+
until = [op for op in ops if op.level == "until"]
|
|
3131
|
+
if until:
|
|
3132
|
+
impl_body = "until"
|
|
3133
|
+
until_alts = "\n".join(
|
|
3134
|
+
f" | same_level_ops {op.grammar} until -> {op.rule_alias}"
|
|
3135
|
+
for op in until
|
|
3136
|
+
)
|
|
3137
|
+
until_block = f"\n?until: same_level_ops\n{until_alts}\n"
|
|
3138
|
+
else:
|
|
3139
|
+
impl_body = "same_level_ops"
|
|
3140
|
+
until_block = ""
|
|
3141
|
+
|
|
3142
|
+
# --- biimplication (↔) — right-assoc; ``op.grammar`` is just the glyph ----
|
|
3143
|
+
biimpl = [op for op in ops if op.level == "biimplication"]
|
|
3144
|
+
biimpl_ops = "\n".join(
|
|
3145
|
+
f" | implication {op.grammar} biimplication -> {op.rule_alias}"
|
|
3146
|
+
for op in biimpl
|
|
3147
|
+
)
|
|
3148
|
+
|
|
3149
|
+
# --- implication (→) — right-assoc; left operand is the implication body --
|
|
3150
|
+
impl = [op for op in ops if op.level == "implication"]
|
|
3151
|
+
impl_ops = "\n".join(
|
|
3152
|
+
f" | {impl_body} {op.grammar} implication -> {op.rule_alias}"
|
|
3153
|
+
for op in impl
|
|
3154
|
+
)
|
|
3155
|
+
|
|
3156
|
+
# --- level2 (the no-mixing same_level_ops group) -------------------------
|
|
3157
|
+
level2 = [op for op in ops if op.level == "level2"]
|
|
3158
|
+
only_members = " | ".join(op.only_name for op in level2)
|
|
3159
|
+
level2_alts = f"{only_members} | prefix" if only_members else "prefix"
|
|
3160
|
+
only_rules = "\n".join(
|
|
3161
|
+
f"?{op.only_name}: prefix ({op.grammar} prefix)+ -> {op.rule_alias}"
|
|
3162
|
+
for op in level2
|
|
3163
|
+
)
|
|
3164
|
+
|
|
3165
|
+
# --- prefix level (¬ and any prefix modal/temporal ops) ------------------
|
|
3166
|
+
prefix = [op for op in ops if op.level == "prefix"]
|
|
3167
|
+
prefix_ops = "\n | ".join(f"{op.grammar} -> {op.rule_alias}" for op in prefix)
|
|
3168
|
+
|
|
3169
|
+
# --- quantifier ----------------------------------------------------------
|
|
3170
|
+
quant = [op for op in ops if op.level == "quantifier"]
|
|
3171
|
+
quant_ops = "\n | ".join(f"{op.grammar} -> {op.rule_alias}" for op in quant)
|
|
3172
|
+
|
|
3173
|
+
# --- term-layer constant handling ----------------------------------------
|
|
3174
|
+
const_alts = _CONST_ALTS_SORTED if mode in _SORTED_MODES else _CONST_ALTS_PLAIN
|
|
3175
|
+
|
|
3176
|
+
# --- term-layer extensions (measure / cardinality; non-registry) ---------
|
|
3177
|
+
term_extra = _MODE_TERM_EXTRA.get(mode, "")
|
|
3178
|
+
|
|
3179
|
+
grammar = _BASE_GRAMMAR_TEMPLATE
|
|
3180
|
+
grammar = grammar.replace("%%TERMINAL_IMPORTS%%", _MODE_TERMINAL_IMPORTS[mode])
|
|
3181
|
+
grammar = grammar.replace("%%TERMINAL_DEFS%%\n", terminal_defs)
|
|
3182
|
+
grammar = grammar.replace("%%BIIMPL_OPS%%", biimpl_ops)
|
|
3183
|
+
grammar = grammar.replace("%%IMPL_BODY%%", impl_body)
|
|
3184
|
+
grammar = grammar.replace("%%IMPL_OPS%%", impl_ops)
|
|
3185
|
+
grammar = grammar.replace("%%UNTIL_BLOCK%%\n", until_block)
|
|
3186
|
+
grammar = grammar.replace("%%LEVEL2_ALTS%%", level2_alts)
|
|
3187
|
+
grammar = grammar.replace("%%ONLY_RULES%%\n", (only_rules + "\n") if only_rules else "")
|
|
3188
|
+
# A mode may register NO quantifier ops (linear, lambek — propositional) or
|
|
3189
|
+
# NO prefix ops. Lark rejects a rule with an empty right-hand side, so the
|
|
3190
|
+
# empty level is excised from the template rather than left dangling: the
|
|
3191
|
+
# `| quantifier` alternative and the ?quantifier rule disappear together,
|
|
3192
|
+
# and an empty prefix level promotes the next alternative into first place.
|
|
3193
|
+
if not quant:
|
|
3194
|
+
grammar = grammar.replace("\n | quantifier", "")
|
|
3195
|
+
grammar = grammar.replace("\n?quantifier: %%QUANT_OPS%%\n", "\n")
|
|
3196
|
+
if prefix_ops:
|
|
3197
|
+
grammar = grammar.replace("%%PREFIX_OPS%%", prefix_ops)
|
|
3198
|
+
else:
|
|
3199
|
+
grammar = grammar.replace("%%PREFIX_OPS%%\n | ", "")
|
|
3200
|
+
grammar = grammar.replace("%%QUANT_OPS%%", quant_ops)
|
|
3201
|
+
grammar = grammar.replace("%%CONST_ALTS%%", const_alts)
|
|
3202
|
+
grammar = grammar.replace("%%TERM_EXTRA%%\n", (term_extra + "\n") if term_extra else "")
|
|
3203
|
+
# The predicate-application argument layer: ``termlist`` (individuals
|
|
3204
|
+
# only) for every mode but the third-order ones, which widen it to
|
|
3205
|
+
# ``hoarglist`` and bring the two extra rules along with it.
|
|
3206
|
+
grammar = grammar.replace("%%ATOM_ARGS%%", _MODE_ATOM_ARGS.get(mode, "termlist"))
|
|
3207
|
+
grammar = grammar.replace(
|
|
3208
|
+
"%%TRUTH_ATOMS%%\n", "" if mode in _NO_TRUTH_ATOM_MODES else _TRUTH_ATOM_ALTS)
|
|
3209
|
+
atom_extra = _MODE_ATOM_EXTRA.get(mode, "")
|
|
3210
|
+
grammar = grammar.replace("%%ATOM_EXTRA%%\n", (atom_extra + "\n") if atom_extra else "")
|
|
3211
|
+
return grammar
|
|
3212
|
+
|
|
3213
|
+
|
|
3214
|
+
def build_transform_handlers(mode: str) -> Dict[str, object]:
|
|
3215
|
+
"""Return ``{rule_alias: transform}`` for every ParserOp in ``mode``.
|
|
3216
|
+
|
|
3217
|
+
MSFLParser attaches these to the assembled Transformer so each operator's
|
|
3218
|
+
parse handler lives next to its node definition, not in a hand-written
|
|
3219
|
+
Transformer subclass.
|
|
3220
|
+
"""
|
|
3221
|
+
return {op.rule_alias: op.transform for op in parser_ops_for_mode(mode)}
|
|
3222
|
+
|
|
3223
|
+
|
|
3224
|
+
# =========================
|
|
3225
|
+
# Transformer
|
|
3226
|
+
# =========================
|
|
3227
|
+
|
|
3228
|
+
class FOLTransformer(Transformer):
|
|
3229
|
+
"""Transforms parsed tokens from Lark parser into AST nodes."""
|
|
3230
|
+
|
|
3231
|
+
@staticmethod
|
|
3232
|
+
def _fold_binary(items, node_cls):
|
|
3233
|
+
"""Left-fold a variable-length item list into nested binary nodes."""
|
|
3234
|
+
node = items[0]
|
|
3235
|
+
for item in items[1:]:
|
|
3236
|
+
node = node_cls(node, item)
|
|
3237
|
+
return node
|
|
3238
|
+
|
|
3239
|
+
def atom0_(self, items):
|
|
3240
|
+
"""Transform bare predicate symbol into a zero-arity Atom node."""
|
|
3241
|
+
pred = str(items[0])
|
|
3242
|
+
return Atom(pred, [])
|
|
3243
|
+
|
|
3244
|
+
def true_(self, items):
|
|
3245
|
+
"""Transform the glyph ``⊤`` into the truth constant, the atom ``$true``."""
|
|
3246
|
+
return Atom("$true", [])
|
|
3247
|
+
|
|
3248
|
+
def false_(self, items):
|
|
3249
|
+
"""Transform the glyph ``⊥`` into the falsity constant, the atom ``$false``."""
|
|
3250
|
+
return Atom("$false", [])
|
|
3251
|
+
|
|
3252
|
+
def VARIABLE(self, items):
|
|
3253
|
+
"""Transform variable token into Variable node."""
|
|
3254
|
+
return Variable(str(items))
|
|
3255
|
+
|
|
3256
|
+
def NAME(self, items):
|
|
3257
|
+
"""Transform name token into Constant node."""
|
|
3258
|
+
return Constant(str(items))
|
|
3259
|
+
|
|
3260
|
+
def const_(self, items):
|
|
3261
|
+
"""Transform a ``c_``-prefixed constant token into a Constant node."""
|
|
3262
|
+
return Constant(str(items[0]))
|
|
3263
|
+
|
|
3264
|
+
def QUOTED_NAME(self, items):
|
|
3265
|
+
"""Transform a quoted name token (``'k2'``) into the Constant of that name.
|
|
3266
|
+
|
|
3267
|
+
A token handler, like :meth:`NAME`, so the source span of the constant
|
|
3268
|
+
is the token's own (quotes included) and ``_sorted_const_transform``
|
|
3269
|
+
takes the name of the Constant here exactly as it does for a NAME.
|
|
3270
|
+
"""
|
|
3271
|
+
return Constant(_identifiers._unquote_constant(str(items)))
|
|
3272
|
+
|
|
3273
|
+
def number_(self, items):
|
|
3274
|
+
"""Transform numeric literal token into Number node."""
|
|
3275
|
+
try:
|
|
3276
|
+
return Number(_numeral_from_text(str(items[0])))
|
|
3277
|
+
except ValueError as exc:
|
|
3278
|
+
raise NumeralTextError(str(exc)) from None
|
|
3279
|
+
|
|
3280
|
+
def function_(self, items):
|
|
3281
|
+
"""Transform function application into Function node."""
|
|
3282
|
+
head = items[0]
|
|
3283
|
+
name = head.name if isinstance(head, Constant) else str(head)
|
|
3284
|
+
args = items[1:]
|
|
3285
|
+
if args and isinstance(args[0], list):
|
|
3286
|
+
args = args[0]
|
|
3287
|
+
return Function(name, args)
|
|
3288
|
+
|
|
3289
|
+
def add_(self, items):
|
|
3290
|
+
"""Transform addition into Function node."""
|
|
3291
|
+
left, right = items
|
|
3292
|
+
return Function("+", [left, right])
|
|
3293
|
+
|
|
3294
|
+
def sub_(self, items):
|
|
3295
|
+
"""Transform subtraction into Function node."""
|
|
3296
|
+
left, right = items
|
|
3297
|
+
return Function("-", [left, right])
|
|
3298
|
+
|
|
3299
|
+
def mul_(self, items):
|
|
3300
|
+
"""Transform multiplication into Function node."""
|
|
3301
|
+
left, right = items
|
|
3302
|
+
return Function("*", [left, right])
|
|
3303
|
+
|
|
3304
|
+
def div_(self, items):
|
|
3305
|
+
"""Transform division into Function node."""
|
|
3306
|
+
left, right = items
|
|
3307
|
+
return Function("/", [left, right])
|
|
3308
|
+
|
|
3309
|
+
def atom_term(self, items):
|
|
3310
|
+
"""Pass through atom term."""
|
|
3311
|
+
return items[0]
|
|
3312
|
+
|
|
3313
|
+
def term(self, items):
|
|
3314
|
+
"""Pass through term."""
|
|
3315
|
+
return items[0]
|
|
3316
|
+
|
|
3317
|
+
def sum(self, items):
|
|
3318
|
+
"""Pass through sum expression."""
|
|
3319
|
+
return items[0]
|
|
3320
|
+
|
|
3321
|
+
def product(self, items):
|
|
3322
|
+
"""Pass through product expression."""
|
|
3323
|
+
return items[0]
|
|
3324
|
+
|
|
3325
|
+
def termlist(self, items):
|
|
3326
|
+
"""Transform term list."""
|
|
3327
|
+
return items
|
|
3328
|
+
|
|
3329
|
+
def hoarglist(self, items):
|
|
3330
|
+
"""Transform a third-order argument list (individuals and/or properties).
|
|
3331
|
+
|
|
3332
|
+
The third-order modes' counterpart to :meth:`termlist`: same contract
|
|
3333
|
+
(return the argument list for ``atom_`` to consume), but an entry may
|
|
3334
|
+
be a PredicateTerm or a Lambda as well as an ordinary term. Unlike
|
|
3335
|
+
``?termlist`` this rule is NOT inlined by lark, so a one-argument
|
|
3336
|
+
application arrives here as a one-element list rather than as a bare
|
|
3337
|
+
node — ``atom_`` accepts either.
|
|
3338
|
+
"""
|
|
3339
|
+
return items
|
|
3340
|
+
|
|
3341
|
+
def infix_predicate(self, items):
|
|
3342
|
+
"""Pass through infix predicate."""
|
|
3343
|
+
return items[0]
|
|
3344
|
+
|
|
3345
|
+
def atom(self, items):
|
|
3346
|
+
"""Pass through atom."""
|
|
3347
|
+
return items[0]
|
|
3348
|
+
|
|
3349
|
+
def atom_(self, items):
|
|
3350
|
+
"""Transform predicate application into Atom node."""
|
|
3351
|
+
pred = str(items[0])
|
|
3352
|
+
if not isinstance(items[1], list):
|
|
3353
|
+
args = [items[1]]
|
|
3354
|
+
else:
|
|
3355
|
+
args = items[1]
|
|
3356
|
+
return Atom(pred, args)
|
|
3357
|
+
|
|
3358
|
+
def lt_(self, items):
|
|
3359
|
+
"""Transform less-than comparison into Atom node."""
|
|
3360
|
+
left, right = items
|
|
3361
|
+
return Atom("<", [left, right])
|
|
3362
|
+
|
|
3363
|
+
def gt_(self, items):
|
|
3364
|
+
"""Transform greater-than comparison into Atom node."""
|
|
3365
|
+
left, right = items
|
|
3366
|
+
return Atom(">", [left, right])
|
|
3367
|
+
|
|
3368
|
+
def eq_(self, items):
|
|
3369
|
+
"""Transform equality comparison into Atom node."""
|
|
3370
|
+
left, right = items
|
|
3371
|
+
return Atom("=", [left, right])
|
|
3372
|
+
|
|
3373
|
+
def le_(self, items):
|
|
3374
|
+
"""Transform less-than-or-equal comparison into Atom node."""
|
|
3375
|
+
left, right = items
|
|
3376
|
+
return Atom("≤", [left, right])
|
|
3377
|
+
|
|
3378
|
+
def ge_(self, items):
|
|
3379
|
+
"""Transform greater-than-or-equal comparison into Atom node."""
|
|
3380
|
+
left, right = items
|
|
3381
|
+
return Atom("≥", [left, right])
|
|
3382
|
+
|
|
3383
|
+
def ne_(self, items):
|
|
3384
|
+
"""Transform not-equal comparison into Atom node."""
|
|
3385
|
+
left, right = items
|
|
3386
|
+
return Atom("≠", [left, right])
|
|
3387
|
+
|
|
3388
|
+
def not_(self, items):
|
|
3389
|
+
"""Transform negation into Not node."""
|
|
3390
|
+
return Not(items[0])
|
|
3391
|
+
|
|
3392
|
+
def and_(self, items):
|
|
3393
|
+
"""Transform conjunction into And node."""
|
|
3394
|
+
return self._fold_binary(items, And)
|
|
3395
|
+
|
|
3396
|
+
def or_(self, items):
|
|
3397
|
+
"""Transform disjunction into Or node."""
|
|
3398
|
+
return self._fold_binary(items, Or)
|
|
3399
|
+
|
|
3400
|
+
def xor_(self, items):
|
|
3401
|
+
"""Transform exclusive or into Xor node."""
|
|
3402
|
+
return self._fold_binary(items, Xor)
|
|
3403
|
+
|
|
3404
|
+
def implies_(self, items):
|
|
3405
|
+
"""Transform implication into Implies node."""
|
|
3406
|
+
return Implies(items[0], items[1])
|
|
3407
|
+
|
|
3408
|
+
def iff_(self, items):
|
|
3409
|
+
"""Transform biconditional into Iff node."""
|
|
3410
|
+
return Iff(items[0], items[1])
|
|
3411
|
+
|
|
3412
|
+
def quantifier_(self, items):
|
|
3413
|
+
"""Transform quantifier expression into Quantifier node."""
|
|
3414
|
+
quant = items[0]
|
|
3415
|
+
var = items[1]
|
|
3416
|
+
formula = items[2]
|
|
3417
|
+
return Quantifier(str(quant), var, formula)
|
|
3418
|
+
|
|
3419
|
+
def measure_(self, items):
|
|
3420
|
+
"""Transform μ(entity, dimension) into a Measure term node (exactly 2 args)."""
|
|
3421
|
+
args = items[0] if items and isinstance(items[0], list) else list(items)
|
|
3422
|
+
if len(args) != 2:
|
|
3423
|
+
raise ValueError(
|
|
3424
|
+
f"μ(...) takes exactly two arguments (entity, dimension); got {len(args)}.")
|
|
3425
|
+
return Measure(args[0], args[1])
|
|
3426
|
+
|
|
3427
|
+
def cardinality_(self, items):
|
|
3428
|
+
"""Transform |{v : φ}| into a Cardinality term node binding v over φ."""
|
|
3429
|
+
return Cardinality(items[0], items[1])
|
|
3430
|
+
|
|
3431
|
+
|
|
3432
|
+
# =========================
|
|
3433
|
+
# Parser registration (FOL / MSFOL connectives + quantifier)
|
|
3434
|
+
# =========================
|
|
3435
|
+
#
|
|
3436
|
+
# Self-register the classical connectives and the unsorted quantifier with the
|
|
3437
|
+
# parser registry. Each transform mirrors the corresponding FOLTransformer method
|
|
3438
|
+
# exactly (same items[…] handling, same node), so the assembled parser produces
|
|
3439
|
+
# byte-identical ASTs. The connectives shared by FOL and MSFOL (∧ ∨ ¬ → ↔) and
|
|
3440
|
+
# the quantifier register once per mode they appear in; FOL additionally has ⊕
|
|
3441
|
+
# (Xor). The sorted quantifier and the Łukasiewicz/MSFL bindings live in
|
|
3442
|
+
# _msfl_nodes.py; the modal/second-order bindings in their own modules.
|
|
3443
|
+
|
|
3444
|
+
def _fold_binary(items, node_cls):
|
|
3445
|
+
"""Left-fold a variable-length item list into nested binary nodes (registry copy)."""
|
|
3446
|
+
node = items[0]
|
|
3447
|
+
for item in items[1:]:
|
|
3448
|
+
node = node_cls(node, item)
|
|
3449
|
+
return node
|
|
3450
|
+
|
|
3451
|
+
|
|
3452
|
+
# Classical ∧ ∨ ¬ → ↔ are shared by FOL, MSFOL, modal, and second-order modes;
|
|
3453
|
+
# ⊕ (Xor) by every CLASSICAL mode — FOL, MSFOL, modal, and second-order (the glyph ⊕
|
|
3454
|
+
# is the Łukasiewicz strong disjunction in the fuzzy modes, so Xor stays out of those).
|
|
3455
|
+
# The unsorted quantifier is shared by FOL, modal, and second-order (the sorted modes
|
|
3456
|
+
# use SortedQuantifier). Each connective registers once per mode with the SAME grammar
|
|
3457
|
+
# fragment and transform, so the assembled parser produces byte-identical ASTs.
|
|
3458
|
+
_CLASSICAL_MODES = ("fol", "msfol", "modal", "second_order")
|
|
3459
|
+
_XOR_MODES = ("fol", "msfol", "modal", "second_order")
|
|
3460
|
+
_UNSORTED_QUANT_MODES = ("fol", "modal", "second_order")
|
|
3461
|
+
|
|
3462
|
+
|
|
3463
|
+
def _quantifier_transform(items):
|
|
3464
|
+
"""Build an unsorted Quantifier from [FORALL/EXISTS token, Variable, body]."""
|
|
3465
|
+
return Quantifier(str(items[0]), items[1], items[2])
|
|
3466
|
+
|
|
3467
|
+
|
|
3468
|
+
# --- prefix: ¬ (Not) ---
|
|
3469
|
+
for _m in _CLASSICAL_MODES:
|
|
3470
|
+
register_parser_op(Not, _m, "prefix", "not_", '"¬" prefix',
|
|
3471
|
+
lambda items: Not(items[0]))
|
|
3472
|
+
|
|
3473
|
+
# --- level2: ∧ ∨ (And, Or) everywhere classical; ⊕ (Xor) where allowed ---
|
|
3474
|
+
for _m in _CLASSICAL_MODES:
|
|
3475
|
+
register_parser_op(And, _m, "level2", "and_", '"∧"',
|
|
3476
|
+
lambda items: _fold_binary(items, And), only_name="only_and")
|
|
3477
|
+
register_parser_op(Or, _m, "level2", "or_", '"∨"',
|
|
3478
|
+
lambda items: _fold_binary(items, Or), only_name="only_or")
|
|
3479
|
+
for _m in _XOR_MODES:
|
|
3480
|
+
register_parser_op(Xor, _m, "level2", "xor_", '"⊕"',
|
|
3481
|
+
lambda items: _fold_binary(items, Xor), only_name="only_xor")
|
|
3482
|
+
|
|
3483
|
+
# --- implication: → (Implies) ---
|
|
3484
|
+
# For binary levels (implication / biimplication / until) the ``grammar`` field
|
|
3485
|
+
# holds JUST the operator glyph; build_grammar assembles the full right-assoc
|
|
3486
|
+
# rule from it (the operand rule names are fixed by the level structure). This
|
|
3487
|
+
# lets the → rule's left operand follow the mode's implication body (same_level_ops
|
|
3488
|
+
# normally, or until in modal mode) without a mode-specific fragment.
|
|
3489
|
+
for _m in _CLASSICAL_MODES:
|
|
3490
|
+
register_parser_op(Implies, _m, "implication", "implies_", '"→"',
|
|
3491
|
+
lambda items: Implies(items[0], items[1]))
|
|
3492
|
+
|
|
3493
|
+
# --- biimplication: ↔ (Iff) ---
|
|
3494
|
+
for _m in _CLASSICAL_MODES:
|
|
3495
|
+
register_parser_op(Iff, _m, "biimplication", "iff_", '"↔"',
|
|
3496
|
+
lambda items: Iff(items[0], items[1]))
|
|
3497
|
+
|
|
3498
|
+
# --- quantifier: unsorted ∀x / ∃x (Quantifier) ---
|
|
3499
|
+
for _m in _UNSORTED_QUANT_MODES:
|
|
3500
|
+
register_parser_op(Quantifier, _m, "quantifier", "quantifier_",
|
|
3501
|
+
"(FORALL | EXISTS) VARIABLE prefix", _quantifier_transform)
|
|
3502
|
+
|
|
3503
|
+
|
|
3504
|
+
# Modes that accept the NL / CCG translation-target nodes (Count, Contrast, and —
|
|
3505
|
+
# via _MODE_TERM_EXTRA below — the Measure / Cardinality terms). These are the
|
|
3506
|
+
# CLASSICAL, UNSORTED modes — every mode that is a conservative extension of
|
|
3507
|
+
# classical unsorted FOL and therefore reads the constructs with IDENTICAL
|
|
3508
|
+
# semantics: plain fol, modal (fol + modal operators), and second-order (fol +
|
|
3509
|
+
# quantifiers over predicate variables). A CCG-derived form routinely nests one of
|
|
3510
|
+
# these fol-level constructs under a modal or second-order operator — e.g. "every
|
|
3511
|
+
# professor believes at least three students will pass" is Believes_x(∃≥3 y …) —
|
|
3512
|
+
# so registering the same grammar fragment + transform across the family lets the
|
|
3513
|
+
# whole mixed formula parse (and round-trip) as a single string, not only as a
|
|
3514
|
+
# hand-built AST. (The SORTED classical modes msfol/msfl need sort-annotated
|
|
3515
|
+
# binders, and the fuzzy modes fl/msfl reinterpret the connectives and reject
|
|
3516
|
+
# comparison atoms, so neither is included here.)
|
|
3517
|
+
_NL_NODE_MODES = ("fol", "modal", "second_order")
|
|
3518
|
+
|
|
3519
|
+
|
|
3520
|
+
# --- counting quantifier: ∃≥n / ∃≤n / ∃=n (Count), fol + modal modes ---
|
|
3521
|
+
# COUNTOP is one named terminal matching all three glyphs (∃ followed by ≥/≤/=),
|
|
3522
|
+
# at lexer priority 5 so it wins over EXISTS (∃) on the longer match; the matched
|
|
3523
|
+
# glyph in items[0] selects the op code. The bound NUMBER must be a non-negative
|
|
3524
|
+
# integer: the terminal reads a sign and a decimal point because TERMS need them
|
|
3525
|
+
# (``P(-3)``, ``x < 2.5``), so ``∃≥-2`` and ``∃≥2.5`` reach this rule and are
|
|
3526
|
+
# refused here, as a parse error (see CountBoundError).
|
|
3527
|
+
class CountBoundError(ParsingError):
|
|
3528
|
+
"""A counting quantifier whose bound is not a non-negative integer.
|
|
3529
|
+
|
|
3530
|
+
Subclasses ParsingError, so the CLI, ``api.parse_any`` and every caller that
|
|
3531
|
+
catches the parser's error type report it as the one-line SYNTAX_ERROR it is
|
|
3532
|
+
(the parser re-raises a ParsingError a transformer handler produced instead
|
|
3533
|
+
of lark's opaque VisitError, as it does for ConflictingArityError). It is
|
|
3534
|
+
constructed directly, not from a Lark exception, so it sets its own message.
|
|
3535
|
+
"""
|
|
3536
|
+
|
|
3537
|
+
def __init__(self, glyph: str, bound: str, column=None):
|
|
3538
|
+
where = f" at position {column}" if isinstance(column, int) and column >= 0 else ""
|
|
3539
|
+
message = (
|
|
3540
|
+
f"SYNTAX_ERROR: the bound of a counting quantifier must be a "
|
|
3541
|
+
f"non-negative integer, got {bound!r}{where} after {glyph!r}. A count "
|
|
3542
|
+
f"is a whole number of witnesses: write it without a sign or a "
|
|
3543
|
+
f"decimal point, e.g. {glyph}2.")
|
|
3544
|
+
self.args = (message,)
|
|
3545
|
+
|
|
3546
|
+
def __str__(self):
|
|
3547
|
+
return self.args[0]
|
|
3548
|
+
|
|
3549
|
+
|
|
3550
|
+
def _count_bound(glyph_token, number_token) -> int:
|
|
3551
|
+
"""The integer a counting quantifier's bound token stands for.
|
|
3552
|
+
|
|
3553
|
+
A bound is an unsigned numeral: a sign makes it a CountBoundError whatever
|
|
3554
|
+
the number is (``-2``, and also ``-0``, which equals 0 as a number but is not
|
|
3555
|
+
the numeral ``0``), and so does a decimal point (``2.5``, and also ``2.0``).
|
|
3556
|
+
"""
|
|
3557
|
+
text = str(number_token)
|
|
3558
|
+
if "." not in text and not text.startswith(("-", "+")):
|
|
3559
|
+
return int(text)
|
|
3560
|
+
raise CountBoundError(str(glyph_token), text, getattr(number_token, "column", None))
|
|
3561
|
+
|
|
3562
|
+
|
|
3563
|
+
def _count_transform(items):
|
|
3564
|
+
"""Build a Count from [COUNTOP glyph token, NUMBER token, Variable, body]."""
|
|
3565
|
+
op = _COUNT_TOKEN_TO_OP[str(items[0])]
|
|
3566
|
+
return Count(op, Number(_count_bound(items[0], items[1])), items[2], items[3])
|
|
3567
|
+
|
|
3568
|
+
|
|
3569
|
+
for _m in _NL_NODE_MODES:
|
|
3570
|
+
register_parser_op(Count, _m, "quantifier", "count_",
|
|
3571
|
+
"COUNTOP NUMBER VARIABLE prefix", _count_transform,
|
|
3572
|
+
terminal_name="COUNTOP", terminal_def="COUNTOP.5: /∃[≥≤=]/")
|
|
3573
|
+
|
|
3574
|
+
|
|
3575
|
+
# --- concessive connective: P Ⓒ Q (Contrast) — every CLASSICAL mode ---
|
|
3576
|
+
# A regular level2 operator (same precedence as ∧ ∨ ⊕): self-registers with the
|
|
3577
|
+
# renderers and the parser, so no renderer branch is needed (it dispatches on
|
|
3578
|
+
# spec.fixity == "level2"). Truth-functionally conjunction; kept distinct in the AST.
|
|
3579
|
+
# Unlike the counting binder it needs no sort annotation, so it drops into the sorted
|
|
3580
|
+
# MSFOL mode too — hence the classical-mode list rather than _NL_NODE_MODES.
|
|
3581
|
+
_CONTRAST_MODES = ("fol", "modal", "second_order", "msfol")
|
|
3582
|
+
register_operator(Contrast, "level2", "Ⓒ", "\\mathbin{\\mathsf{C}}", 3)
|
|
3583
|
+
for _m in _CONTRAST_MODES:
|
|
3584
|
+
register_parser_op(Contrast, _m, "level2", "contrast_", '"Ⓒ"',
|
|
3585
|
+
lambda items: _fold_binary(items, Contrast),
|
|
3586
|
+
only_name="only_contrast")
|