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.
Files changed (237) hide show
  1. unicode_logic_kit/__init__.py +385 -0
  2. unicode_logic_kit/__main__.py +520 -0
  3. unicode_logic_kit/_deadline.py +219 -0
  4. unicode_logic_kit/ace/__init__.py +126 -0
  5. unicode_logic_kit/ace/_align.py +135 -0
  6. unicode_logic_kit/ace/chem_lexicon.py +128 -0
  7. unicode_logic_kit/ace/drs_reader.py +570 -0
  8. unicode_logic_kit/ace/mapping.py +666 -0
  9. unicode_logic_kit/ace/reverse_modal.py +138 -0
  10. unicode_logic_kit/ace/runner.py +551 -0
  11. unicode_logic_kit/ace/translate.py +452 -0
  12. unicode_logic_kit/ace/verbalize.py +1070 -0
  13. unicode_logic_kit/api.py +1284 -0
  14. unicode_logic_kit/atp/__init__.py +177 -0
  15. unicode_logic_kit/atp/_ascii_names.py +113 -0
  16. unicode_logic_kit/atp/_html.py +72 -0
  17. unicode_logic_kit/atp/_substructural_input.py +228 -0
  18. unicode_logic_kit/atp/_tff_problem.py +715 -0
  19. unicode_logic_kit/atp/_tptp_problem.py +1111 -0
  20. unicode_logic_kit/atp/_writer_support.py +289 -0
  21. unicode_logic_kit/atp/clingo_backend.py +1180 -0
  22. unicode_logic_kit/atp/cvc5_backend.py +1385 -0
  23. unicode_logic_kit/atp/eprover_backend.py +732 -0
  24. unicode_logic_kit/atp/finite_domain.py +1055 -0
  25. unicode_logic_kit/atp/fitch.py +1547 -0
  26. unicode_logic_kit/atp/fitch_search.py +551 -0
  27. unicode_logic_kit/atp/hets_backend.py +339 -0
  28. unicode_logic_kit/atp/hybrid_down.py +120 -0
  29. unicode_logic_kit/atp/incremental.py +250 -0
  30. unicode_logic_kit/atp/kripke_enum.py +741 -0
  31. unicode_logic_kit/atp/lambek.py +436 -0
  32. unicode_logic_kit/atp/leo3_backend.py +332 -0
  33. unicode_logic_kit/atp/linear.py +738 -0
  34. unicode_logic_kit/atp/lj.py +705 -0
  35. unicode_logic_kit/atp/logic_backends.py +566 -0
  36. unicode_logic_kit/atp/ltl_tableau.py +1084 -0
  37. unicode_logic_kit/atp/minizinc_backend.py +1402 -0
  38. unicode_logic_kit/atp/modal_tableau.py +1382 -0
  39. unicode_logic_kit/atp/nanocop_backend.py +410 -0
  40. unicode_logic_kit/atp/portfolio.py +489 -0
  41. unicode_logic_kit/atp/protocol.py +1803 -0
  42. unicode_logic_kit/atp/prover9_entailment.py +1153 -0
  43. unicode_logic_kit/atp/resolution.py +1376 -0
  44. unicode_logic_kit/atp/resolution_check.py +1114 -0
  45. unicode_logic_kit/atp/sequent.py +1050 -0
  46. unicode_logic_kit/atp/tableau.py +921 -0
  47. unicode_logic_kit/atp/tableau_check.py +543 -0
  48. unicode_logic_kit/atp/tptp_ncl.py +811 -0
  49. unicode_logic_kit/atp/tptp_tff.py +1546 -0
  50. unicode_logic_kit/atp/tstp.py +1333 -0
  51. unicode_logic_kit/atp/tstp_check.py +1096 -0
  52. unicode_logic_kit/atp/twee_backend.py +236 -0
  53. unicode_logic_kit/atp/twee_check.py +711 -0
  54. unicode_logic_kit/atp/twee_entailment.py +953 -0
  55. unicode_logic_kit/atp/vampire_entailment.py +540 -0
  56. unicode_logic_kit/atp/z3_arith.py +470 -0
  57. unicode_logic_kit/atp/z3_equivalence.py +36 -0
  58. unicode_logic_kit/atp/z3_fuzzy.py +362 -0
  59. unicode_logic_kit/atp/z3_input.py +500 -0
  60. unicode_logic_kit/atp/z3_models.py +208 -0
  61. unicode_logic_kit/chem/__init__.py +88 -0
  62. unicode_logic_kit/chem/_naming.py +284 -0
  63. unicode_logic_kit/chem/cache.py +185 -0
  64. unicode_logic_kit/chem/interop.py +244 -0
  65. unicode_logic_kit/chem/mol.py +525 -0
  66. unicode_logic_kit/chem/signature.py +112 -0
  67. unicode_logic_kit/comorphism.py +497 -0
  68. unicode_logic_kit/dl/__init__.py +384 -0
  69. unicode_logic_kit/dl/classification.py +227 -0
  70. unicode_logic_kit/dl/concepts.py +632 -0
  71. unicode_logic_kit/dl/datatypes.py +818 -0
  72. unicode_logic_kit/dl/owl_functional.py +2433 -0
  73. unicode_logic_kit/dl/owl_manchester.py +1637 -0
  74. unicode_logic_kit/dl/owl_reasoner.py +790 -0
  75. unicode_logic_kit/dl/parser.py +391 -0
  76. unicode_logic_kit/dl/tableau.py +4048 -0
  77. unicode_logic_kit/dl/translate.py +2704 -0
  78. unicode_logic_kit/drt/__init__.py +94 -0
  79. unicode_logic_kit/drt/export.py +179 -0
  80. unicode_logic_kit/drt/nodes.py +506 -0
  81. unicode_logic_kit/drt/parser.py +965 -0
  82. unicode_logic_kit/drt/resolve.py +195 -0
  83. unicode_logic_kit/drt/reverse.py +175 -0
  84. unicode_logic_kit/eval/__init__.py +106 -0
  85. unicode_logic_kit/eval/batch.py +382 -0
  86. unicode_logic_kit/eval/canonical.py +663 -0
  87. unicode_logic_kit/eval/chem_batch.py +606 -0
  88. unicode_logic_kit/eval/converses.py +200 -0
  89. unicode_logic_kit/eval/datasets/__init__.py +136 -0
  90. unicode_logic_kit/eval/datasets/_base.py +263 -0
  91. unicode_logic_kit/eval/datasets/_proofwriter_proof.py +422 -0
  92. unicode_logic_kit/eval/datasets/c3po.py +678 -0
  93. unicode_logic_kit/eval/datasets/folio.py +158 -0
  94. unicode_logic_kit/eval/datasets/fracas.py +418 -0
  95. unicode_logic_kit/eval/datasets/groves.py +191 -0
  96. unicode_logic_kit/eval/datasets/logicbench.py +467 -0
  97. unicode_logic_kit/eval/datasets/logicnli.py +303 -0
  98. unicode_logic_kit/eval/datasets/malls.py +133 -0
  99. unicode_logic_kit/eval/datasets/pfolio.py +594 -0
  100. unicode_logic_kit/eval/datasets/pmb.py +242 -0
  101. unicode_logic_kit/eval/datasets/prontoqa.py +611 -0
  102. unicode_logic_kit/eval/datasets/proofwriter.py +1431 -0
  103. unicode_logic_kit/eval/datasets/proverqa.py +674 -0
  104. unicode_logic_kit/eval/datasets/willow.py +478 -0
  105. unicode_logic_kit/eval/equivalence.py +466 -0
  106. unicode_logic_kit/eval/exercise_gen.py +533 -0
  107. unicode_logic_kit/eval/explain.py +791 -0
  108. unicode_logic_kit/eval/generality.py +750 -0
  109. unicode_logic_kit/eval/metric_hf.py +458 -0
  110. unicode_logic_kit/eval/predicate_match.py +343 -0
  111. unicode_logic_kit/eval/theory_check.py +1170 -0
  112. unicode_logic_kit/eval/validate.py +306 -0
  113. unicode_logic_kit/fol/__init__.py +177 -0
  114. unicode_logic_kit/fol/_atom_keys.py +510 -0
  115. unicode_logic_kit/fol/_fol_nodes.py +3586 -0
  116. unicode_logic_kit/fol/_free_parameters.py +105 -0
  117. unicode_logic_kit/fol/_ho_nodes.py +448 -0
  118. unicode_logic_kit/fol/_hybrid_nodes.py +308 -0
  119. unicode_logic_kit/fol/_identifiers.py +1091 -0
  120. unicode_logic_kit/fol/_lambek_nodes.py +112 -0
  121. unicode_logic_kit/fol/_linear_nodes.py +352 -0
  122. unicode_logic_kit/fol/_modal_nodes.py +1467 -0
  123. unicode_logic_kit/fol/_msfl_nodes.py +2196 -0
  124. unicode_logic_kit/fol/_numeral_symbols.py +231 -0
  125. unicode_logic_kit/fol/_so_nodes.py +200 -0
  126. unicode_logic_kit/fol/_symbol_names.py +81 -0
  127. unicode_logic_kit/fol/_team_nodes.py +181 -0
  128. unicode_logic_kit/fol/_tptp_symbols.py +551 -0
  129. unicode_logic_kit/fol/_truth_constants.py +117 -0
  130. unicode_logic_kit/fol/casl_export.py +1135 -0
  131. unicode_logic_kit/fol/casl_import.py +929 -0
  132. unicode_logic_kit/fol/derivation.py +367 -0
  133. unicode_logic_kit/fol/dialect_detect.py +70 -0
  134. unicode_logic_kit/fol/dialect_repair.py +537 -0
  135. unicode_logic_kit/fol/frames.py +637 -0
  136. unicode_logic_kit/fol/grammars/terminals.lark +31 -0
  137. unicode_logic_kit/fol/lambda_tools.py +297 -0
  138. unicode_logic_kit/fol/latex_input.py +429 -0
  139. unicode_logic_kit/fol/modal_translation.py +944 -0
  140. unicode_logic_kit/fol/msflparser.py +1033 -0
  141. unicode_logic_kit/fol/naming.py +422 -0
  142. unicode_logic_kit/fol/nodes.py +241 -0
  143. unicode_logic_kit/fol/normalforms.py +492 -0
  144. unicode_logic_kit/fol/pal.py +287 -0
  145. unicode_logic_kit/fol/prolog_export.py +566 -0
  146. unicode_logic_kit/fol/prolog_input.py +505 -0
  147. unicode_logic_kit/fol/prover9_input.py +1325 -0
  148. unicode_logic_kit/fol/qml.py +1760 -0
  149. unicode_logic_kit/fol/qmltp_input.py +525 -0
  150. unicode_logic_kit/fol/sanitize.py +221 -0
  151. unicode_logic_kit/fol/serialize.py +79 -0
  152. unicode_logic_kit/fol/signature.py +1290 -0
  153. unicode_logic_kit/fol/simplify_check.py +544 -0
  154. unicode_logic_kit/fol/spans.py +594 -0
  155. unicode_logic_kit/fol/tptp_input.py +1503 -0
  156. unicode_logic_kit/fol/tptp_repair.py +941 -0
  157. unicode_logic_kit/fol/unification.py +157 -0
  158. unicode_logic_kit/fol/verbalize.py +263 -0
  159. unicode_logic_kit/hets/__init__.py +163 -0
  160. unicode_logic_kit/hets/bridge.py +142 -0
  161. unicode_logic_kit/hets/client.py +748 -0
  162. unicode_logic_kit/hets/docker.py +420 -0
  163. unicode_logic_kit/hets/dol.py +712 -0
  164. unicode_logic_kit/hets/haskell_json.py +355 -0
  165. unicode_logic_kit/hets/owl_backend.py +794 -0
  166. unicode_logic_kit/hets/owl_cli.py +598 -0
  167. unicode_logic_kit/hets/symbols.py +512 -0
  168. unicode_logic_kit/hol/__init__.py +140 -0
  169. unicode_logic_kit/hol/_ho_common.py +323 -0
  170. unicode_logic_kit/hol/_isabelle_binders.py +125 -0
  171. unicode_logic_kit/hol/classical.py +812 -0
  172. unicode_logic_kit/hol/deepshallow/__init__.py +45 -0
  173. unicode_logic_kit/hol/deepshallow/_common.py +177 -0
  174. unicode_logic_kit/hol/deepshallow/conditional.py +225 -0
  175. unicode_logic_kit/hol/deepshallow/intuitionistic.py +181 -0
  176. unicode_logic_kit/hol/deepshallow/modal.py +217 -0
  177. unicode_logic_kit/hol/deepshallow/qml.py +406 -0
  178. unicode_logic_kit/hol/deepshallow/relevant.py +206 -0
  179. unicode_logic_kit/hol/free.py +753 -0
  180. unicode_logic_kit/hol/goedel.py +336 -0
  181. unicode_logic_kit/hol/ho_modal.py +1743 -0
  182. unicode_logic_kit/hol/intuitionistic.py +403 -0
  183. unicode_logic_kit/hol/isabelle_conditional.py +593 -0
  184. unicode_logic_kit/hol/isabelle_modal.py +1908 -0
  185. unicode_logic_kit/hol/isabelle_relevant.py +412 -0
  186. unicode_logic_kit/hol/isabelle_runner.py +1147 -0
  187. unicode_logic_kit/hol/isabelle_substructural.py +884 -0
  188. unicode_logic_kit/hol/lean.py +1018 -0
  189. unicode_logic_kit/hol/manyvalued.py +921 -0
  190. unicode_logic_kit/hol/secondorder.py +687 -0
  191. unicode_logic_kit/hol/thf_modal.py +941 -0
  192. unicode_logic_kit/hol/thirdorder.py +397 -0
  193. unicode_logic_kit/ilp/__init__.py +89 -0
  194. unicode_logic_kit/ilp/readback.py +389 -0
  195. unicode_logic_kit/ilp/separation.py +153 -0
  196. unicode_logic_kit/ilp/task.py +730 -0
  197. unicode_logic_kit/logic.py +163 -0
  198. unicode_logic_kit/mcp/__init__.py +28 -0
  199. unicode_logic_kit/mcp/__main__.py +5 -0
  200. unicode_logic_kit/mcp/chem_tools.py +1031 -0
  201. unicode_logic_kit/mcp/server.py +2453 -0
  202. unicode_logic_kit/mcp/syntax_spec.py +681 -0
  203. unicode_logic_kit/prob/__init__.py +53 -0
  204. unicode_logic_kit/prob/_bdd.py +225 -0
  205. unicode_logic_kit/prob/_column_gen.py +668 -0
  206. unicode_logic_kit/prob/distribution.py +686 -0
  207. unicode_logic_kit/prob/nilsson.py +470 -0
  208. unicode_logic_kit/py.typed +0 -0
  209. unicode_logic_kit/semantics/__init__.py +137 -0
  210. unicode_logic_kit/semantics/_modal_reject.py +156 -0
  211. unicode_logic_kit/semantics/action_models.py +466 -0
  212. unicode_logic_kit/semantics/asp_models.py +1200 -0
  213. unicode_logic_kit/semantics/conditional.py +580 -0
  214. unicode_logic_kit/semantics/dynamic_epistemic.py +95 -0
  215. unicode_logic_kit/semantics/free_logic.py +913 -0
  216. unicode_logic_kit/semantics/fuzzy.py +384 -0
  217. unicode_logic_kit/semantics/fuzzy_kripke.py +442 -0
  218. unicode_logic_kit/semantics/intuitionistic.py +581 -0
  219. unicode_logic_kit/semantics/kripke.py +1139 -0
  220. unicode_logic_kit/semantics/manyvalued.py +580 -0
  221. unicode_logic_kit/semantics/matrix.py +342 -0
  222. unicode_logic_kit/semantics/model_eval.py +1135 -0
  223. unicode_logic_kit/semantics/modelfinder.py +1036 -0
  224. unicode_logic_kit/semantics/nonmonotonic.py +372 -0
  225. unicode_logic_kit/semantics/relevant.py +331 -0
  226. unicode_logic_kit/semantics/secondorder.py +657 -0
  227. unicode_logic_kit/semantics/structures.py +352 -0
  228. unicode_logic_kit/semantics/tarski.py +975 -0
  229. unicode_logic_kit/semantics/team.py +315 -0
  230. unicode_logic_kit/semantics/team_translation.py +416 -0
  231. unicode_logic_kit/semantics/thirdorder.py +358 -0
  232. unicode_logic_kit/semantics/tnorm.py +85 -0
  233. unicode_logic_kit/semantics/truthtable.py +201 -0
  234. unicode_logic_kit-0.31.0.dist-info/METADATA +333 -0
  235. unicode_logic_kit-0.31.0.dist-info/RECORD +237 -0
  236. unicode_logic_kit-0.31.0.dist-info/WHEEL +4 -0
  237. 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_{"')