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,1467 @@
|
|
|
1
|
+
"""Modal AST node classes: alethic, epistemic, doxastic, temporal, and deontic operators.
|
|
2
|
+
|
|
3
|
+
These nodes extend the classical FOL AST with modal/temporal operators. They are
|
|
4
|
+
purely syntactic in this phase: there is no Kripke semantics yet, and they reject
|
|
5
|
+
the classical export back-ends (to_z3/to_prover9/to_tptp) — a modal formula must
|
|
6
|
+
first be evaluated by the Kripke evaluator or translated to plain FOL via a
|
|
7
|
+
standard-translation (``modal_to_fol``) before any first-order export.
|
|
8
|
+
|
|
9
|
+
Every node is a frozen dataclass (hashable) subclassing Node. Structural methods
|
|
10
|
+
(free_variables, traversal, to_msfol, _relativize) are inherited unchanged: they
|
|
11
|
+
go through Node.map_children / Node._child_nodes, which already treat the formula
|
|
12
|
+
fields here as structural children, so recursion works automatically. The
|
|
13
|
+
``agent`` field of Knows/Believes is itself a **term** (Variable or Constant), a
|
|
14
|
+
structural child — so it is reached by free_variables / substitution / β-reduction
|
|
15
|
+
and can be a quantified variable (``∀x (Student(x) → K_x φ)``). The Unicode and
|
|
16
|
+
LaTeX renderers live in _msfl_nodes.py and dispatch by class name; serialisation,
|
|
17
|
+
tree labels, and export-rejection live here.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from typing import Tuple
|
|
22
|
+
|
|
23
|
+
from ._fol_nodes import (
|
|
24
|
+
Node, Variable, Constant, Z3Env, NODE_CLASSES,
|
|
25
|
+
register_operator, register_parser_op,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _coerce_agent(agent):
|
|
30
|
+
"""Coerce an agent to a term Node: a bare string becomes a Constant (named agent)."""
|
|
31
|
+
return Constant(agent) if isinstance(agent, str) else agent
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _agent_label(agent: Node) -> str:
|
|
35
|
+
"""Render an agent term as the short name used in K_<agent> / relation keys."""
|
|
36
|
+
return getattr(agent, "name", None) or agent.to_unicode_str()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _coerce_agent_group(group) -> Tuple[Node, ...]:
|
|
40
|
+
"""Coerce an iterable of agents to a tuple of term Nodes (see :func:`_coerce_agent`)."""
|
|
41
|
+
return tuple(_coerce_agent(a) for a in group)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _group_label(group: Tuple[Node, ...]) -> str:
|
|
45
|
+
"""Render a group of agent terms as the comma-joined list used inside ``{…}``."""
|
|
46
|
+
return ",".join(_agent_label(a) for a in group)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def parse_agent_token(token_text: str, prefix_len: int = 2) -> Node:
|
|
50
|
+
"""Turn a 'K_x' / 'B_alice' token into an agent **term**.
|
|
51
|
+
|
|
52
|
+
The agent is parsed as a Variable; binding is decided afterwards by the
|
|
53
|
+
:func:`resolve_agent_variables` scope pass (the same architecture as
|
|
54
|
+
:func:`resolve_lambda_scope`): an agent bound by an enclosing object quantifier
|
|
55
|
+
stays a Variable, a free one is demoted to a named Constant. We do not re-decide
|
|
56
|
+
variable-vs-constant by name form here — that would duplicate the lexer's VARIABLE
|
|
57
|
+
convention (a quantifier can only bind a VARIABLE-form name, so a bound agent is
|
|
58
|
+
always such a name anyway).
|
|
59
|
+
"""
|
|
60
|
+
return Variable(str(token_text)[prefix_len:])
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def resolve_agent_variables(node: Node, bound: frozenset = frozenset()) -> Node:
|
|
64
|
+
"""Demote a FREE epistemic/doxastic agent variable to a Constant (a named agent).
|
|
65
|
+
|
|
66
|
+
An agent that lexes as a variable (``K_x``) is genuinely a bound variable only when
|
|
67
|
+
an enclosing object quantifier binds its name — ``∀x (Student(x) → K_x φ)`` quantifies
|
|
68
|
+
over agents. A bare ``K_a`` with no binder denotes the specific named agent ``a``, so
|
|
69
|
+
its free agent variable is demoted to ``Constant("a")``. Applied as a parser post-pass.
|
|
70
|
+
"""
|
|
71
|
+
from dataclasses import replace
|
|
72
|
+
from .nodes import Quantifier, SortedQuantifier
|
|
73
|
+
if isinstance(node, (Quantifier, SortedQuantifier)):
|
|
74
|
+
inner = bound | {node.variable.name}
|
|
75
|
+
return replace(node, formula=resolve_agent_variables(node.formula, inner))
|
|
76
|
+
if isinstance(node, (Knows, Believes, Says, Wants)):
|
|
77
|
+
agent = node.agent
|
|
78
|
+
if isinstance(agent, Variable) and agent.name not in bound:
|
|
79
|
+
agent = Constant(agent.name)
|
|
80
|
+
return replace(node, agent=agent,
|
|
81
|
+
formula=resolve_agent_variables(node.formula, bound))
|
|
82
|
+
if isinstance(node, (EverybodyKnows, DistributedKnowledge, CommonKnowledge)):
|
|
83
|
+
group = tuple(
|
|
84
|
+
Constant(a.name) if isinstance(a, Variable) and a.name not in bound else a
|
|
85
|
+
for a in node.group
|
|
86
|
+
)
|
|
87
|
+
return replace(node, group=group,
|
|
88
|
+
formula=resolve_agent_variables(node.formula, bound))
|
|
89
|
+
return node.map_children(lambda c: resolve_agent_variables(c, bound))
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# Shared message for the export back-ends: modal nodes cannot be lowered to a
|
|
93
|
+
# first-order back-end directly.
|
|
94
|
+
_NO_EXPORT = (
|
|
95
|
+
"Modal operators have no direct first-order export; evaluate the formula with "
|
|
96
|
+
"the Kripke evaluator or translate it to plain FOL (modal_to_fol) before "
|
|
97
|
+
"calling to_z3/to_prover9/to_tptp."
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass(frozen=True)
|
|
102
|
+
class Box(Node):
|
|
103
|
+
"""Alethic necessity □φ: φ holds in every accessible world."""
|
|
104
|
+
|
|
105
|
+
formula: Node
|
|
106
|
+
|
|
107
|
+
def _tree_parts(self):
|
|
108
|
+
"""Return the □ label and the single subformula child."""
|
|
109
|
+
return "□", [self.formula]
|
|
110
|
+
|
|
111
|
+
def to_dict(self):
|
|
112
|
+
"""Serialise to dict with type tag and recursively serialised subformula."""
|
|
113
|
+
return {"_type": "Box", "formula": self.formula.to_dict()}
|
|
114
|
+
|
|
115
|
+
@staticmethod
|
|
116
|
+
def from_dict(d):
|
|
117
|
+
"""Deserialise a Box from a dict produced by to_dict."""
|
|
118
|
+
return Box(Node.from_dict(d["formula"]))
|
|
119
|
+
|
|
120
|
+
def to_z3(self, env: Z3Env = None):
|
|
121
|
+
"""Reject Z3 export: modal operators have no direct first-order encoding."""
|
|
122
|
+
raise NotImplementedError(_NO_EXPORT)
|
|
123
|
+
|
|
124
|
+
def to_prover9(self) -> str:
|
|
125
|
+
"""Reject Prover9 export: modal operators have no direct first-order encoding."""
|
|
126
|
+
raise NotImplementedError(_NO_EXPORT)
|
|
127
|
+
|
|
128
|
+
def to_tptp(self) -> str:
|
|
129
|
+
"""Reject TPTP export: modal operators have no direct first-order encoding."""
|
|
130
|
+
raise NotImplementedError(_NO_EXPORT)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@dataclass(frozen=True)
|
|
134
|
+
class Diamond(Node):
|
|
135
|
+
"""Alethic possibility ◇φ: φ holds in some accessible world."""
|
|
136
|
+
|
|
137
|
+
formula: Node
|
|
138
|
+
|
|
139
|
+
def _tree_parts(self):
|
|
140
|
+
"""Return the ◇ label and the single subformula child."""
|
|
141
|
+
return "◇", [self.formula]
|
|
142
|
+
|
|
143
|
+
def to_dict(self):
|
|
144
|
+
"""Serialise to dict with type tag and recursively serialised subformula."""
|
|
145
|
+
return {"_type": "Diamond", "formula": self.formula.to_dict()}
|
|
146
|
+
|
|
147
|
+
@staticmethod
|
|
148
|
+
def from_dict(d):
|
|
149
|
+
"""Deserialise a Diamond from a dict produced by to_dict."""
|
|
150
|
+
return Diamond(Node.from_dict(d["formula"]))
|
|
151
|
+
|
|
152
|
+
def to_z3(self, env: Z3Env = None):
|
|
153
|
+
"""Reject Z3 export: modal operators have no direct first-order encoding."""
|
|
154
|
+
raise NotImplementedError(_NO_EXPORT)
|
|
155
|
+
|
|
156
|
+
def to_prover9(self) -> str:
|
|
157
|
+
"""Reject Prover9 export: modal operators have no direct first-order encoding."""
|
|
158
|
+
raise NotImplementedError(_NO_EXPORT)
|
|
159
|
+
|
|
160
|
+
def to_tptp(self) -> str:
|
|
161
|
+
"""Reject TPTP export: modal operators have no direct first-order encoding."""
|
|
162
|
+
raise NotImplementedError(_NO_EXPORT)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@dataclass(frozen=True)
|
|
166
|
+
class Knows(Node):
|
|
167
|
+
"""Epistemic K_a φ: agent ``agent`` knows φ.
|
|
168
|
+
|
|
169
|
+
``agent`` is a **term** (Variable or Constant), a structural child, so it can be
|
|
170
|
+
a quantified variable (``∀x (Student(x) → K_x φ)``). A bare string passed to the
|
|
171
|
+
constructor is coerced to a Constant (a named agent) for backward compatibility.
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
agent: Node
|
|
175
|
+
formula: Node
|
|
176
|
+
|
|
177
|
+
def __post_init__(self):
|
|
178
|
+
"""Coerce a string agent to a Constant so legacy ``Knows("alice", φ)`` still works."""
|
|
179
|
+
coerced = _coerce_agent(self.agent)
|
|
180
|
+
if coerced is not self.agent:
|
|
181
|
+
object.__setattr__(self, "agent", coerced)
|
|
182
|
+
|
|
183
|
+
def _tree_parts(self):
|
|
184
|
+
"""Return the K_<agent> label and the single subformula child."""
|
|
185
|
+
return f"K_{_agent_label(self.agent)}", [self.formula]
|
|
186
|
+
|
|
187
|
+
def to_dict(self):
|
|
188
|
+
"""Serialise to dict with type tag, agent term, and serialised subformula."""
|
|
189
|
+
return {"_type": "Knows", "agent": self.agent.to_dict(),
|
|
190
|
+
"formula": self.formula.to_dict()}
|
|
191
|
+
|
|
192
|
+
@staticmethod
|
|
193
|
+
def from_dict(d):
|
|
194
|
+
"""Deserialise a Knows from a dict produced by to_dict (legacy string agent ok)."""
|
|
195
|
+
agent = d["agent"]
|
|
196
|
+
agent = Node.from_dict(agent) if isinstance(agent, dict) else agent
|
|
197
|
+
return Knows(agent, Node.from_dict(d["formula"]))
|
|
198
|
+
|
|
199
|
+
def to_z3(self, env: Z3Env = None):
|
|
200
|
+
"""Reject Z3 export: modal operators have no direct first-order encoding."""
|
|
201
|
+
raise NotImplementedError(_NO_EXPORT)
|
|
202
|
+
|
|
203
|
+
def to_prover9(self) -> str:
|
|
204
|
+
"""Reject Prover9 export: modal operators have no direct first-order encoding."""
|
|
205
|
+
raise NotImplementedError(_NO_EXPORT)
|
|
206
|
+
|
|
207
|
+
def to_tptp(self) -> str:
|
|
208
|
+
"""Reject TPTP export: modal operators have no direct first-order encoding."""
|
|
209
|
+
raise NotImplementedError(_NO_EXPORT)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
@dataclass(frozen=True)
|
|
213
|
+
class Believes(Node):
|
|
214
|
+
"""Doxastic B_a φ: agent ``agent`` believes φ.
|
|
215
|
+
|
|
216
|
+
``agent`` is a **term** (Variable or Constant), a structural child, so it can be a
|
|
217
|
+
quantified variable. A bare string is coerced to a Constant for backward compatibility.
|
|
218
|
+
"""
|
|
219
|
+
|
|
220
|
+
agent: Node
|
|
221
|
+
formula: Node
|
|
222
|
+
|
|
223
|
+
def __post_init__(self):
|
|
224
|
+
"""Coerce a string agent to a Constant so legacy ``Believes("alice", φ)`` still works."""
|
|
225
|
+
coerced = _coerce_agent(self.agent)
|
|
226
|
+
if coerced is not self.agent:
|
|
227
|
+
object.__setattr__(self, "agent", coerced)
|
|
228
|
+
|
|
229
|
+
def _tree_parts(self):
|
|
230
|
+
"""Return the B_<agent> label and the single subformula child."""
|
|
231
|
+
return f"B_{_agent_label(self.agent)}", [self.formula]
|
|
232
|
+
|
|
233
|
+
def to_dict(self):
|
|
234
|
+
"""Serialise to dict with type tag, agent term, and serialised subformula."""
|
|
235
|
+
return {"_type": "Believes", "agent": self.agent.to_dict(),
|
|
236
|
+
"formula": self.formula.to_dict()}
|
|
237
|
+
|
|
238
|
+
@staticmethod
|
|
239
|
+
def from_dict(d):
|
|
240
|
+
"""Deserialise a Believes from a dict produced by to_dict (legacy string agent ok)."""
|
|
241
|
+
agent = d["agent"]
|
|
242
|
+
agent = Node.from_dict(agent) if isinstance(agent, dict) else agent
|
|
243
|
+
return Believes(agent, Node.from_dict(d["formula"]))
|
|
244
|
+
|
|
245
|
+
def to_z3(self, env: Z3Env = None):
|
|
246
|
+
"""Reject Z3 export: modal operators have no direct first-order encoding."""
|
|
247
|
+
raise NotImplementedError(_NO_EXPORT)
|
|
248
|
+
|
|
249
|
+
def to_prover9(self) -> str:
|
|
250
|
+
"""Reject Prover9 export: modal operators have no direct first-order encoding."""
|
|
251
|
+
raise NotImplementedError(_NO_EXPORT)
|
|
252
|
+
|
|
253
|
+
def to_tptp(self) -> str:
|
|
254
|
+
"""Reject TPTP export: modal operators have no direct first-order encoding."""
|
|
255
|
+
raise NotImplementedError(_NO_EXPORT)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
#: Why the group-epistemic operators (E_G / D_G / C_G) have no direct
|
|
259
|
+
#: first-order export via to_z3/to_prover9/to_tptp, even though E_G and D_G
|
|
260
|
+
#: individually ARE first-order definable (see fol.modal_translation): every
|
|
261
|
+
#: modal node in this module rejects the classical back-ends directly and
|
|
262
|
+
#: requires going through modal_to_fol (standard_translation) first — the
|
|
263
|
+
#: SAME rule Knows/Believes themselves follow despite being FO-definable too.
|
|
264
|
+
_NO_GROUP_EXPORT = _NO_EXPORT
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
@dataclass(frozen=True)
|
|
268
|
+
class EverybodyKnows(Node):
|
|
269
|
+
"""Group-epistemic E_G φ: every agent in ``group`` knows φ (one-step).
|
|
270
|
+
|
|
271
|
+
By definition E_G φ ≡ ⋀_{a∈G} K_a φ — equivalently, φ holds at every world
|
|
272
|
+
reachable from the current one by a SINGLE edge of the UNION of the
|
|
273
|
+
group's per-agent ``"K:"+agent`` relations (see
|
|
274
|
+
:func:`unicode_logic_kit.semantics.action_models.everybody_knows`, which
|
|
275
|
+
this node's :func:`~unicode_logic_kit.semantics.kripke.satisfies_modal`
|
|
276
|
+
dispatch calls directly — the same "thin AST wrapper over an existing
|
|
277
|
+
function" pattern :class:`Announce` uses for
|
|
278
|
+
:func:`~unicode_logic_kit.semantics.dynamic_epistemic.announce`).
|
|
279
|
+
|
|
280
|
+
``group`` is a tuple of agent **terms** (Variable or Constant, exactly
|
|
281
|
+
like :class:`Knows`'s ``agent`` field — each reachable by
|
|
282
|
+
free_variables/substitution/β-reduction and resolved by the same
|
|
283
|
+
:func:`resolve_agent_variables` scope pass, so a group member bound by an
|
|
284
|
+
enclosing quantifier stays a Variable, e.g.
|
|
285
|
+
``∀x (Student(x) → E_{x,b} φ)``). An iterable of bare strings/Nodes is
|
|
286
|
+
accepted and coerced. An EMPTY group is allowed and, matching
|
|
287
|
+
:func:`everybody_knows`'s own convention, makes ``E_∅ φ`` vacuously true
|
|
288
|
+
at every world (the union over zero relations is the empty relation).
|
|
289
|
+
"""
|
|
290
|
+
|
|
291
|
+
group: Tuple[Node, ...]
|
|
292
|
+
formula: Node
|
|
293
|
+
|
|
294
|
+
def __post_init__(self):
|
|
295
|
+
"""Coerce every group member to a term Node (see :func:`_coerce_agent_group`)."""
|
|
296
|
+
object.__setattr__(self, "group", _coerce_agent_group(self.group))
|
|
297
|
+
|
|
298
|
+
def _tree_parts(self):
|
|
299
|
+
"""Return the E_{group} label and the single subformula child."""
|
|
300
|
+
return f"E_{{{_group_label(self.group)}}}", [self.formula]
|
|
301
|
+
|
|
302
|
+
def to_dict(self):
|
|
303
|
+
"""Serialise to dict with type tag, group terms, and serialised subformula."""
|
|
304
|
+
return {"_type": "EverybodyKnows",
|
|
305
|
+
"group": [a.to_dict() for a in self.group],
|
|
306
|
+
"formula": self.formula.to_dict()}
|
|
307
|
+
|
|
308
|
+
@staticmethod
|
|
309
|
+
def from_dict(d):
|
|
310
|
+
"""Deserialise an EverybodyKnows from a dict produced by to_dict."""
|
|
311
|
+
return EverybodyKnows(tuple(Node.from_dict(a) for a in d["group"]),
|
|
312
|
+
Node.from_dict(d["formula"]))
|
|
313
|
+
|
|
314
|
+
def to_unicode_str(self) -> str:
|
|
315
|
+
"""Render as ``E_{a,b,c} φ`` (see the class docstring; overridden like
|
|
316
|
+
:class:`Announce` — no registry fixity expresses a variable-length
|
|
317
|
+
agent LIST, only :class:`Knows`'s single agent)."""
|
|
318
|
+
post = _pal_wrap(self.formula, 4, lambda n: n.to_unicode_str())
|
|
319
|
+
return f"E_{{{_group_label(self.group)}}} {post}"
|
|
320
|
+
|
|
321
|
+
def to_latex(self) -> str:
|
|
322
|
+
"""Render as LaTeX ``\\mathsf{E}_{\\{a,b,c\\}} φ``."""
|
|
323
|
+
from ._msfl_nodes import _latex_escape # lazy: avoid a module-load-order cycle
|
|
324
|
+
agents = ", ".join(_latex_escape(_agent_label(a)) for a in self.group)
|
|
325
|
+
post = _pal_wrap(self.formula, 4, lambda n: n.to_latex())
|
|
326
|
+
return f"\\mathsf{{E}}_{{\\{{{agents}\\}}}} {post}"
|
|
327
|
+
|
|
328
|
+
def to_z3(self, env: Z3Env = None):
|
|
329
|
+
"""Reject Z3 export: modal operators have no direct first-order encoding."""
|
|
330
|
+
raise NotImplementedError(_NO_GROUP_EXPORT)
|
|
331
|
+
|
|
332
|
+
def to_prover9(self) -> str:
|
|
333
|
+
"""Reject Prover9 export: modal operators have no direct first-order encoding."""
|
|
334
|
+
raise NotImplementedError(_NO_GROUP_EXPORT)
|
|
335
|
+
|
|
336
|
+
def to_tptp(self) -> str:
|
|
337
|
+
"""Reject TPTP export: modal operators have no direct first-order encoding."""
|
|
338
|
+
raise NotImplementedError(_NO_GROUP_EXPORT)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
@dataclass(frozen=True)
|
|
342
|
+
class DistributedKnowledge(Node):
|
|
343
|
+
"""Group-epistemic D_G φ: φ is DISTRIBUTED knowledge in ``group`` (one-step).
|
|
344
|
+
|
|
345
|
+
D_G φ holds at ``world`` iff φ holds at every world reachable by a SINGLE
|
|
346
|
+
edge of the INTERSECTION ``⋂_{a∈G} R_a`` of the group's per-agent
|
|
347
|
+
``"K:"+agent`` relations — pooling every agent's information together.
|
|
348
|
+
D_G is the LOGICALLY WEAKEST of the group notions (``K_a φ → D_G φ`` for
|
|
349
|
+
every ``a ∈ G``, and transitively ``E_G φ → D_G φ`` / ``C_G φ → D_G φ``,
|
|
350
|
+
never the converse — see
|
|
351
|
+
:func:`~unicode_logic_kit.semantics.action_models.distributed_knowledge_holds`'s
|
|
352
|
+
docstring for why "logically weakest formal constraint" is not in tension
|
|
353
|
+
with "pools richer information": FHMV, *Reasoning About Knowledge*, MIT
|
|
354
|
+
Press 1995, ch. 2). Dispatch goes to
|
|
355
|
+
:func:`unicode_logic_kit.semantics.action_models.distributed_knowledge_holds`
|
|
356
|
+
(same pattern as :class:`EverybodyKnows` / :func:`everybody_knows`).
|
|
357
|
+
|
|
358
|
+
``group`` is a tuple of agent terms exactly like :class:`EverybodyKnows`'s
|
|
359
|
+
``group`` (coerced, resolved by :func:`resolve_agent_variables`) — but,
|
|
360
|
+
UNLIKE :class:`EverybodyKnows`/:class:`CommonKnowledge`, an EMPTY group is
|
|
361
|
+
refused at construction: an intersection over an empty family is
|
|
362
|
+
conventionally the UNIVERSAL relation, so ``D_∅ φ`` would demand φ true at
|
|
363
|
+
literally every world of the model — a degenerate reading nobody
|
|
364
|
+
constructing this node means, so this class raises loudly here (matching
|
|
365
|
+
the same refusal
|
|
366
|
+
:func:`~unicode_logic_kit.semantics.action_models.distributed_knowledge_holds`
|
|
367
|
+
raises at evaluation time) rather than let it reach ``modal_to_fol``/the
|
|
368
|
+
Kripke evaluator as a silent vacuous-universal.
|
|
369
|
+
"""
|
|
370
|
+
|
|
371
|
+
group: Tuple[Node, ...]
|
|
372
|
+
formula: Node
|
|
373
|
+
|
|
374
|
+
def __post_init__(self):
|
|
375
|
+
"""Coerce every group member to a term Node; refuse an empty group."""
|
|
376
|
+
object.__setattr__(self, "group", _coerce_agent_group(self.group))
|
|
377
|
+
if not self.group:
|
|
378
|
+
raise ValueError(
|
|
379
|
+
"DistributedKnowledge: D_∅ φ (an empty group) is refused — an "
|
|
380
|
+
"intersection over zero relations is conventionally the "
|
|
381
|
+
"UNIVERSAL relation, which would make D_∅ φ demand φ at every "
|
|
382
|
+
"world of the model, not a sensible reading of 'distributed "
|
|
383
|
+
"knowledge of an empty group'. Name at least one agent."
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
def _tree_parts(self):
|
|
387
|
+
"""Return the D_{group} label and the single subformula child."""
|
|
388
|
+
return f"D_{{{_group_label(self.group)}}}", [self.formula]
|
|
389
|
+
|
|
390
|
+
def to_dict(self):
|
|
391
|
+
"""Serialise to dict with type tag, group terms, and serialised subformula."""
|
|
392
|
+
return {"_type": "DistributedKnowledge",
|
|
393
|
+
"group": [a.to_dict() for a in self.group],
|
|
394
|
+
"formula": self.formula.to_dict()}
|
|
395
|
+
|
|
396
|
+
@staticmethod
|
|
397
|
+
def from_dict(d):
|
|
398
|
+
"""Deserialise a DistributedKnowledge from a dict produced by to_dict."""
|
|
399
|
+
return DistributedKnowledge(tuple(Node.from_dict(a) for a in d["group"]),
|
|
400
|
+
Node.from_dict(d["formula"]))
|
|
401
|
+
|
|
402
|
+
def to_unicode_str(self) -> str:
|
|
403
|
+
"""Render as ``D_{a,b,c} φ`` (see :class:`EverybodyKnows`)."""
|
|
404
|
+
post = _pal_wrap(self.formula, 4, lambda n: n.to_unicode_str())
|
|
405
|
+
return f"D_{{{_group_label(self.group)}}} {post}"
|
|
406
|
+
|
|
407
|
+
def to_latex(self) -> str:
|
|
408
|
+
"""Render as LaTeX ``\\mathsf{D}_{\\{a,b,c\\}} φ``."""
|
|
409
|
+
from ._msfl_nodes import _latex_escape # lazy: avoid a module-load-order cycle
|
|
410
|
+
agents = ", ".join(_latex_escape(_agent_label(a)) for a in self.group)
|
|
411
|
+
post = _pal_wrap(self.formula, 4, lambda n: n.to_latex())
|
|
412
|
+
return f"\\mathsf{{D}}_{{\\{{{agents}\\}}}} {post}"
|
|
413
|
+
|
|
414
|
+
def to_z3(self, env: Z3Env = None):
|
|
415
|
+
"""Reject Z3 export: modal operators have no direct first-order encoding."""
|
|
416
|
+
raise NotImplementedError(_NO_GROUP_EXPORT)
|
|
417
|
+
|
|
418
|
+
def to_prover9(self) -> str:
|
|
419
|
+
"""Reject Prover9 export: modal operators have no direct first-order encoding."""
|
|
420
|
+
raise NotImplementedError(_NO_GROUP_EXPORT)
|
|
421
|
+
|
|
422
|
+
def to_tptp(self) -> str:
|
|
423
|
+
"""Reject TPTP export: modal operators have no direct first-order encoding."""
|
|
424
|
+
raise NotImplementedError(_NO_GROUP_EXPORT)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
@dataclass(frozen=True)
|
|
428
|
+
class CommonKnowledge(Node):
|
|
429
|
+
"""Group-epistemic C_G φ: φ is COMMON knowledge in ``group``.
|
|
430
|
+
|
|
431
|
+
C_G φ is the infinite conjunction ``E_G φ ∧ E_G E_G φ ∧ …`` — equivalently
|
|
432
|
+
(FHMV, *Reasoning About Knowledge*, MIT Press 1995, §2.4), φ holds at
|
|
433
|
+
every world reachable from the current one by the REFLEXIVE-TRANSITIVE
|
|
434
|
+
closure of the group's union relation ``⋃_{a∈G} R_a``. Dispatch goes to
|
|
435
|
+
:func:`unicode_logic_kit.semantics.action_models.common_knowledge_holds`
|
|
436
|
+
(same pattern as :class:`EverybodyKnows`). UNLIKE E_G/D_G, C_G is NOT
|
|
437
|
+
first-order definable (transitive closure): :func:`modal_to_fol`
|
|
438
|
+
(:mod:`unicode_logic_kit.fol.modal_translation`) refuses it by name, exactly
|
|
439
|
+
as it already refuses :class:`Until`/:class:`Since`.
|
|
440
|
+
|
|
441
|
+
``group`` is a tuple of agent terms exactly like :class:`EverybodyKnows`'s
|
|
442
|
+
``group`` (coerced, resolved by :func:`resolve_agent_variables`). An EMPTY
|
|
443
|
+
group is allowed, matching :func:`common_knowledge_holds`'s own
|
|
444
|
+
convention (``C_∅ φ`` reduces to plain ``φ`` — the reflexive closure of
|
|
445
|
+
the empty relation is just the current world).
|
|
446
|
+
"""
|
|
447
|
+
|
|
448
|
+
group: Tuple[Node, ...]
|
|
449
|
+
formula: Node
|
|
450
|
+
|
|
451
|
+
def __post_init__(self):
|
|
452
|
+
"""Coerce every group member to a term Node (see :func:`_coerce_agent_group`)."""
|
|
453
|
+
object.__setattr__(self, "group", _coerce_agent_group(self.group))
|
|
454
|
+
|
|
455
|
+
def _tree_parts(self):
|
|
456
|
+
"""Return the C_{group} label and the single subformula child."""
|
|
457
|
+
return f"C_{{{_group_label(self.group)}}}", [self.formula]
|
|
458
|
+
|
|
459
|
+
def to_dict(self):
|
|
460
|
+
"""Serialise to dict with type tag, group terms, and serialised subformula."""
|
|
461
|
+
return {"_type": "CommonKnowledge",
|
|
462
|
+
"group": [a.to_dict() for a in self.group],
|
|
463
|
+
"formula": self.formula.to_dict()}
|
|
464
|
+
|
|
465
|
+
@staticmethod
|
|
466
|
+
def from_dict(d):
|
|
467
|
+
"""Deserialise a CommonKnowledge from a dict produced by to_dict."""
|
|
468
|
+
return CommonKnowledge(tuple(Node.from_dict(a) for a in d["group"]),
|
|
469
|
+
Node.from_dict(d["formula"]))
|
|
470
|
+
|
|
471
|
+
def to_unicode_str(self) -> str:
|
|
472
|
+
"""Render as ``C_{a,b,c} φ`` (see :class:`EverybodyKnows`)."""
|
|
473
|
+
post = _pal_wrap(self.formula, 4, lambda n: n.to_unicode_str())
|
|
474
|
+
return f"C_{{{_group_label(self.group)}}} {post}"
|
|
475
|
+
|
|
476
|
+
def to_latex(self) -> str:
|
|
477
|
+
"""Render as LaTeX ``\\mathsf{C}_{\\{a,b,c\\}} φ``."""
|
|
478
|
+
from ._msfl_nodes import _latex_escape # lazy: avoid a module-load-order cycle
|
|
479
|
+
agents = ", ".join(_latex_escape(_agent_label(a)) for a in self.group)
|
|
480
|
+
post = _pal_wrap(self.formula, 4, lambda n: n.to_latex())
|
|
481
|
+
return f"\\mathsf{{C}}_{{\\{{{agents}\\}}}} {post}"
|
|
482
|
+
|
|
483
|
+
def to_z3(self, env: Z3Env = None):
|
|
484
|
+
"""Reject Z3 export: modal operators have no direct first-order encoding."""
|
|
485
|
+
raise NotImplementedError(_NO_GROUP_EXPORT)
|
|
486
|
+
|
|
487
|
+
def to_prover9(self) -> str:
|
|
488
|
+
"""Reject Prover9 export: modal operators have no direct first-order encoding."""
|
|
489
|
+
raise NotImplementedError(_NO_GROUP_EXPORT)
|
|
490
|
+
|
|
491
|
+
def to_tptp(self) -> str:
|
|
492
|
+
"""Reject TPTP export: modal operators have no direct first-order encoding."""
|
|
493
|
+
raise NotImplementedError(_NO_GROUP_EXPORT)
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
NODE_CLASSES.update({
|
|
497
|
+
"EverybodyKnows": EverybodyKnows,
|
|
498
|
+
"DistributedKnowledge": DistributedKnowledge,
|
|
499
|
+
"CommonKnowledge": CommonKnowledge,
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
@dataclass(frozen=True)
|
|
504
|
+
class Says(Node):
|
|
505
|
+
"""Assertive (reportative) Say_a φ: agent ``agent`` asserts/says that φ.
|
|
506
|
+
|
|
507
|
+
A NON-factive, NON-doxastic attitude: ``Says_a φ`` does not entail φ (an
|
|
508
|
+
assertion can be false) and does not entail ``Believes_a φ`` (one may assert
|
|
509
|
+
what one disbelieves). It is the minimal normal modality K over its own
|
|
510
|
+
per-agent ``"Say:"`` accessibility relation — no frame conditions — so neither
|
|
511
|
+
factivity (T) nor any doxastic bridge is validated. ``agent`` is a **term**
|
|
512
|
+
(Variable or Constant), a structural child, so a bound agent variable works
|
|
513
|
+
(``∀x (Speaker(x) → Say_x φ)``); a bare string is coerced to a Constant.
|
|
514
|
+
"""
|
|
515
|
+
|
|
516
|
+
agent: Node
|
|
517
|
+
formula: Node
|
|
518
|
+
|
|
519
|
+
def __post_init__(self):
|
|
520
|
+
"""Coerce a string agent to a Constant so ``Says("alice", φ)`` still works."""
|
|
521
|
+
coerced = _coerce_agent(self.agent)
|
|
522
|
+
if coerced is not self.agent:
|
|
523
|
+
object.__setattr__(self, "agent", coerced)
|
|
524
|
+
|
|
525
|
+
def _tree_parts(self):
|
|
526
|
+
"""Return the Say_<agent> label and the single subformula child."""
|
|
527
|
+
return f"Say_{_agent_label(self.agent)}", [self.formula]
|
|
528
|
+
|
|
529
|
+
def to_dict(self):
|
|
530
|
+
"""Serialise to dict with type tag, agent term, and serialised subformula."""
|
|
531
|
+
return {"_type": "Says", "agent": self.agent.to_dict(),
|
|
532
|
+
"formula": self.formula.to_dict()}
|
|
533
|
+
|
|
534
|
+
@staticmethod
|
|
535
|
+
def from_dict(d):
|
|
536
|
+
"""Deserialise a Says from a dict produced by to_dict (legacy string agent ok)."""
|
|
537
|
+
agent = d["agent"]
|
|
538
|
+
agent = Node.from_dict(agent) if isinstance(agent, dict) else agent
|
|
539
|
+
return Says(agent, Node.from_dict(d["formula"]))
|
|
540
|
+
|
|
541
|
+
def to_z3(self, env: Z3Env = None):
|
|
542
|
+
"""Reject Z3 export: modal operators have no direct first-order encoding."""
|
|
543
|
+
raise NotImplementedError(_NO_EXPORT)
|
|
544
|
+
|
|
545
|
+
def to_prover9(self) -> str:
|
|
546
|
+
"""Reject Prover9 export: modal operators have no direct first-order encoding."""
|
|
547
|
+
raise NotImplementedError(_NO_EXPORT)
|
|
548
|
+
|
|
549
|
+
def to_tptp(self) -> str:
|
|
550
|
+
"""Reject TPTP export: modal operators have no direct first-order encoding."""
|
|
551
|
+
raise NotImplementedError(_NO_EXPORT)
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
@dataclass(frozen=True)
|
|
555
|
+
class Wants(Node):
|
|
556
|
+
"""Bouletic (desiderative) Want_a φ: agent ``agent`` wants it to be that φ.
|
|
557
|
+
|
|
558
|
+
A NON-veridical attitude: ``Wants_a φ`` does not entail φ — wanting something
|
|
559
|
+
does not make it so (this is the whole point: ``Wants_a(Fly(a)) ∧ Fly(a)`` is
|
|
560
|
+
not derivable). It is the minimal normal modality K over its own per-agent
|
|
561
|
+
``"Want:"`` accessibility relation — no frame conditions — so veridicality (T)
|
|
562
|
+
is not validated. ``agent`` is a **term** (Variable or Constant), a structural
|
|
563
|
+
child, so a bound agent variable works; a bare string is coerced to a Constant.
|
|
564
|
+
"""
|
|
565
|
+
|
|
566
|
+
agent: Node
|
|
567
|
+
formula: Node
|
|
568
|
+
|
|
569
|
+
def __post_init__(self):
|
|
570
|
+
"""Coerce a string agent to a Constant so ``Wants("alice", φ)`` still works."""
|
|
571
|
+
coerced = _coerce_agent(self.agent)
|
|
572
|
+
if coerced is not self.agent:
|
|
573
|
+
object.__setattr__(self, "agent", coerced)
|
|
574
|
+
|
|
575
|
+
def _tree_parts(self):
|
|
576
|
+
"""Return the Want_<agent> label and the single subformula child."""
|
|
577
|
+
return f"Want_{_agent_label(self.agent)}", [self.formula]
|
|
578
|
+
|
|
579
|
+
def to_dict(self):
|
|
580
|
+
"""Serialise to dict with type tag, agent term, and serialised subformula."""
|
|
581
|
+
return {"_type": "Wants", "agent": self.agent.to_dict(),
|
|
582
|
+
"formula": self.formula.to_dict()}
|
|
583
|
+
|
|
584
|
+
@staticmethod
|
|
585
|
+
def from_dict(d):
|
|
586
|
+
"""Deserialise a Wants from a dict produced by to_dict (legacy string agent ok)."""
|
|
587
|
+
agent = d["agent"]
|
|
588
|
+
agent = Node.from_dict(agent) if isinstance(agent, dict) else agent
|
|
589
|
+
return Wants(agent, Node.from_dict(d["formula"]))
|
|
590
|
+
|
|
591
|
+
def to_z3(self, env: Z3Env = None):
|
|
592
|
+
"""Reject Z3 export: modal operators have no direct first-order encoding."""
|
|
593
|
+
raise NotImplementedError(_NO_EXPORT)
|
|
594
|
+
|
|
595
|
+
def to_prover9(self) -> str:
|
|
596
|
+
"""Reject Prover9 export: modal operators have no direct first-order encoding."""
|
|
597
|
+
raise NotImplementedError(_NO_EXPORT)
|
|
598
|
+
|
|
599
|
+
def to_tptp(self) -> str:
|
|
600
|
+
"""Reject TPTP export: modal operators have no direct first-order encoding."""
|
|
601
|
+
raise NotImplementedError(_NO_EXPORT)
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
@dataclass(frozen=True)
|
|
605
|
+
class Always(Node):
|
|
606
|
+
"""Temporal "globally/henceforth" Gφ: φ holds now and at every future point."""
|
|
607
|
+
|
|
608
|
+
formula: Node
|
|
609
|
+
|
|
610
|
+
def _tree_parts(self):
|
|
611
|
+
"""Return the Ⓖ label and the single subformula child."""
|
|
612
|
+
return "Ⓖ", [self.formula]
|
|
613
|
+
|
|
614
|
+
def to_dict(self):
|
|
615
|
+
"""Serialise to dict with type tag and recursively serialised subformula."""
|
|
616
|
+
return {"_type": "Always", "formula": self.formula.to_dict()}
|
|
617
|
+
|
|
618
|
+
@staticmethod
|
|
619
|
+
def from_dict(d):
|
|
620
|
+
"""Deserialise an Always from a dict produced by to_dict."""
|
|
621
|
+
return Always(Node.from_dict(d["formula"]))
|
|
622
|
+
|
|
623
|
+
def to_z3(self, env: Z3Env = None):
|
|
624
|
+
"""Reject Z3 export: modal operators have no direct first-order encoding."""
|
|
625
|
+
raise NotImplementedError(_NO_EXPORT)
|
|
626
|
+
|
|
627
|
+
def to_prover9(self) -> str:
|
|
628
|
+
"""Reject Prover9 export: modal operators have no direct first-order encoding."""
|
|
629
|
+
raise NotImplementedError(_NO_EXPORT)
|
|
630
|
+
|
|
631
|
+
def to_tptp(self) -> str:
|
|
632
|
+
"""Reject TPTP export: modal operators have no direct first-order encoding."""
|
|
633
|
+
raise NotImplementedError(_NO_EXPORT)
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
@dataclass(frozen=True)
|
|
637
|
+
class Eventually(Node):
|
|
638
|
+
"""Temporal "finally" Fφ: φ holds now or at some future point."""
|
|
639
|
+
|
|
640
|
+
formula: Node
|
|
641
|
+
|
|
642
|
+
def _tree_parts(self):
|
|
643
|
+
"""Return the Ⓕ label and the single subformula child."""
|
|
644
|
+
return "Ⓕ", [self.formula]
|
|
645
|
+
|
|
646
|
+
def to_dict(self):
|
|
647
|
+
"""Serialise to dict with type tag and recursively serialised subformula."""
|
|
648
|
+
return {"_type": "Eventually", "formula": self.formula.to_dict()}
|
|
649
|
+
|
|
650
|
+
@staticmethod
|
|
651
|
+
def from_dict(d):
|
|
652
|
+
"""Deserialise an Eventually from a dict produced by to_dict."""
|
|
653
|
+
return Eventually(Node.from_dict(d["formula"]))
|
|
654
|
+
|
|
655
|
+
def to_z3(self, env: Z3Env = None):
|
|
656
|
+
"""Reject Z3 export: modal operators have no direct first-order encoding."""
|
|
657
|
+
raise NotImplementedError(_NO_EXPORT)
|
|
658
|
+
|
|
659
|
+
def to_prover9(self) -> str:
|
|
660
|
+
"""Reject Prover9 export: modal operators have no direct first-order encoding."""
|
|
661
|
+
raise NotImplementedError(_NO_EXPORT)
|
|
662
|
+
|
|
663
|
+
def to_tptp(self) -> str:
|
|
664
|
+
"""Reject TPTP export: modal operators have no direct first-order encoding."""
|
|
665
|
+
raise NotImplementedError(_NO_EXPORT)
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
@dataclass(frozen=True)
|
|
669
|
+
class Next(Node):
|
|
670
|
+
"""Temporal "next" Xφ: φ holds at the immediately following point."""
|
|
671
|
+
|
|
672
|
+
formula: Node
|
|
673
|
+
|
|
674
|
+
def _tree_parts(self):
|
|
675
|
+
"""Return the Ⓝ label and the single subformula child."""
|
|
676
|
+
return "Ⓝ", [self.formula]
|
|
677
|
+
|
|
678
|
+
def to_dict(self):
|
|
679
|
+
"""Serialise to dict with type tag and recursively serialised subformula."""
|
|
680
|
+
return {"_type": "Next", "formula": self.formula.to_dict()}
|
|
681
|
+
|
|
682
|
+
@staticmethod
|
|
683
|
+
def from_dict(d):
|
|
684
|
+
"""Deserialise a Next from a dict produced by to_dict."""
|
|
685
|
+
return Next(Node.from_dict(d["formula"]))
|
|
686
|
+
|
|
687
|
+
def to_z3(self, env: Z3Env = None):
|
|
688
|
+
"""Reject Z3 export: modal operators have no direct first-order encoding."""
|
|
689
|
+
raise NotImplementedError(_NO_EXPORT)
|
|
690
|
+
|
|
691
|
+
def to_prover9(self) -> str:
|
|
692
|
+
"""Reject Prover9 export: modal operators have no direct first-order encoding."""
|
|
693
|
+
raise NotImplementedError(_NO_EXPORT)
|
|
694
|
+
|
|
695
|
+
def to_tptp(self) -> str:
|
|
696
|
+
"""Reject TPTP export: modal operators have no direct first-order encoding."""
|
|
697
|
+
raise NotImplementedError(_NO_EXPORT)
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
@dataclass(frozen=True)
|
|
701
|
+
class Until(Node):
|
|
702
|
+
"""Temporal "left until right" (left U right): left holds until right does.
|
|
703
|
+
|
|
704
|
+
``right`` must eventually hold; ``left`` holds at every point until then.
|
|
705
|
+
"""
|
|
706
|
+
|
|
707
|
+
left: Node
|
|
708
|
+
right: Node
|
|
709
|
+
|
|
710
|
+
def _tree_parts(self):
|
|
711
|
+
"""Return the Ⓤ label and the two subformula children."""
|
|
712
|
+
return "Ⓤ", [self.left, self.right]
|
|
713
|
+
|
|
714
|
+
def to_dict(self):
|
|
715
|
+
"""Serialise to dict with type tag and recursively serialised operands."""
|
|
716
|
+
return {"_type": "Until", "left": self.left.to_dict(), "right": self.right.to_dict()}
|
|
717
|
+
|
|
718
|
+
@staticmethod
|
|
719
|
+
def from_dict(d):
|
|
720
|
+
"""Deserialise an Until from a dict produced by to_dict."""
|
|
721
|
+
return Until(Node.from_dict(d["left"]), Node.from_dict(d["right"]))
|
|
722
|
+
|
|
723
|
+
def to_z3(self, env: Z3Env = None):
|
|
724
|
+
"""Reject Z3 export: modal operators have no direct first-order encoding."""
|
|
725
|
+
raise NotImplementedError(_NO_EXPORT)
|
|
726
|
+
|
|
727
|
+
def to_prover9(self) -> str:
|
|
728
|
+
"""Reject Prover9 export: modal operators have no direct first-order encoding."""
|
|
729
|
+
raise NotImplementedError(_NO_EXPORT)
|
|
730
|
+
|
|
731
|
+
def to_tptp(self) -> str:
|
|
732
|
+
"""Reject TPTP export: modal operators have no direct first-order encoding."""
|
|
733
|
+
raise NotImplementedError(_NO_EXPORT)
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
#: Why the counterfactual conditionals have no first-order (or Kripke) export.
|
|
737
|
+
_NO_COUNTERFACTUAL_EXPORT = (
|
|
738
|
+
"Counterfactual conditionals (□→ / ◇→) are evaluated over a similarity "
|
|
739
|
+
"ordering of worlds (Lewis spheres), not over an accessibility relation, and "
|
|
740
|
+
"are not first-order definable — collapsing □→ to the material → is exactly "
|
|
741
|
+
"the mistake the connective exists to avoid. Evaluate with "
|
|
742
|
+
"unicode_logic_kit.cf_satisfies / would / might over a CounterfactualModel."
|
|
743
|
+
)
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
@dataclass(frozen=True)
|
|
747
|
+
class Would(Node):
|
|
748
|
+
"""Lewis counterfactual ``left □→ right`` — "if left WERE the case, right WOULD be".
|
|
749
|
+
|
|
750
|
+
True at ``w`` iff no sphere around ``w`` contains a ``left``-world (vacuous), or
|
|
751
|
+
else, in the SMALLEST sphere that does, every ``left``-world is a ``right``-world.
|
|
752
|
+
Distinct from the material :class:`Implies`: "if kangaroos had no tails they would
|
|
753
|
+
topple over" is not made true merely by kangaroos having tails.
|
|
754
|
+
|
|
755
|
+
The similarity ordering is not an accessibility relation, so this node belongs to
|
|
756
|
+
the sphere semantics (:mod:`unicode_logic_kit.semantics.conditional`) rather than to
|
|
757
|
+
the Kripke evaluator, and it has no first-order export.
|
|
758
|
+
"""
|
|
759
|
+
|
|
760
|
+
left: Node
|
|
761
|
+
right: Node
|
|
762
|
+
|
|
763
|
+
def _tree_parts(self):
|
|
764
|
+
"""Return the □→ label and the antecedent/consequent children."""
|
|
765
|
+
return "□→", [self.left, self.right]
|
|
766
|
+
|
|
767
|
+
def to_dict(self):
|
|
768
|
+
"""Serialise to dict with type tag and recursively serialised operands."""
|
|
769
|
+
return {"_type": "Would", "left": self.left.to_dict(),
|
|
770
|
+
"right": self.right.to_dict()}
|
|
771
|
+
|
|
772
|
+
@staticmethod
|
|
773
|
+
def from_dict(d):
|
|
774
|
+
"""Deserialise a Would from a dict produced by to_dict."""
|
|
775
|
+
return Would(Node.from_dict(d["left"]), Node.from_dict(d["right"]))
|
|
776
|
+
|
|
777
|
+
def to_z3(self, env: Z3Env = None):
|
|
778
|
+
"""Reject Z3 export: the counterfactual is not first-order definable."""
|
|
779
|
+
raise NotImplementedError(_NO_COUNTERFACTUAL_EXPORT)
|
|
780
|
+
|
|
781
|
+
def to_prover9(self) -> str:
|
|
782
|
+
"""Reject Prover9 export: the counterfactual is not first-order definable."""
|
|
783
|
+
raise NotImplementedError(_NO_COUNTERFACTUAL_EXPORT)
|
|
784
|
+
|
|
785
|
+
def to_tptp(self) -> str:
|
|
786
|
+
"""Reject TPTP export: the counterfactual is not first-order definable."""
|
|
787
|
+
raise NotImplementedError(_NO_COUNTERFACTUAL_EXPORT)
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
@dataclass(frozen=True)
|
|
791
|
+
class Might(Node):
|
|
792
|
+
"""Lewis "might" counterfactual ``left ◇→ right`` — the dual ``¬(left □→ ¬right)``.
|
|
793
|
+
|
|
794
|
+
"If left were the case, right MIGHT be": some closest ``left``-world is a
|
|
795
|
+
``right``-world. Note the duality makes it **vacuously false** exactly where
|
|
796
|
+
:class:`Would` is vacuously true (no ``left``-world in any sphere).
|
|
797
|
+
"""
|
|
798
|
+
|
|
799
|
+
left: Node
|
|
800
|
+
right: Node
|
|
801
|
+
|
|
802
|
+
def _tree_parts(self):
|
|
803
|
+
"""Return the ◇→ label and the antecedent/consequent children."""
|
|
804
|
+
return "◇→", [self.left, self.right]
|
|
805
|
+
|
|
806
|
+
def to_dict(self):
|
|
807
|
+
"""Serialise to dict with type tag and recursively serialised operands."""
|
|
808
|
+
return {"_type": "Might", "left": self.left.to_dict(),
|
|
809
|
+
"right": self.right.to_dict()}
|
|
810
|
+
|
|
811
|
+
@staticmethod
|
|
812
|
+
def from_dict(d):
|
|
813
|
+
"""Deserialise a Might from a dict produced by to_dict."""
|
|
814
|
+
return Might(Node.from_dict(d["left"]), Node.from_dict(d["right"]))
|
|
815
|
+
|
|
816
|
+
def to_z3(self, env: Z3Env = None):
|
|
817
|
+
"""Reject Z3 export: the counterfactual is not first-order definable."""
|
|
818
|
+
raise NotImplementedError(_NO_COUNTERFACTUAL_EXPORT)
|
|
819
|
+
|
|
820
|
+
def to_prover9(self) -> str:
|
|
821
|
+
"""Reject Prover9 export: the counterfactual is not first-order definable."""
|
|
822
|
+
raise NotImplementedError(_NO_COUNTERFACTUAL_EXPORT)
|
|
823
|
+
|
|
824
|
+
def to_tptp(self) -> str:
|
|
825
|
+
"""Reject TPTP export: the counterfactual is not first-order definable."""
|
|
826
|
+
raise NotImplementedError(_NO_COUNTERFACTUAL_EXPORT)
|
|
827
|
+
|
|
828
|
+
|
|
829
|
+
@dataclass(frozen=True)
|
|
830
|
+
class Obligatory(Node):
|
|
831
|
+
"""Deontic necessity Oφ: φ holds in every deontically accessible world.
|
|
832
|
+
|
|
833
|
+
The obligation operator of Standard Deontic Logic (the modal system KD): a
|
|
834
|
+
□-style box over a SERIAL "deontic" accessibility relation. Seriality gives
|
|
835
|
+
the characteristic D axiom ``Oφ → Pφ`` (whatever is obligatory is permitted).
|
|
836
|
+
"""
|
|
837
|
+
|
|
838
|
+
formula: Node
|
|
839
|
+
|
|
840
|
+
def _tree_parts(self):
|
|
841
|
+
"""Return the Ⓞ label and the single subformula child."""
|
|
842
|
+
return "Ⓞ", [self.formula]
|
|
843
|
+
|
|
844
|
+
def to_dict(self):
|
|
845
|
+
"""Serialise to dict with type tag and recursively serialised subformula."""
|
|
846
|
+
return {"_type": "Obligatory", "formula": self.formula.to_dict()}
|
|
847
|
+
|
|
848
|
+
@staticmethod
|
|
849
|
+
def from_dict(d):
|
|
850
|
+
"""Deserialise an Obligatory from a dict produced by to_dict."""
|
|
851
|
+
return Obligatory(Node.from_dict(d["formula"]))
|
|
852
|
+
|
|
853
|
+
def to_z3(self, env: Z3Env = None):
|
|
854
|
+
"""Reject Z3 export: modal operators have no direct first-order encoding."""
|
|
855
|
+
raise NotImplementedError(_NO_EXPORT)
|
|
856
|
+
|
|
857
|
+
def to_prover9(self) -> str:
|
|
858
|
+
"""Reject Prover9 export: modal operators have no direct first-order encoding."""
|
|
859
|
+
raise NotImplementedError(_NO_EXPORT)
|
|
860
|
+
|
|
861
|
+
def to_tptp(self) -> str:
|
|
862
|
+
"""Reject TPTP export: modal operators have no direct first-order encoding."""
|
|
863
|
+
raise NotImplementedError(_NO_EXPORT)
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
@dataclass(frozen=True)
|
|
867
|
+
class Permitted(Node):
|
|
868
|
+
"""Deontic possibility Pφ: φ holds in some deontically accessible world.
|
|
869
|
+
|
|
870
|
+
The permission operator of Standard Deontic Logic (the modal system KD): a
|
|
871
|
+
◇-style diamond over the SERIAL "deontic" accessibility relation, dual to
|
|
872
|
+
:class:`Obligatory` (``Pφ ≡ ¬O¬φ``). Prohibition ``Fφ ≡ ¬Pφ ≡ O¬φ`` is
|
|
873
|
+
derived and has no dedicated node.
|
|
874
|
+
"""
|
|
875
|
+
|
|
876
|
+
formula: Node
|
|
877
|
+
|
|
878
|
+
def _tree_parts(self):
|
|
879
|
+
"""Return the Ⓟ label and the single subformula child."""
|
|
880
|
+
return "Ⓟ", [self.formula]
|
|
881
|
+
|
|
882
|
+
def to_dict(self):
|
|
883
|
+
"""Serialise to dict with type tag and recursively serialised subformula."""
|
|
884
|
+
return {"_type": "Permitted", "formula": self.formula.to_dict()}
|
|
885
|
+
|
|
886
|
+
@staticmethod
|
|
887
|
+
def from_dict(d):
|
|
888
|
+
"""Deserialise a Permitted from a dict produced by to_dict."""
|
|
889
|
+
return Permitted(Node.from_dict(d["formula"]))
|
|
890
|
+
|
|
891
|
+
def to_z3(self, env: Z3Env = None):
|
|
892
|
+
"""Reject Z3 export: modal operators have no direct first-order encoding."""
|
|
893
|
+
raise NotImplementedError(_NO_EXPORT)
|
|
894
|
+
|
|
895
|
+
def to_prover9(self) -> str:
|
|
896
|
+
"""Reject Prover9 export: modal operators have no direct first-order encoding."""
|
|
897
|
+
raise NotImplementedError(_NO_EXPORT)
|
|
898
|
+
|
|
899
|
+
def to_tptp(self) -> str:
|
|
900
|
+
"""Reject TPTP export: modal operators have no direct first-order encoding."""
|
|
901
|
+
raise NotImplementedError(_NO_EXPORT)
|
|
902
|
+
|
|
903
|
+
|
|
904
|
+
@dataclass(frozen=True)
|
|
905
|
+
class Historically(Node):
|
|
906
|
+
"""Past-tense "has always been" Hφ: φ holds now and at every PAST point.
|
|
907
|
+
|
|
908
|
+
The past dual of :class:`Always` (G): a □ over the *converse* of the one-step
|
|
909
|
+
``"temporal"`` relation, i.e. over the reflexive-transitive set of points from
|
|
910
|
+
which the current world is reachable. Rendered with the parenthesised glyph
|
|
911
|
+
``⒣`` — the past operators are parenthesised so they read apart from the circled
|
|
912
|
+
future operators (Ⓖ Ⓕ Ⓝ Ⓤ).
|
|
913
|
+
"""
|
|
914
|
+
|
|
915
|
+
formula: Node
|
|
916
|
+
|
|
917
|
+
def _tree_parts(self):
|
|
918
|
+
"""Return the ⒣ label and the single subformula child."""
|
|
919
|
+
return "⒣", [self.formula]
|
|
920
|
+
|
|
921
|
+
def to_dict(self):
|
|
922
|
+
"""Serialise to dict with type tag and recursively serialised subformula."""
|
|
923
|
+
return {"_type": "Historically", "formula": self.formula.to_dict()}
|
|
924
|
+
|
|
925
|
+
@staticmethod
|
|
926
|
+
def from_dict(d):
|
|
927
|
+
"""Deserialise a Historically from a dict produced by to_dict."""
|
|
928
|
+
return Historically(Node.from_dict(d["formula"]))
|
|
929
|
+
|
|
930
|
+
def to_z3(self, env: Z3Env = None):
|
|
931
|
+
"""Reject Z3 export: modal operators have no direct first-order encoding."""
|
|
932
|
+
raise NotImplementedError(_NO_EXPORT)
|
|
933
|
+
|
|
934
|
+
def to_prover9(self) -> str:
|
|
935
|
+
"""Reject Prover9 export: modal operators have no direct first-order encoding."""
|
|
936
|
+
raise NotImplementedError(_NO_EXPORT)
|
|
937
|
+
|
|
938
|
+
def to_tptp(self) -> str:
|
|
939
|
+
"""Reject TPTP export: modal operators have no direct first-order encoding."""
|
|
940
|
+
raise NotImplementedError(_NO_EXPORT)
|
|
941
|
+
|
|
942
|
+
|
|
943
|
+
@dataclass(frozen=True)
|
|
944
|
+
class Once(Node):
|
|
945
|
+
"""Past-tense "was once the case" Pφ: φ held now or at some PAST point.
|
|
946
|
+
|
|
947
|
+
The past dual of :class:`Eventually` (F): a ◇ over the converse of the one-step
|
|
948
|
+
``"temporal"`` relation. Rendered with the parenthesised glyph ``⒫``.
|
|
949
|
+
"""
|
|
950
|
+
|
|
951
|
+
formula: Node
|
|
952
|
+
|
|
953
|
+
def _tree_parts(self):
|
|
954
|
+
"""Return the ⒫ label and the single subformula child."""
|
|
955
|
+
return "⒫", [self.formula]
|
|
956
|
+
|
|
957
|
+
def to_dict(self):
|
|
958
|
+
"""Serialise to dict with type tag and recursively serialised subformula."""
|
|
959
|
+
return {"_type": "Once", "formula": self.formula.to_dict()}
|
|
960
|
+
|
|
961
|
+
@staticmethod
|
|
962
|
+
def from_dict(d):
|
|
963
|
+
"""Deserialise an Once from a dict produced by to_dict."""
|
|
964
|
+
return Once(Node.from_dict(d["formula"]))
|
|
965
|
+
|
|
966
|
+
def to_z3(self, env: Z3Env = None):
|
|
967
|
+
"""Reject Z3 export: modal operators have no direct first-order encoding."""
|
|
968
|
+
raise NotImplementedError(_NO_EXPORT)
|
|
969
|
+
|
|
970
|
+
def to_prover9(self) -> str:
|
|
971
|
+
"""Reject Prover9 export: modal operators have no direct first-order encoding."""
|
|
972
|
+
raise NotImplementedError(_NO_EXPORT)
|
|
973
|
+
|
|
974
|
+
def to_tptp(self) -> str:
|
|
975
|
+
"""Reject TPTP export: modal operators have no direct first-order encoding."""
|
|
976
|
+
raise NotImplementedError(_NO_EXPORT)
|
|
977
|
+
|
|
978
|
+
|
|
979
|
+
@dataclass(frozen=True)
|
|
980
|
+
class Previous(Node):
|
|
981
|
+
"""Past-tense "yesterday" Yφ: φ holds at every immediate PAST point.
|
|
982
|
+
|
|
983
|
+
The past dual of :class:`Next` (X): the □-style operator over the converse of
|
|
984
|
+
the one-step ``"temporal"`` relation (universal over immediate predecessors, so
|
|
985
|
+
vacuously true at a world with no past). Rendered with the parenthesised glyph
|
|
986
|
+
``⒴``.
|
|
987
|
+
"""
|
|
988
|
+
|
|
989
|
+
formula: Node
|
|
990
|
+
|
|
991
|
+
def _tree_parts(self):
|
|
992
|
+
"""Return the ⒴ label and the single subformula child."""
|
|
993
|
+
return "⒴", [self.formula]
|
|
994
|
+
|
|
995
|
+
def to_dict(self):
|
|
996
|
+
"""Serialise to dict with type tag and recursively serialised subformula."""
|
|
997
|
+
return {"_type": "Previous", "formula": self.formula.to_dict()}
|
|
998
|
+
|
|
999
|
+
@staticmethod
|
|
1000
|
+
def from_dict(d):
|
|
1001
|
+
"""Deserialise a Previous from a dict produced by to_dict."""
|
|
1002
|
+
return Previous(Node.from_dict(d["formula"]))
|
|
1003
|
+
|
|
1004
|
+
def to_z3(self, env: Z3Env = None):
|
|
1005
|
+
"""Reject Z3 export: modal operators have no direct first-order encoding."""
|
|
1006
|
+
raise NotImplementedError(_NO_EXPORT)
|
|
1007
|
+
|
|
1008
|
+
def to_prover9(self) -> str:
|
|
1009
|
+
"""Reject Prover9 export: modal operators have no direct first-order encoding."""
|
|
1010
|
+
raise NotImplementedError(_NO_EXPORT)
|
|
1011
|
+
|
|
1012
|
+
def to_tptp(self) -> str:
|
|
1013
|
+
"""Reject TPTP export: modal operators have no direct first-order encoding."""
|
|
1014
|
+
raise NotImplementedError(_NO_EXPORT)
|
|
1015
|
+
|
|
1016
|
+
|
|
1017
|
+
#: Why the PAL announcement operators have no direct first-order (or classical
|
|
1018
|
+
#: Kripke) export: they denote a MODEL UPDATE, not a static accessibility relation.
|
|
1019
|
+
_NO_PAL_EXPORT = (
|
|
1020
|
+
"Public-announcement operators ([φ!]ψ / ⟨φ!⟩ψ) denote a MODEL UPDATE (restrict "
|
|
1021
|
+
"the Kripke model to the worlds where the announcement is true), not a static "
|
|
1022
|
+
"accessibility relation, so they have no direct first-order (or classical "
|
|
1023
|
+
"Kripke) export. Eliminate them first with "
|
|
1024
|
+
"unicode_logic_kit.fol.pal.reduce_announcements (the standard PAL reduction "
|
|
1025
|
+
"axioms, producing an announcement-free modal formula), or evaluate them "
|
|
1026
|
+
"directly with semantics.kripke.satisfies_modal / "
|
|
1027
|
+
"semantics.dynamic_epistemic.announce."
|
|
1028
|
+
)
|
|
1029
|
+
|
|
1030
|
+
|
|
1031
|
+
def _pal_wrap(node: Node, min_prec: float, render) -> str:
|
|
1032
|
+
"""Parenthesise ``render(node)`` when ``node`` binds looser than ``min_prec``.
|
|
1033
|
+
|
|
1034
|
+
Mirrors ``_msfl_nodes._uni_wrap`` / ``_latex_wrap``'s parenthesisation rule
|
|
1035
|
+
(reusing the SAME precedence table, via the read-only helper ``_uni_prec``)
|
|
1036
|
+
but recurses through the CHILD's own ``to_unicode_str`` / ``to_latex`` method
|
|
1037
|
+
rather than the module-level ``_uni`` / ``_latex`` dispatcher — the
|
|
1038
|
+
announcements' bracket-delimited two-argument shape fits none of the six
|
|
1039
|
+
registered operator fixities, so they render via these overrides; the
|
|
1040
|
+
central dispatcher delegates back here for announcement CHILDREN (see the
|
|
1041
|
+
"RENDERING" paragraph on :class:`Announce`).
|
|
1042
|
+
"""
|
|
1043
|
+
from ._msfl_nodes import _uni_prec # lazy: avoid a module-load-order cycle
|
|
1044
|
+
text = render(node)
|
|
1045
|
+
return f"({text})" if _uni_prec(node) < min_prec else text
|
|
1046
|
+
|
|
1047
|
+
|
|
1048
|
+
@dataclass(frozen=True)
|
|
1049
|
+
class Announce(Node):
|
|
1050
|
+
"""Public-announcement box ``[announcement!]formula`` (PAL): after a truthful
|
|
1051
|
+
public announcement of ``announcement``, ``formula`` holds.
|
|
1052
|
+
|
|
1053
|
+
``M, w ⊨ [φ!]ψ`` iff ``M, w ⊨ φ`` implies ``M|φ, w ⊨ ψ``, where ``M|φ`` is the
|
|
1054
|
+
model restricted to the worlds satisfying ``φ``
|
|
1055
|
+
(:func:`unicode_logic_kit.semantics.dynamic_epistemic.announce`), which
|
|
1056
|
+
:func:`unicode_logic_kit.semantics.kripke.satisfies_modal` calls directly for
|
|
1057
|
+
this node — the ORACLE that
|
|
1058
|
+
:func:`unicode_logic_kit.fol.pal.reduce_announcements` (the syntactic
|
|
1059
|
+
reduction to an announcement-free modal formula) is differentially tested
|
|
1060
|
+
against. Vacuously true when ``announcement`` is false at the evaluation
|
|
1061
|
+
world (an untruthful announcement is not made).
|
|
1062
|
+
|
|
1063
|
+
Both fields are formulas. ``announcement`` (φ) is delimited by the surface
|
|
1064
|
+
brackets ``[ … !]`` and so parses at FULL formula precedence — exactly like
|
|
1065
|
+
the base grammar's own ``"(" formula ")"`` grouping or Cardinality's
|
|
1066
|
+
``"|{" v ":" φ "}|"``. ``formula`` (ψ), the post-condition following the
|
|
1067
|
+
closing bracket, parses at the PREFIX level (as tightly as ¬ / □ / K_a): a
|
|
1068
|
+
deliberate design choice mirroring how every other modal operator in this
|
|
1069
|
+
module binds, so ``[p!]q ∧ r`` groups as ``([p!]q) ∧ r``, not
|
|
1070
|
+
``[p!](q ∧ r)`` — see the parser-registration comment below.
|
|
1071
|
+
|
|
1072
|
+
RENDERING: ``to_unicode_str`` / ``to_latex`` are direct method overrides on
|
|
1073
|
+
this class, NOT driven by ``register_operator``'s central registry. Each of
|
|
1074
|
+
the registry's six fixities (prefix, agent_prefix, binary_iff,
|
|
1075
|
+
binary_implies, binary_until, level2) is a single FIXED template string, and
|
|
1076
|
+
none can express "bracket, formula, delimiter, bracket, ANOTHER formula" —
|
|
1077
|
+
two independent operands with the second one OUTSIDE the delimiters. The
|
|
1078
|
+
override recurses via each child's own ``to_unicode_str`` / ``to_latex``,
|
|
1079
|
+
and the MODULE-LEVEL ``_msfl_nodes._uni`` / ``_latex`` dispatchers carry a
|
|
1080
|
+
matching delegation case (plus prefix-level precedence entries in
|
|
1081
|
+
``_UNI_BASE_PREC``), so an Announce/AnnounceDiamond renders — and
|
|
1082
|
+
round-trips — both as the outer node and nested at any depth under any
|
|
1083
|
+
registry-driven operator (∧ / → / ¬ / □ / K_a / …); see
|
|
1084
|
+
``tests/test_pal.py``.
|
|
1085
|
+
"""
|
|
1086
|
+
|
|
1087
|
+
announcement: Node
|
|
1088
|
+
formula: Node
|
|
1089
|
+
|
|
1090
|
+
def _tree_parts(self):
|
|
1091
|
+
"""Return the "[!]" label and the [announcement, formula] children."""
|
|
1092
|
+
return "[!]", [self.announcement, self.formula]
|
|
1093
|
+
|
|
1094
|
+
def to_dict(self):
|
|
1095
|
+
"""Serialise to dict with type tag and both serialised operands."""
|
|
1096
|
+
return {"_type": "Announce", "announcement": self.announcement.to_dict(),
|
|
1097
|
+
"formula": self.formula.to_dict()}
|
|
1098
|
+
|
|
1099
|
+
@staticmethod
|
|
1100
|
+
def from_dict(d):
|
|
1101
|
+
"""Deserialise an Announce from a dict produced by to_dict."""
|
|
1102
|
+
return Announce(Node.from_dict(d["announcement"]), Node.from_dict(d["formula"]))
|
|
1103
|
+
|
|
1104
|
+
def to_unicode_str(self) -> str:
|
|
1105
|
+
"""Render as ``[announcement!]formula`` (see the class docstring)."""
|
|
1106
|
+
post = _pal_wrap(self.formula, 4, lambda n: n.to_unicode_str())
|
|
1107
|
+
return f"[{self.announcement.to_unicode_str()}!]{post}"
|
|
1108
|
+
|
|
1109
|
+
def to_latex(self) -> str:
|
|
1110
|
+
"""Render as LaTeX ``[announcement\\mathbin{!}]formula``."""
|
|
1111
|
+
post = _pal_wrap(self.formula, 4, lambda n: n.to_latex())
|
|
1112
|
+
return f"[{self.announcement.to_latex()}\\mathbin{{!}}]{post}"
|
|
1113
|
+
|
|
1114
|
+
def to_z3(self, env: Z3Env = None):
|
|
1115
|
+
"""Reject Z3 export: a model update has no static first-order encoding."""
|
|
1116
|
+
raise NotImplementedError(_NO_PAL_EXPORT)
|
|
1117
|
+
|
|
1118
|
+
def to_prover9(self) -> str:
|
|
1119
|
+
"""Reject Prover9 export: a model update has no static first-order encoding."""
|
|
1120
|
+
raise NotImplementedError(_NO_PAL_EXPORT)
|
|
1121
|
+
|
|
1122
|
+
def to_tptp(self) -> str:
|
|
1123
|
+
"""Reject TPTP export: a model update has no static first-order encoding."""
|
|
1124
|
+
raise NotImplementedError(_NO_PAL_EXPORT)
|
|
1125
|
+
|
|
1126
|
+
|
|
1127
|
+
@dataclass(frozen=True)
|
|
1128
|
+
class AnnounceDiamond(Node):
|
|
1129
|
+
"""Public-announcement diamond ``⟨announcement!⟩formula`` (PAL): the truthful
|
|
1130
|
+
dual of :class:`Announce` — ``announcement`` is (actually) true AND, after
|
|
1131
|
+
announcing it, ``formula`` holds.
|
|
1132
|
+
|
|
1133
|
+
``M, w ⊨ ⟨φ!⟩ψ`` iff ``M, w ⊨ φ`` AND ``M|φ, w ⊨ ψ``. See :class:`Announce`
|
|
1134
|
+
for the shared oracle / precedence / rendering contract; the surface syntax
|
|
1135
|
+
swaps square brackets for angle brackets (``⟨ … !⟩``, U+27E8/U+27E9) — a
|
|
1136
|
+
glyph pair unused elsewhere in the grammar, so unlike ``Announce``'s ``[``/``]``
|
|
1137
|
+
it needs no disambiguation against another rule at all.
|
|
1138
|
+
"""
|
|
1139
|
+
|
|
1140
|
+
announcement: Node
|
|
1141
|
+
formula: Node
|
|
1142
|
+
|
|
1143
|
+
def _tree_parts(self):
|
|
1144
|
+
"""Return the "⟨!⟩" label and the [announcement, formula] children."""
|
|
1145
|
+
return "⟨!⟩", [self.announcement, self.formula]
|
|
1146
|
+
|
|
1147
|
+
def to_dict(self):
|
|
1148
|
+
"""Serialise to dict with type tag and both serialised operands."""
|
|
1149
|
+
return {"_type": "AnnounceDiamond", "announcement": self.announcement.to_dict(),
|
|
1150
|
+
"formula": self.formula.to_dict()}
|
|
1151
|
+
|
|
1152
|
+
@staticmethod
|
|
1153
|
+
def from_dict(d):
|
|
1154
|
+
"""Deserialise an AnnounceDiamond from a dict produced by to_dict."""
|
|
1155
|
+
return AnnounceDiamond(Node.from_dict(d["announcement"]), Node.from_dict(d["formula"]))
|
|
1156
|
+
|
|
1157
|
+
def to_unicode_str(self) -> str:
|
|
1158
|
+
"""Render as ``⟨announcement!⟩formula`` (see :class:`Announce`)."""
|
|
1159
|
+
post = _pal_wrap(self.formula, 4, lambda n: n.to_unicode_str())
|
|
1160
|
+
return f"⟨{self.announcement.to_unicode_str()}!⟩{post}"
|
|
1161
|
+
|
|
1162
|
+
def to_latex(self) -> str:
|
|
1163
|
+
"""Render as LaTeX ``\\langle announcement\\mathbin{!}\\rangle formula``."""
|
|
1164
|
+
post = _pal_wrap(self.formula, 4, lambda n: n.to_latex())
|
|
1165
|
+
return f"\\langle {self.announcement.to_latex()}\\mathbin{{!}}\\rangle {post}"
|
|
1166
|
+
|
|
1167
|
+
def to_z3(self, env: Z3Env = None):
|
|
1168
|
+
"""Reject Z3 export: a model update has no static first-order encoding."""
|
|
1169
|
+
raise NotImplementedError(_NO_PAL_EXPORT)
|
|
1170
|
+
|
|
1171
|
+
def to_prover9(self) -> str:
|
|
1172
|
+
"""Reject Prover9 export: a model update has no static first-order encoding."""
|
|
1173
|
+
raise NotImplementedError(_NO_PAL_EXPORT)
|
|
1174
|
+
|
|
1175
|
+
def to_tptp(self) -> str:
|
|
1176
|
+
"""Reject TPTP export: a model update has no static first-order encoding."""
|
|
1177
|
+
raise NotImplementedError(_NO_PAL_EXPORT)
|
|
1178
|
+
|
|
1179
|
+
|
|
1180
|
+
NODE_CLASSES.update({"Announce": Announce, "AnnounceDiamond": AnnounceDiamond})
|
|
1181
|
+
|
|
1182
|
+
|
|
1183
|
+
@dataclass(frozen=True)
|
|
1184
|
+
class Since(Node):
|
|
1185
|
+
"""Past-tense "left since right" (left S right): the mirror of :class:`Until`.
|
|
1186
|
+
|
|
1187
|
+
Holds at the current point iff ``right`` was true at some past point and ``left``
|
|
1188
|
+
has held at every point strictly since then up to now — the exact backward dual
|
|
1189
|
+
of strong Until over the one-step ``"temporal"`` relation. Rendered ``⒮``.
|
|
1190
|
+
"""
|
|
1191
|
+
|
|
1192
|
+
left: Node
|
|
1193
|
+
right: Node
|
|
1194
|
+
|
|
1195
|
+
def _tree_parts(self):
|
|
1196
|
+
"""Return the ⒮ label and the two subformula children."""
|
|
1197
|
+
return "⒮", [self.left, self.right]
|
|
1198
|
+
|
|
1199
|
+
def to_dict(self):
|
|
1200
|
+
"""Serialise to dict with type tag and recursively serialised operands."""
|
|
1201
|
+
return {"_type": "Since", "left": self.left.to_dict(), "right": self.right.to_dict()}
|
|
1202
|
+
|
|
1203
|
+
@staticmethod
|
|
1204
|
+
def from_dict(d):
|
|
1205
|
+
"""Deserialise a Since from a dict produced by to_dict."""
|
|
1206
|
+
return Since(Node.from_dict(d["left"]), Node.from_dict(d["right"]))
|
|
1207
|
+
|
|
1208
|
+
def to_z3(self, env: Z3Env = None):
|
|
1209
|
+
"""Reject Z3 export: modal operators have no direct first-order encoding."""
|
|
1210
|
+
raise NotImplementedError(_NO_EXPORT)
|
|
1211
|
+
|
|
1212
|
+
def to_prover9(self) -> str:
|
|
1213
|
+
"""Reject Prover9 export: modal operators have no direct first-order encoding."""
|
|
1214
|
+
raise NotImplementedError(_NO_EXPORT)
|
|
1215
|
+
|
|
1216
|
+
def to_tptp(self) -> str:
|
|
1217
|
+
"""Reject TPTP export: modal operators have no direct first-order encoding."""
|
|
1218
|
+
raise NotImplementedError(_NO_EXPORT)
|
|
1219
|
+
|
|
1220
|
+
|
|
1221
|
+
# =========================
|
|
1222
|
+
# Operator registration
|
|
1223
|
+
# =========================
|
|
1224
|
+
#
|
|
1225
|
+
# Self-register the modal/epistemic/doxastic/temporal/deontic operators with the
|
|
1226
|
+
# central renderers. The prefix ops bind as tightly as ¬ (precedence 4); the
|
|
1227
|
+
# latex markup carries the exact trailing space the renderer emits. Knows/Believes
|
|
1228
|
+
# are agent_prefix (K_<agent> / K_{<agent>}); Until is the binary temporal
|
|
1229
|
+
# operator at precedence 2.5. register_operator also adds each class to
|
|
1230
|
+
# NODE_CLASSES, so no separate NODE_CLASSES.update is needed.
|
|
1231
|
+
|
|
1232
|
+
register_operator(Box, "prefix", "□", "\\Box ", 4)
|
|
1233
|
+
register_operator(Diamond, "prefix", "◇", "\\Diamond ", 4)
|
|
1234
|
+
register_operator(Always, "prefix", "Ⓖ", "\\mathsf{G} ", 4)
|
|
1235
|
+
register_operator(Eventually, "prefix", "Ⓕ", "\\mathsf{F} ", 4)
|
|
1236
|
+
register_operator(Next, "prefix", "Ⓝ", "\\mathsf{X} ", 4)
|
|
1237
|
+
register_operator(Obligatory, "prefix", "Ⓞ", "\\mathsf{O} ", 4)
|
|
1238
|
+
register_operator(Permitted, "prefix", "Ⓟ", "\\mathsf{P} ", 4)
|
|
1239
|
+
register_operator(Knows, "agent_prefix", "K_", "K", 4)
|
|
1240
|
+
register_operator(Believes, "agent_prefix", "B_", "B", 4)
|
|
1241
|
+
register_operator(Says, "agent_prefix", "Say_", "\\mathsf{Say}", 4)
|
|
1242
|
+
register_operator(Wants, "agent_prefix", "Want_", "\\mathsf{Want}", 4)
|
|
1243
|
+
register_operator(Until, "binary_until", "Ⓤ", "\\mathbin{\\mathsf{U}}", 2.5)
|
|
1244
|
+
# Past-tense (parenthesised glyphs to read apart from the circled future
|
|
1245
|
+
# operators; LaTeX overlined so ⒫ never collides with the deontic \mathsf{P}=Ⓟ).
|
|
1246
|
+
register_operator(Historically, "prefix", "⒣", "\\overline{\\mathsf{H}} ", 4)
|
|
1247
|
+
register_operator(Once, "prefix", "⒫", "\\overline{\\mathsf{P}} ", 4)
|
|
1248
|
+
register_operator(Previous, "prefix", "⒴", "\\overline{\\mathsf{Y}} ", 4)
|
|
1249
|
+
register_operator(Since, "binary_until", "⒮", "\\mathbin{\\overline{\\mathsf{S}}}", 2.5)
|
|
1250
|
+
# Counterfactuals sit at the Until level (2.5): tighter than → and ↔, looser than
|
|
1251
|
+
# ∧/∨, so `P ∧ Q □→ R` groups as `(P ∧ Q) □→ R` — the reading the English has.
|
|
1252
|
+
register_operator(Would, "binary_until", "□→", "\\mathbin{\\Box\\!\\rightarrow}", 2.5)
|
|
1253
|
+
register_operator(Might, "binary_until", "◇→", "\\mathbin{\\Diamond\\!\\rightarrow}", 2.5)
|
|
1254
|
+
|
|
1255
|
+
|
|
1256
|
+
# =========================
|
|
1257
|
+
# Parser registration (modal mode)
|
|
1258
|
+
# =========================
|
|
1259
|
+
#
|
|
1260
|
+
# Self-register the modal/epistemic/doxastic/temporal/deontic operators with the
|
|
1261
|
+
# PARSER registry so MSFLParser assembles the modal grammar + transformer from the
|
|
1262
|
+
# registry alone — there is no hand-written modal transformer or modal grammar file:
|
|
1263
|
+
#
|
|
1264
|
+
# * The prefix operators (□ ◇ Ⓖ Ⓕ Ⓝ K_a B_a Ⓞ Ⓟ) sit at the ¬ (prefix) level —
|
|
1265
|
+
# ``OP prefix -> alias`` — binding as tightly as negation. The classical ¬
|
|
1266
|
+
# (not_) is registered for "modal" in _fol_nodes.py; these add to it.
|
|
1267
|
+
# * Until (Ⓤ) is the binary right-assoc level between the same_level group and →;
|
|
1268
|
+
# registering ANY "until"-level op makes build_grammar route the → level's
|
|
1269
|
+
# left operand through the until rule (impl_body = "until"), giving the
|
|
1270
|
+
# ``?implication: until`` layering. Its grammar field is the bare TUNTIL
|
|
1271
|
+
# terminal; build_grammar wraps it as ``same_level_ops TUNTIL until``.
|
|
1272
|
+
# * Knows/Believes carry their agent in the matched token (e.g. "K_alice"); the
|
|
1273
|
+
# transform strips the two-character "K_"/"B_" prefix.
|
|
1274
|
+
#
|
|
1275
|
+
# Named terminals (with their lexer priorities) are declared via terminal_def so
|
|
1276
|
+
# the generated grammar lexes K_a / B_a as KNOWS/BELIEVES (priority 5) ahead of
|
|
1277
|
+
# PREDICATE (terminals come from terminals.lark).
|
|
1278
|
+
|
|
1279
|
+
# --- prefix modal/temporal/deontic operators (¬-level) ---
|
|
1280
|
+
register_parser_op(Box, "modal", "prefix", "box_", "BOX prefix",
|
|
1281
|
+
lambda items: Box(items[1]),
|
|
1282
|
+
terminal_name="BOX", terminal_def='BOX: "□"')
|
|
1283
|
+
register_parser_op(Diamond, "modal", "prefix", "diamond_", "DIAMOND prefix",
|
|
1284
|
+
lambda items: Diamond(items[1]),
|
|
1285
|
+
terminal_name="DIAMOND", terminal_def='DIAMOND: "◇"')
|
|
1286
|
+
register_parser_op(Always, "modal", "prefix", "always_", "TALWAYS prefix",
|
|
1287
|
+
lambda items: Always(items[1]),
|
|
1288
|
+
terminal_name="TALWAYS", terminal_def='TALWAYS: "Ⓖ"')
|
|
1289
|
+
register_parser_op(Eventually, "modal", "prefix", "eventually_", "TEVENTUALLY prefix",
|
|
1290
|
+
lambda items: Eventually(items[1]),
|
|
1291
|
+
terminal_name="TEVENTUALLY", terminal_def='TEVENTUALLY: "Ⓕ"')
|
|
1292
|
+
register_parser_op(Next, "modal", "prefix", "next_", "TNEXT prefix",
|
|
1293
|
+
lambda items: Next(items[1]),
|
|
1294
|
+
terminal_name="TNEXT", terminal_def='TNEXT: "Ⓝ"')
|
|
1295
|
+
register_parser_op(Knows, "modal", "prefix", "knows_", "KNOWS prefix",
|
|
1296
|
+
lambda items: Knows(parse_agent_token(items[0]), items[1]),
|
|
1297
|
+
terminal_name="KNOWS", terminal_def='KNOWS.5: /K_[a-z][a-zA-Z0-9]*/')
|
|
1298
|
+
register_parser_op(Believes, "modal", "prefix", "believes_", "BELIEVES prefix",
|
|
1299
|
+
lambda items: Believes(parse_agent_token(items[0]), items[1]),
|
|
1300
|
+
terminal_name="BELIEVES", terminal_def='BELIEVES.5: /B_[a-z][a-zA-Z0-9]*/')
|
|
1301
|
+
# Assertive Say_<agent> / bouletic Want_<agent>: agent_prefix like Knows/Believes.
|
|
1302
|
+
# The "Say_"/"Want_" prefix is 4/5 characters; parse_agent_token strips it to get
|
|
1303
|
+
# the agent name. Priority 5 so the token wins over PREDICATE (Say_alice, Want_x).
|
|
1304
|
+
register_parser_op(Says, "modal", "prefix", "says_", "SAYS prefix",
|
|
1305
|
+
lambda items: Says(parse_agent_token(items[0], 4), items[1]),
|
|
1306
|
+
terminal_name="SAYS", terminal_def='SAYS.5: /Say_[a-z][a-zA-Z0-9]*/')
|
|
1307
|
+
register_parser_op(Wants, "modal", "prefix", "wants_", "WANTS prefix",
|
|
1308
|
+
lambda items: Wants(parse_agent_token(items[0], 5), items[1]),
|
|
1309
|
+
terminal_name="WANTS", terminal_def='WANTS.5: /Want_[a-z][a-zA-Z0-9]*/')
|
|
1310
|
+
register_parser_op(Obligatory, "modal", "prefix", "obligatory_", "OBLIG prefix",
|
|
1311
|
+
lambda items: Obligatory(items[1]),
|
|
1312
|
+
terminal_name="OBLIG", terminal_def='OBLIG: "Ⓞ"')
|
|
1313
|
+
register_parser_op(Permitted, "modal", "prefix", "permitted_", "PERMIT prefix",
|
|
1314
|
+
lambda items: Permitted(items[1]),
|
|
1315
|
+
terminal_name="PERMIT", terminal_def='PERMIT: "Ⓟ"')
|
|
1316
|
+
|
|
1317
|
+
# --- binary temporal Until (Ⓤ): same_level_ops TUNTIL until -> until_ ---
|
|
1318
|
+
# items = [left, TUNTIL token, right]; the token is the named terminal (kept in
|
|
1319
|
+
# the item list), so the right operand is items[2].
|
|
1320
|
+
register_parser_op(Until, "modal", "until", "until_", "TUNTIL",
|
|
1321
|
+
lambda items: Until(items[0], items[2]),
|
|
1322
|
+
terminal_name="TUNTIL", terminal_def='TUNTIL: "Ⓤ"')
|
|
1323
|
+
|
|
1324
|
+
# --- past-tense prefix operators (¬-level) ---
|
|
1325
|
+
register_parser_op(Historically, "modal", "prefix", "historically_", "PAST_H prefix",
|
|
1326
|
+
lambda items: Historically(items[1]),
|
|
1327
|
+
terminal_name="PAST_H", terminal_def='PAST_H: "⒣"')
|
|
1328
|
+
register_parser_op(Once, "modal", "prefix", "once_", "PAST_P prefix",
|
|
1329
|
+
lambda items: Once(items[1]),
|
|
1330
|
+
terminal_name="PAST_P", terminal_def='PAST_P: "⒫"')
|
|
1331
|
+
register_parser_op(Previous, "modal", "prefix", "previous_", "PAST_Y prefix",
|
|
1332
|
+
lambda items: Previous(items[1]),
|
|
1333
|
+
terminal_name="PAST_Y", terminal_def='PAST_Y: "⒴"')
|
|
1334
|
+
|
|
1335
|
+
# --- past-tense binary Since (⒮): same until-level as Ⓤ ---
|
|
1336
|
+
register_parser_op(Since, "modal", "until", "since_", "TSINCE",
|
|
1337
|
+
lambda items: Since(items[0], items[2]),
|
|
1338
|
+
terminal_name="TSINCE", terminal_def='TSINCE: "⒮"')
|
|
1339
|
+
|
|
1340
|
+
# --- counterfactual conditionals (□→ / ◇→) --------------------------------
|
|
1341
|
+
# Explicit terminal priority: the glyphs START with the box/diamond glyphs, so
|
|
1342
|
+
# without it "□→" could lex as BOX followed by the implication arrow and the
|
|
1343
|
+
# counterfactual would silently parse as a modalised material conditional.
|
|
1344
|
+
register_parser_op(Would, "modal", "until", "would_", "CFWOULD",
|
|
1345
|
+
lambda items: Would(items[0], items[2]),
|
|
1346
|
+
terminal_name="CFWOULD", terminal_def='CFWOULD.5: "□→"')
|
|
1347
|
+
register_parser_op(Might, "modal", "until", "might_", "CFMIGHT",
|
|
1348
|
+
lambda items: Might(items[0], items[2]),
|
|
1349
|
+
terminal_name="CFMIGHT", terminal_def='CFMIGHT.5: "◇→"')
|
|
1350
|
+
|
|
1351
|
+
# --- Public Announcement Logic (PAL): [φ!]ψ / ⟨φ!⟩ψ -----------------------
|
|
1352
|
+
#
|
|
1353
|
+
# Parser-ONLY registration: unlike every other operator in this module,
|
|
1354
|
+
# Announce/AnnounceDiamond are NOT passed to register_operator, because none of
|
|
1355
|
+
# its six fixities (prefix, agent_prefix, binary_iff, binary_implies,
|
|
1356
|
+
# binary_until, level2) can express a construct with a formula BETWEEN two
|
|
1357
|
+
# delimiters and a SECOND, independent formula after the closing delimiter. They
|
|
1358
|
+
# render via their own to_unicode_str/to_latex overrides instead (see the
|
|
1359
|
+
# "RENDERING" paragraph on the Announce class) and are added to NODE_CLASSES
|
|
1360
|
+
# directly above — mirroring how Nominal (also outside the registry, for the
|
|
1361
|
+
# same reason: an atomic construct the six fixities cannot express) is handled
|
|
1362
|
+
# in _hybrid_nodes.py.
|
|
1363
|
+
#
|
|
1364
|
+
# Both operators sit at the "prefix" level: the announcement (φ) is parsed as a
|
|
1365
|
+
# full `formula` — it is fully delimited by its own brackets, exactly like the
|
|
1366
|
+
# base grammar's `"(" formula ")"` grouping or Cardinality's `"|{" v ":" φ "}|"`
|
|
1367
|
+
# — while the post-condition (ψ) is parsed as `prefix`, binding as tightly as
|
|
1368
|
+
# ¬ / □ / K_a (the convention every other operator in this module follows), so
|
|
1369
|
+
# `[p!]q ∧ r` groups as `([p!]q) ∧ r`, not `[p!](q ∧ r)`.
|
|
1370
|
+
#
|
|
1371
|
+
# Terminal design / collision note: the base grammar TEMPLATE already has a
|
|
1372
|
+
# `"[" formula "]"` alternative (plain bracket-grouping, equivalent to parens) at
|
|
1373
|
+
# this same `prefix` level (see _fol_nodes._BASE_GRAMMAR_TEMPLATE). Reusing
|
|
1374
|
+
# "["/"]" for the PAL box syntax is therefore the LEAST-INTRUSIVE choice: it
|
|
1375
|
+
# shares the SAME anonymous terminal with that existing rule instead of
|
|
1376
|
+
# introducing a new bracket glyph, and Lark's Earley parser accepts two
|
|
1377
|
+
# alternatives with a common leading literal with no special priority
|
|
1378
|
+
# annotation needed (unlike the lexer-priority fix CFWOULD/CFMIGHT need above,
|
|
1379
|
+
# this is a grammar-RULE-level, not a terminal-level, ambiguity question). The
|
|
1380
|
+
# two rules cannot actually collide on any input: bracket-grouping requires "]"
|
|
1381
|
+
# to close the bracketed formula immediately, whereas this rule requires a
|
|
1382
|
+
# literal "!" first — a character that occurs in NO other terminal or rule in
|
|
1383
|
+
# the grammar — so a given "[...]" span parses under at most one of the two
|
|
1384
|
+
# alternatives (round-tripped in tests/test_pal.py: "[p]" parses as plain
|
|
1385
|
+
# grouping, "[p!]q" as Announce, and nothing straddles the two readings). The
|
|
1386
|
+
# diamond form's "⟨"/"⟩" (U+27E8/U+27E9) are not used ANYWHERE else in the
|
|
1387
|
+
# grammar, so it has no collision to document at all.
|
|
1388
|
+
register_parser_op(Announce, "modal", "prefix", "announce_",
|
|
1389
|
+
'"[" formula "!" "]" prefix',
|
|
1390
|
+
lambda items: Announce(items[0], items[1]))
|
|
1391
|
+
register_parser_op(AnnounceDiamond, "modal", "prefix", "announce_diamond_",
|
|
1392
|
+
'"⟨" formula "!" "⟩" prefix',
|
|
1393
|
+
lambda items: AnnounceDiamond(items[0], items[1]))
|
|
1394
|
+
|
|
1395
|
+
|
|
1396
|
+
# --- group-epistemic operators: E_{a,b,…} / D_{a,b,…} / C_{a,b,…} ----------
|
|
1397
|
+
#
|
|
1398
|
+
# Knows/Believes/Says/Wants each carry exactly ONE agent, folded into the
|
|
1399
|
+
# matched token itself ("K_alice" as a single KNOWS terminal) — the
|
|
1400
|
+
# `agent_prefix` machinery `register_operator` drives is built around that
|
|
1401
|
+
# single-token shape and has no way to express a variable-length LIST of
|
|
1402
|
+
# agents, so (like Announce/AnnounceDiamond just above) these three are
|
|
1403
|
+
# parser-only registrations with their own to_unicode_str/to_latex overrides
|
|
1404
|
+
# rather than register_operator calls; NODE_CLASSES already carries them (see
|
|
1405
|
+
# the NODE_CLASSES.update() next to the three class definitions above).
|
|
1406
|
+
#
|
|
1407
|
+
# Surface syntax: GLYPH_{agent,agent,…} formula — e.g. "C_{a,b,c} φ",
|
|
1408
|
+
# "D_{a,b} φ", "E_{a,b,c} φ". Each opening glyph is its OWN named terminal
|
|
1409
|
+
# fusing the letter, the underscore AND the opening brace into one token
|
|
1410
|
+
# ("C_{" / "D_{" / "E_{"), exactly so the lexer never has to choose between
|
|
1411
|
+
# it and a same-length PREDICATE match: a bare predicate named "C_" can only
|
|
1412
|
+
# ever lex as PREDICATE up to the "_" (no valid PREDICATE character follows
|
|
1413
|
+
# a "{"), so GROUP_C's 3-character match is strictly LONGER at every
|
|
1414
|
+
# position it can start, and standard longest-match lexing picks it with no
|
|
1415
|
+
# ambiguity to resolve by priority — the ".5" priority below is added anyway,
|
|
1416
|
+
# purely for consistency with KNOWS/BELIEVES/SAYS/WANTS/COUNTOP/CFWOULD/
|
|
1417
|
+
# CFMIGHT's own explicit-priority convention, not because it changes the
|
|
1418
|
+
# outcome here. "C_"/"D_"/"E_" are not used as a prefix ANYWHERE else in the
|
|
1419
|
+
# grammar (Historically/Once/Previous use the parenthesised glyphs ⒣⒫⒴, not
|
|
1420
|
+
# ASCII letters), so this introduces no collision with any existing operator.
|
|
1421
|
+
#
|
|
1422
|
+
# After the opening terminal, each agent is an ordinary VARIABLE (a, b, x1 —
|
|
1423
|
+
# single term-valued letter + digits) or NAME (alice, carol_smith — ≥2
|
|
1424
|
+
# letters, matching what Knows'/Believes' own agent regex already accepts)
|
|
1425
|
+
# token, comma-separated, closed by a bare "}" — the SAME anonymous "}"
|
|
1426
|
+
# terminal Cardinality's "|{v : φ}|" and (in the dependence mode) the slashed
|
|
1427
|
+
# existential's "∃x/{y, z}" already use, so no new terminal is introduced for
|
|
1428
|
+
# it either. Lark's inline `(A|B) ("," (A|B))*` repetition is already proven
|
|
1429
|
+
# for exactly this "name, name, …" shape by SlashedExists's slash set in
|
|
1430
|
+
# _team_nodes.py — mirrored here. Both VARIABLE and NAME tokens are
|
|
1431
|
+
# auto-transformed to Variable/Constant Nodes by FOLTransformer.VARIABLE/.NAME
|
|
1432
|
+
# (bottom-up, before this rule's own transform runs — see
|
|
1433
|
+
# _team_nodes.py's ``_slashed_transform`` for the identical items-slicing
|
|
1434
|
+
# pattern), so ``items[1:-1]`` below is already a tuple of agent term Nodes
|
|
1435
|
+
# and needs no further parsing — exactly the Variable/Constant shape
|
|
1436
|
+
# resolve_agent_variables (this module) expects for EverybodyKnows/
|
|
1437
|
+
# DistributedKnowledge/CommonKnowledge's ``group`` field. The trailing
|
|
1438
|
+
# ``prefix`` operand (the payload φ) binds at the SAME precedence every other
|
|
1439
|
+
# operator in this module uses for its post-modality operand.
|
|
1440
|
+
#
|
|
1441
|
+
# The grammar REQUIRES at least one agent (VARIABLE|NAME, not optional), so
|
|
1442
|
+
# the parser can never produce an empty group; DistributedKnowledge's own
|
|
1443
|
+
# __post_init__ additionally refuses one built directly through the Python
|
|
1444
|
+
# API (see that class's docstring) — modal_to_fol therefore never has to
|
|
1445
|
+
# render a D_∅.
|
|
1446
|
+
def _group_transform(node_cls):
|
|
1447
|
+
"""Build ``node_cls(group, formula)`` from [GLYPH token, agent…, formula]."""
|
|
1448
|
+
def transform(items):
|
|
1449
|
+
return node_cls(tuple(items[1:-1]), items[-1])
|
|
1450
|
+
return transform
|
|
1451
|
+
|
|
1452
|
+
|
|
1453
|
+
register_parser_op(
|
|
1454
|
+
CommonKnowledge, "modal", "prefix", "common_knowledge_",
|
|
1455
|
+
'GROUP_C (VARIABLE|NAME) ("," (VARIABLE|NAME))* "}" prefix',
|
|
1456
|
+
_group_transform(CommonKnowledge),
|
|
1457
|
+
terminal_name="GROUP_C", terminal_def='GROUP_C.5: "C_{"')
|
|
1458
|
+
register_parser_op(
|
|
1459
|
+
DistributedKnowledge, "modal", "prefix", "distributed_knowledge_",
|
|
1460
|
+
'GROUP_D (VARIABLE|NAME) ("," (VARIABLE|NAME))* "}" prefix',
|
|
1461
|
+
_group_transform(DistributedKnowledge),
|
|
1462
|
+
terminal_name="GROUP_D", terminal_def='GROUP_D.5: "D_{"')
|
|
1463
|
+
register_parser_op(
|
|
1464
|
+
EverybodyKnows, "modal", "prefix", "everybody_knows_",
|
|
1465
|
+
'GROUP_E (VARIABLE|NAME) ("," (VARIABLE|NAME))* "}" prefix',
|
|
1466
|
+
_group_transform(EverybodyKnows),
|
|
1467
|
+
terminal_name="GROUP_E", terminal_def='GROUP_E.5: "E_{"')
|