sofic 0.1.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 (150) hide show
  1. sofic/__init__.py +185 -0
  2. sofic/automata/__init__.py +207 -0
  3. sofic/automata/_config_simulation.py +40 -0
  4. sofic/automata/active.py +611 -0
  5. sofic/automata/alergia.py +222 -0
  6. sofic/automata/algorithms.py +376 -0
  7. sofic/automata/atomaton.py +58 -0
  8. sofic/automata/base.py +161 -0
  9. sofic/automata/buchi.py +23 -0
  10. sofic/automata/buchi_simulation.py +67 -0
  11. sofic/automata/canonical_dual.py +18 -0
  12. sofic/automata/canonical_extraction.py +122 -0
  13. sofic/automata/dfa.py +85 -0
  14. sofic/automata/dfasat.py +195 -0
  15. sofic/automata/edsm.py +219 -0
  16. sofic/automata/enumeration.py +44 -0
  17. sofic/automata/icdfa.py +421 -0
  18. sofic/automata/idfa.py +363 -0
  19. sofic/automata/languages/__init__.py +39 -0
  20. sofic/automata/languages/_quotient_utils.py +64 -0
  21. sofic/automata/languages/atoms.py +31 -0
  22. sofic/automata/languages/automaton_ops.py +243 -0
  23. sofic/automata/languages/base.py +67 -0
  24. sofic/automata/languages/operations.py +78 -0
  25. sofic/automata/languages/quotients.py +66 -0
  26. sofic/automata/languages/residuals.py +25 -0
  27. sofic/automata/learning.py +79 -0
  28. sofic/automata/nfa.py +39 -0
  29. sofic/automata/nwa.py +343 -0
  30. sofic/automata/nwa_simulation.py +56 -0
  31. sofic/automata/observation.py +40 -0
  32. sofic/automata/papni.py +301 -0
  33. sofic/automata/regex.py +128 -0
  34. sofic/automata/rfsa.py +35 -0
  35. sofic/automata/rpni.py +193 -0
  36. sofic/automata/subsequential.py +201 -0
  37. sofic/automata/transducer_operations.py +350 -0
  38. sofic/automata/transducer_simulation.py +150 -0
  39. sofic/automata/transducers.py +365 -0
  40. sofic/automata/unifilar.py +107 -0
  41. sofic/automata/vpa.py +1373 -0
  42. sofic/automata/vpa_simulation.py +53 -0
  43. sofic/base.py +153 -0
  44. sofic/core.py +47 -0
  45. sofic/examples/__init__.py +86 -0
  46. sofic/examples/epsilon_machines.py +1089 -0
  47. sofic/examples/processes.py +1491 -0
  48. sofic/examples/shifts.py +144 -0
  49. sofic/exceptions.py +33 -0
  50. sofic/generators/__init__.py +115 -0
  51. sofic/generators/_word_measures.py +94 -0
  52. sofic/generators/alternative_complexity.py +104 -0
  53. sofic/generators/base.py +327 -0
  54. sofic/generators/bidirectional_construction.py +717 -0
  55. sofic/generators/bidirectional_epsilon_machine.py +689 -0
  56. sofic/generators/block_convergence.py +668 -0
  57. sofic/generators/block_entropy.py +578 -0
  58. sofic/generators/channel_measures.py +75 -0
  59. sofic/generators/conversions.py +182 -0
  60. sofic/generators/directional_flow.py +245 -0
  61. sofic/generators/edge_emissions.py +36 -0
  62. sofic/generators/edge_machine.py +178 -0
  63. sofic/generators/epsilon_construction.py +193 -0
  64. sofic/generators/epsilon_inference.py +703 -0
  65. sofic/generators/epsilon_machine.py +557 -0
  66. sofic/generators/epsilon_transducer.py +168 -0
  67. sofic/generators/epsilon_transducer_construction.py +185 -0
  68. sofic/generators/epsilon_transducer_inference.py +499 -0
  69. sofic/generators/hmm_inference.py +719 -0
  70. sofic/generators/information_diagram.py +428 -0
  71. sofic/generators/lumping.py +447 -0
  72. sofic/generators/markov.py +100 -0
  73. sofic/generators/mealy.py +156 -0
  74. sofic/generators/measures.py +257 -0
  75. sofic/generators/minimal_generative_model.py +821 -0
  76. sofic/generators/mixed_state.py +250 -0
  77. sofic/generators/mixed_state_construction.py +163 -0
  78. sofic/generators/moore.py +75 -0
  79. sofic/generators/nmachine.py +78 -0
  80. sofic/generators/nmachine_construction.py +70 -0
  81. sofic/generators/pfa.py +100 -0
  82. sofic/generators/prob.py +291 -0
  83. sofic/generators/process_equivalence.py +207 -0
  84. sofic/generators/quasi_inference.py +74 -0
  85. sofic/generators/quasi_realization.py +97 -0
  86. sofic/generators/reversal.py +66 -0
  87. sofic/generators/stack_hmm.py +426 -0
  88. sofic/generators/stack_inference.py +509 -0
  89. sofic/generators/stationary.py +134 -0
  90. sofic/generators/stochastic.py +65 -0
  91. sofic/generators/synchronization.py +407 -0
  92. sofic/generators/topological_epsilon_enumeration.py +349 -0
  93. sofic/generators/words.py +226 -0
  94. sofic/graph.py +135 -0
  95. sofic/indexing.py +31 -0
  96. sofic/inference/__init__.py +45 -0
  97. sofic/inference/bayesian/__init__.py +68 -0
  98. sofic/inference/bayesian/comparison.py +199 -0
  99. sofic/inference/bayesian/counts.py +219 -0
  100. sofic/inference/bayesian/diversity.py +254 -0
  101. sofic/inference/bayesian/epsilon.py +270 -0
  102. sofic/inference/bayesian/hdp_hmm.py +340 -0
  103. sofic/inference/bayesian/markov.py +294 -0
  104. sofic/inference/bayesian/pymc_backend.py +71 -0
  105. sofic/inference/bayesian/stack_hmm.py +215 -0
  106. sofic/inference/model_selection.py +365 -0
  107. sofic/inference/spectral.py +564 -0
  108. sofic/operations.py +16 -0
  109. sofic/properties.py +339 -0
  110. sofic/serialization.py +450 -0
  111. sofic/shifts/__init__.py +48 -0
  112. sofic/shifts/algorithms.py +84 -0
  113. sofic/shifts/base.py +49 -0
  114. sofic/shifts/cover_construction.py +76 -0
  115. sofic/shifts/covers.py +47 -0
  116. sofic/shifts/dyck_algorithms.py +100 -0
  117. sofic/shifts/dyck_enumeration.py +275 -0
  118. sofic/shifts/markov_dyck.py +172 -0
  119. sofic/shifts/parry_construction.py +82 -0
  120. sofic/shifts/sft.py +104 -0
  121. sofic/shifts/sft_construction.py +52 -0
  122. sofic/shifts/sliding_block_code.py +156 -0
  123. sofic/shifts/sofic.py +111 -0
  124. sofic/shifts/sofic_dyck.py +110 -0
  125. sofic/shifts/sofic_relation.py +64 -0
  126. sofic/shifts/textile.py +104 -0
  127. sofic/shifts/tmc.py +46 -0
  128. sofic/shifts/tmc_construction.py +58 -0
  129. sofic/shifts/topological_anatomy.py +150 -0
  130. sofic/states.py +27 -0
  131. sofic/testing/__init__.py +8 -0
  132. sofic/testing/strategies.py +154 -0
  133. sofic/viz/__init__.py +16 -0
  134. sofic/viz/_context.py +345 -0
  135. sofic/viz/_edge.py +216 -0
  136. sofic/viz/_format.py +89 -0
  137. sofic/viz/_labels.py +34 -0
  138. sofic/viz/_names.py +17 -0
  139. sofic/viz/_rational.py +20 -0
  140. sofic/viz/_tikz_compile.py +177 -0
  141. sofic/viz/_tikz_format.py +122 -0
  142. sofic/viz/_tikz_layout.py +218 -0
  143. sofic/viz/assets/vaucanson.tikz +71 -0
  144. sofic/viz/graphviz.py +158 -0
  145. sofic/viz/idiagram.py +350 -0
  146. sofic/viz/tikz.py +381 -0
  147. sofic-0.1.0.dist-info/METADATA +444 -0
  148. sofic-0.1.0.dist-info/RECORD +150 -0
  149. sofic-0.1.0.dist-info/WHEEL +4 -0
  150. sofic-0.1.0.dist-info/licenses/LICENSE.txt +29 -0
sofic/automata/vpa.py ADDED
@@ -0,0 +1,1373 @@
1
+ """Visibly pushdown automata."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections import deque
6
+ from collections.abc import Hashable, Iterable, Mapping, Sequence
7
+ from typing import Any
8
+
9
+ from sofic.base import StateMachine
10
+ from sofic.exceptions import NonDeterministicError
11
+ from sofic.graph import (
12
+ ATTR_KIND,
13
+ ATTR_STACK_SYMBOL,
14
+ ATTR_SYMBOL,
15
+ KIND_CALL,
16
+ KIND_INTERNAL,
17
+ KIND_RETURN,
18
+ )
19
+
20
+ _MISSING = object()
21
+
22
+
23
+ class VisiblyPushdownAutomaton(StateMachine):
24
+ """Standard 1-stack VPA with call / return / internal input partition."""
25
+
26
+ input_alphabet: frozenset[Any]
27
+ call_alphabet: frozenset[Any]
28
+ return_alphabet: frozenset[Any]
29
+ internal_alphabet: frozenset[Any]
30
+ stack_alphabet: frozenset[Any]
31
+ bottom_stack_symbol: Any | None
32
+ initial_state: Hashable | None
33
+ accepting_states: frozenset[Hashable]
34
+
35
+ def __init__(
36
+ self,
37
+ input_alphabet: frozenset[Any] | None = None,
38
+ call_alphabet: frozenset[Any] | None = None,
39
+ return_alphabet: frozenset[Any] | None = None,
40
+ internal_alphabet: frozenset[Any] | None = None,
41
+ stack_alphabet: frozenset[Any] | None = None,
42
+ bottom_stack_symbol: Any | None = None,
43
+ initial_state: Hashable | None = None,
44
+ accepting_states: frozenset[Hashable] | None = None,
45
+ **kwargs: Any,
46
+ ) -> None:
47
+ super().__init__(**kwargs)
48
+ self.call_alphabet = call_alphabet if call_alphabet is not None else frozenset()
49
+ self.return_alphabet = return_alphabet if return_alphabet is not None else frozenset()
50
+ self.internal_alphabet = internal_alphabet if internal_alphabet is not None else frozenset()
51
+ self.stack_alphabet = stack_alphabet if stack_alphabet is not None else frozenset()
52
+ self.bottom_stack_symbol = bottom_stack_symbol
53
+ self.input_alphabet = (
54
+ input_alphabet
55
+ if input_alphabet is not None
56
+ else self.call_alphabet | self.return_alphabet | self.internal_alphabet
57
+ )
58
+ self.initial_state = initial_state
59
+ self.accepting_states = accepting_states if accepting_states is not None else frozenset()
60
+
61
+ def validate(self) -> None:
62
+ partition = self.call_alphabet | self.return_alphabet | self.internal_alphabet
63
+ self._require(
64
+ len(self.call_alphabet) + len(self.return_alphabet) + len(self.internal_alphabet) == len(partition),
65
+ "call, return, and internal alphabets must be disjoint",
66
+ )
67
+ self._require(partition == self.input_alphabet, "input alphabet must equal partition of call/return/internal")
68
+ if self.bottom_stack_symbol is not None:
69
+ self._require(
70
+ self.bottom_stack_symbol in self.stack_alphabet, "bottom_stack_symbol must be in stack alphabet"
71
+ )
72
+ if self.initial_state is not None:
73
+ self._require(self.graph.has_state(self.initial_state), "missing initial state")
74
+ for state in self.accepting_states:
75
+ self._require(self.graph.has_state(state), f"missing accepting state {state!r}")
76
+ for transition in self.transitions():
77
+ kind = transition.data.get(ATTR_KIND)
78
+ symbol = transition.data.get(ATTR_SYMBOL)
79
+ self._require(kind in {KIND_CALL, KIND_RETURN, KIND_INTERNAL}, f"invalid VPA kind {kind!r}")
80
+ if symbol is not None:
81
+ if kind == KIND_CALL:
82
+ self._require(symbol in self.call_alphabet, f"{symbol!r} not in call alphabet")
83
+ stack_sym = transition.data.get(ATTR_STACK_SYMBOL)
84
+ self._require(stack_sym in self.stack_alphabet, "call edge requires stack_symbol in stack alphabet")
85
+ self._require(
86
+ stack_sym != self.bottom_stack_symbol,
87
+ "call edge cannot push the bottom_stack_symbol",
88
+ )
89
+ elif kind == KIND_RETURN:
90
+ self._require(symbol in self.return_alphabet, f"{symbol!r} not in return alphabet")
91
+ stack_sym = transition.data.get(ATTR_STACK_SYMBOL)
92
+ if stack_sym is not None:
93
+ self._require(stack_sym in self.stack_alphabet, "return stack_symbol must be in stack alphabet")
94
+ else:
95
+ self._require(symbol in self.internal_alphabet, f"{symbol!r} not in internal alphabet")
96
+
97
+ def add_call_transition(
98
+ self,
99
+ source: Hashable,
100
+ target: Hashable,
101
+ symbol: Any,
102
+ stack_symbol: Any,
103
+ **attrs: Any,
104
+ ) -> int:
105
+ """Add a call transition that pushes ``stack_symbol``."""
106
+ data = {**attrs, ATTR_KIND: KIND_CALL, ATTR_SYMBOL: symbol, ATTR_STACK_SYMBOL: stack_symbol}
107
+ return self.graph.add_transition(source, target, **data)
108
+
109
+ def add_return_transition(
110
+ self,
111
+ source: Hashable,
112
+ target: Hashable,
113
+ symbol: Any,
114
+ stack_symbol: Any | None = None,
115
+ **attrs: Any,
116
+ ) -> int:
117
+ """Add a return transition.
118
+
119
+ If ``stack_symbol`` is omitted, the transition is a wildcard over
120
+ non-bottom stack symbols. This preserves the historical unguarded
121
+ return behavior of :class:`VisiblyPushdownAutomaton`.
122
+ """
123
+ data = {**attrs, ATTR_KIND: KIND_RETURN, ATTR_SYMBOL: symbol}
124
+ if stack_symbol is not None:
125
+ data[ATTR_STACK_SYMBOL] = stack_symbol
126
+ return self.graph.add_transition(source, target, **data)
127
+
128
+ def add_internal_transition(self, source: Hashable, target: Hashable, symbol: Any, **attrs: Any) -> int:
129
+ """Add an internal transition."""
130
+ data = {**attrs, ATTR_KIND: KIND_INTERNAL, ATTR_SYMBOL: symbol}
131
+ return self.graph.add_transition(source, target, **data)
132
+
133
+ def call_transition_map(self) -> dict[tuple[Hashable, Any], tuple[Hashable, Any]]:
134
+ """Return deterministic call transitions keyed by ``(state, symbol)``."""
135
+ result: dict[tuple[Hashable, Any], tuple[Hashable, Any]] = {}
136
+ for transition in self.transitions():
137
+ if transition.data.get(ATTR_KIND) != KIND_CALL:
138
+ continue
139
+ symbol = transition.data.get(ATTR_SYMBOL)
140
+ if symbol is None:
141
+ continue
142
+ key = (transition.source, symbol)
143
+ value = (transition.target, transition.data.get(ATTR_STACK_SYMBOL))
144
+ if key in result and result[key] != value:
145
+ raise NonDeterministicError(f"non-deterministic call transition on {key}")
146
+ result[key] = value
147
+ return result
148
+
149
+ def return_transition_map(self) -> dict[tuple[Hashable, Any, Any | None], Hashable]:
150
+ """Return deterministic return transitions keyed by ``(state, symbol, stack_symbol)``."""
151
+ result: dict[tuple[Hashable, Any, Any | None], Hashable] = {}
152
+ for transition in self.transitions():
153
+ if transition.data.get(ATTR_KIND) != KIND_RETURN:
154
+ continue
155
+ symbol = transition.data.get(ATTR_SYMBOL)
156
+ if symbol is None:
157
+ continue
158
+ key = (transition.source, symbol, transition.data.get(ATTR_STACK_SYMBOL))
159
+ value = transition.target
160
+ if key in result and result[key] != value:
161
+ raise NonDeterministicError(f"non-deterministic return transition on {key}")
162
+ result[key] = value
163
+ return result
164
+
165
+ def internal_transition_map(self) -> dict[tuple[Hashable, Any], Hashable]:
166
+ """Return deterministic internal transitions keyed by ``(state, symbol)``."""
167
+ result: dict[tuple[Hashable, Any], Hashable] = {}
168
+ for transition in self.transitions():
169
+ if transition.data.get(ATTR_KIND) != KIND_INTERNAL:
170
+ continue
171
+ symbol = transition.data.get(ATTR_SYMBOL)
172
+ if symbol is None:
173
+ continue
174
+ key = (transition.source, symbol)
175
+ value = transition.target
176
+ if key in result and result[key] != value:
177
+ raise NonDeterministicError(f"non-deterministic internal transition on {key}")
178
+ result[key] = value
179
+ return result
180
+
181
+ def recognizes(self, word: Sequence[Any]) -> bool:
182
+ from sofic.automata.vpa_simulation import recognizes_vpa
183
+
184
+ return recognizes_vpa(self, word)
185
+
186
+ def union(self, other: VisiblyPushdownAutomaton) -> CompositeVisiblyPushdownAutomaton:
187
+ """Return a VPA recognizer for the union with ``other``."""
188
+ return union_vpa(self, other)
189
+
190
+ def intersection(self, other: VisiblyPushdownAutomaton) -> CompositeVisiblyPushdownAutomaton:
191
+ """Return a VPA recognizer for the intersection with ``other``."""
192
+ return intersection_vpa(self, other)
193
+
194
+ def intersect(self, other: VisiblyPushdownAutomaton) -> CompositeVisiblyPushdownAutomaton:
195
+ """Alias for :meth:`intersection`."""
196
+ return self.intersection(other)
197
+
198
+ def complement(self) -> CompositeVisiblyPushdownAutomaton:
199
+ """Return a VPA recognizer for the complement over this visible alphabet."""
200
+ return complement_vpa(self)
201
+
202
+ def difference(self, other: VisiblyPushdownAutomaton) -> CompositeVisiblyPushdownAutomaton:
203
+ """Return a VPA recognizer for this language minus ``other``."""
204
+ return difference_vpa(self, other)
205
+
206
+ def concat(self, other: VisiblyPushdownAutomaton) -> CompositeVisiblyPushdownAutomaton:
207
+ """Return a VPA recognizer for concatenation with ``other``."""
208
+ return concat_vpa(self, other)
209
+
210
+ def concatenate(self, other: VisiblyPushdownAutomaton) -> CompositeVisiblyPushdownAutomaton:
211
+ """Alias for :meth:`concat`."""
212
+ return self.concat(other)
213
+
214
+ def kleene_star(self) -> CompositeVisiblyPushdownAutomaton:
215
+ """Return a VPA recognizer for the Kleene star of this language."""
216
+ return kleene_star_vpa(self)
217
+
218
+ def star(self) -> CompositeVisiblyPushdownAutomaton:
219
+ """Alias for :meth:`kleene_star`."""
220
+ return self.kleene_star()
221
+
222
+
223
+ class CompositeVisiblyPushdownAutomaton(VisiblyPushdownAutomaton):
224
+ """Lazy VPA language expression built from standard closure operations.
225
+
226
+ Composite VPAs keep exact language semantics for operations whose concrete
227
+ graph construction would otherwise need a larger normalization pass. They
228
+ still expose the regular VPA membership API through :meth:`recognizes`.
229
+ """
230
+
231
+ operation: str
232
+ operands: tuple[VisiblyPushdownAutomaton, ...]
233
+
234
+ def __init__(
235
+ self,
236
+ *,
237
+ operation: str,
238
+ operands: Iterable[VisiblyPushdownAutomaton],
239
+ ) -> None:
240
+ operands = tuple(operands)
241
+ if not operands:
242
+ raise ValueError("CompositeVisiblyPushdownAutomaton requires at least one operand")
243
+ call_alphabet, return_alphabet, internal_alphabet = _merge_visible_alphabets(operands)
244
+ super().__init__(
245
+ call_alphabet=call_alphabet,
246
+ return_alphabet=return_alphabet,
247
+ internal_alphabet=internal_alphabet,
248
+ stack_alphabet=frozenset(),
249
+ )
250
+ self.operation = operation
251
+ self.operands = operands
252
+
253
+ def validate(self) -> None:
254
+ self._require(
255
+ self.operation in {"union", "intersection", "complement", "difference", "concat", "kleene_star"},
256
+ f"unknown composite VPA operation {self.operation!r}",
257
+ )
258
+ if self.operation in {"complement", "kleene_star"}:
259
+ self._require(len(self.operands) == 1, f"{self.operation} requires one operand")
260
+ elif self.operation in {"difference", "concat"}:
261
+ self._require(len(self.operands) == 2, f"{self.operation} requires two operands")
262
+ else:
263
+ self._require(len(self.operands) >= 2, f"{self.operation} requires at least two operands")
264
+ for operand in self.operands:
265
+ operand.validate()
266
+ call_alphabet, return_alphabet, internal_alphabet = _merge_visible_alphabets(self.operands)
267
+ self._require(call_alphabet == self.call_alphabet, "composite call alphabet is stale")
268
+ self._require(return_alphabet == self.return_alphabet, "composite return alphabet is stale")
269
+ self._require(internal_alphabet == self.internal_alphabet, "composite internal alphabet is stale")
270
+
271
+ def recognizes(self, word: Sequence[Any]) -> bool:
272
+ word = tuple(word)
273
+ if any(symbol not in self.input_alphabet for symbol in word):
274
+ return False
275
+ if self.operation == "union":
276
+ return any(operand.recognizes(word) for operand in self.operands)
277
+ if self.operation == "intersection":
278
+ return all(operand.recognizes(word) for operand in self.operands)
279
+ if self.operation == "complement":
280
+ return not self.operands[0].recognizes(word)
281
+ if self.operation == "difference":
282
+ return self.operands[0].recognizes(word) and not self.operands[1].recognizes(word)
283
+ if self.operation == "concat":
284
+ left, right = self.operands
285
+ return any(
286
+ left.recognizes(word[:index]) and right.recognizes(word[index:]) for index in range(len(word) + 1)
287
+ )
288
+ if self.operation == "kleene_star":
289
+ operand = self.operands[0]
290
+ accepted = [False] * (len(word) + 1)
291
+ accepted[0] = True
292
+ for end in range(1, len(word) + 1):
293
+ accepted[end] = any(accepted[start] and operand.recognizes(word[start:end]) for start in range(end))
294
+ return accepted[-1]
295
+ raise ValueError(f"unknown composite VPA operation {self.operation!r}")
296
+
297
+
298
+ def union_vpa(
299
+ left: VisiblyPushdownAutomaton,
300
+ right: VisiblyPushdownAutomaton,
301
+ *rest: VisiblyPushdownAutomaton,
302
+ ) -> CompositeVisiblyPushdownAutomaton:
303
+ """Return a VPA recognizer for the union of the operands."""
304
+ return CompositeVisiblyPushdownAutomaton(operation="union", operands=(left, right, *rest))
305
+
306
+
307
+ def intersection_vpa(
308
+ left: VisiblyPushdownAutomaton,
309
+ right: VisiblyPushdownAutomaton,
310
+ *rest: VisiblyPushdownAutomaton,
311
+ ) -> CompositeVisiblyPushdownAutomaton:
312
+ """Return a VPA recognizer for the intersection of the operands."""
313
+ return CompositeVisiblyPushdownAutomaton(operation="intersection", operands=(left, right, *rest))
314
+
315
+
316
+ def complement_vpa(vpa: VisiblyPushdownAutomaton) -> CompositeVisiblyPushdownAutomaton:
317
+ """Return a VPA recognizer for complement over ``vpa``'s visible alphabet."""
318
+ return CompositeVisiblyPushdownAutomaton(operation="complement", operands=(vpa,))
319
+
320
+
321
+ def difference_vpa(
322
+ left: VisiblyPushdownAutomaton,
323
+ right: VisiblyPushdownAutomaton,
324
+ ) -> CompositeVisiblyPushdownAutomaton:
325
+ """Return a VPA recognizer for ``left`` minus ``right``."""
326
+ return CompositeVisiblyPushdownAutomaton(operation="difference", operands=(left, right))
327
+
328
+
329
+ def concat_vpa(
330
+ left: VisiblyPushdownAutomaton,
331
+ right: VisiblyPushdownAutomaton,
332
+ ) -> CompositeVisiblyPushdownAutomaton:
333
+ """Return a VPA recognizer for language concatenation."""
334
+ return CompositeVisiblyPushdownAutomaton(operation="concat", operands=(left, right))
335
+
336
+
337
+ def kleene_star_vpa(vpa: VisiblyPushdownAutomaton) -> CompositeVisiblyPushdownAutomaton:
338
+ """Return a VPA recognizer for Kleene star."""
339
+ return CompositeVisiblyPushdownAutomaton(operation="kleene_star", operands=(vpa,))
340
+
341
+
342
+ def _merge_visible_alphabets(
343
+ vpas: Iterable[VisiblyPushdownAutomaton],
344
+ ) -> tuple[frozenset[Any], frozenset[Any], frozenset[Any]]:
345
+ call_symbols: set[Any] = set()
346
+ return_symbols: set[Any] = set()
347
+ internal_symbols: set[Any] = set()
348
+ owners: dict[Any, str] = {}
349
+ for vpa in vpas:
350
+ for kind, symbols in (
351
+ ("call", vpa.call_alphabet),
352
+ ("return", vpa.return_alphabet),
353
+ ("internal", vpa.internal_alphabet),
354
+ ):
355
+ for symbol in symbols:
356
+ existing = owners.get(symbol)
357
+ if existing is not None and existing != kind:
358
+ raise ValueError(f"symbol {symbol!r} is both {existing} and {kind}")
359
+ owners[symbol] = kind
360
+ if kind == "call":
361
+ call_symbols.add(symbol)
362
+ elif kind == "return":
363
+ return_symbols.add(symbol)
364
+ else:
365
+ internal_symbols.add(symbol)
366
+ return frozenset(call_symbols), frozenset(return_symbols), frozenset(internal_symbols)
367
+
368
+
369
+ class DeterministicVisiblyPushdownAutomaton(VisiblyPushdownAutomaton):
370
+ """VPA with at most one enabled transition for each visible configuration."""
371
+
372
+ def validate(self) -> None:
373
+ super().validate()
374
+ if self.initial_state is None:
375
+ raise NonDeterministicError("deterministic VPA requires an initial state")
376
+ self._check_determinism()
377
+
378
+ def _check_determinism(self) -> None:
379
+ call_keys: set[tuple[Hashable, Any]] = set()
380
+ internal_keys: set[tuple[Hashable, Any]] = set()
381
+ return_keys: set[tuple[Hashable, Any, Any]] = set()
382
+ wildcard_returns: set[tuple[Hashable, Any]] = set()
383
+
384
+ for transition in self.transitions():
385
+ kind = transition.data.get(ATTR_KIND)
386
+ symbol = transition.data.get(ATTR_SYMBOL)
387
+ if symbol is None:
388
+ continue
389
+ if kind == KIND_CALL:
390
+ key = (transition.source, symbol)
391
+ if key in call_keys:
392
+ raise NonDeterministicError(f"non-deterministic call transition on {key}")
393
+ call_keys.add(key)
394
+ elif kind == KIND_INTERNAL:
395
+ key = (transition.source, symbol)
396
+ if key in internal_keys:
397
+ raise NonDeterministicError(f"non-deterministic internal transition on {key}")
398
+ internal_keys.add(key)
399
+ elif kind == KIND_RETURN:
400
+ stack_symbol = transition.data.get(ATTR_STACK_SYMBOL)
401
+ wildcard_key = (transition.source, symbol)
402
+ if stack_symbol is None:
403
+ if wildcard_key in wildcard_returns:
404
+ raise NonDeterministicError(f"duplicate wildcard return transition on {wildcard_key}")
405
+ if any(source == transition.source and ret == symbol for source, ret, _stack in return_keys):
406
+ raise NonDeterministicError(
407
+ f"wildcard return overlaps guarded return on {wildcard_key}",
408
+ )
409
+ wildcard_returns.add(wildcard_key)
410
+ else:
411
+ key = (transition.source, symbol, stack_symbol)
412
+ if wildcard_key in wildcard_returns:
413
+ raise NonDeterministicError(
414
+ f"guarded return overlaps wildcard return on {wildcard_key}",
415
+ )
416
+ if key in return_keys:
417
+ raise NonDeterministicError(f"duplicate guarded return transition on {key}")
418
+ return_keys.add(key)
419
+
420
+ def add_call_transition(
421
+ self,
422
+ source: Hashable,
423
+ target: Hashable,
424
+ symbol: Any,
425
+ stack_symbol: Any,
426
+ **attrs: Any,
427
+ ) -> int:
428
+ for transition in self.graph.out_transitions(source):
429
+ if transition.data.get(ATTR_KIND) == KIND_CALL and transition.data.get(ATTR_SYMBOL) == symbol:
430
+ raise NonDeterministicError(f"non-deterministic call transition on {(source, symbol)}")
431
+ return super().add_call_transition(source, target, symbol, stack_symbol, **attrs)
432
+
433
+ def add_return_transition(
434
+ self,
435
+ source: Hashable,
436
+ target: Hashable,
437
+ symbol: Any,
438
+ stack_symbol: Any | None = None,
439
+ **attrs: Any,
440
+ ) -> int:
441
+ for transition in self.graph.out_transitions(source):
442
+ if transition.data.get(ATTR_KIND) != KIND_RETURN or transition.data.get(ATTR_SYMBOL) != symbol:
443
+ continue
444
+ existing_stack = transition.data.get(ATTR_STACK_SYMBOL)
445
+ if existing_stack is None or stack_symbol is None or existing_stack == stack_symbol:
446
+ raise NonDeterministicError(f"non-deterministic return transition on {(source, symbol)}")
447
+ return super().add_return_transition(source, target, symbol, stack_symbol, **attrs)
448
+
449
+ def add_internal_transition(self, source: Hashable, target: Hashable, symbol: Any, **attrs: Any) -> int:
450
+ for transition in self.graph.out_transitions(source):
451
+ if transition.data.get(ATTR_KIND) == KIND_INTERNAL and transition.data.get(ATTR_SYMBOL) == symbol:
452
+ raise NonDeterministicError(f"non-deterministic internal transition on {(source, symbol)}")
453
+ return super().add_internal_transition(source, target, symbol, **attrs)
454
+
455
+ def call_successor(self, state: Hashable, symbol: Any) -> tuple[Hashable, Any] | None:
456
+ """Return ``(target, pushed_stack_symbol)`` for a deterministic call."""
457
+ return self.call_transition_map().get((state, symbol))
458
+
459
+ def internal_successor(self, state: Hashable, symbol: Any) -> Hashable | None:
460
+ """Return the deterministic internal successor, if present."""
461
+ return self.internal_transition_map().get((state, symbol))
462
+
463
+ def return_successor(self, state: Hashable, symbol: Any, stack_symbol: Any) -> Hashable | None:
464
+ """Return the deterministic return successor for ``stack_symbol``, if present."""
465
+ transitions = self.return_transition_map()
466
+ explicit = transitions.get((state, symbol, stack_symbol), _MISSING)
467
+ if explicit is not _MISSING:
468
+ return explicit
469
+ wildcard = transitions.get((state, symbol, None), _MISSING)
470
+ if wildcard is not _MISSING:
471
+ return wildcard
472
+ return None
473
+
474
+ @classmethod
475
+ def from_vpa(cls, vpa: VisiblyPushdownAutomaton) -> DeterministicVisiblyPushdownAutomaton:
476
+ """Copy ``vpa`` into a deterministic VPA and validate determinism."""
477
+ result = cls(
478
+ input_alphabet=vpa.input_alphabet,
479
+ call_alphabet=vpa.call_alphabet,
480
+ return_alphabet=vpa.return_alphabet,
481
+ internal_alphabet=vpa.internal_alphabet,
482
+ stack_alphabet=vpa.stack_alphabet,
483
+ bottom_stack_symbol=vpa.bottom_stack_symbol,
484
+ initial_state=vpa.initial_state,
485
+ accepting_states=vpa.accepting_states,
486
+ graph=vpa.graph.copy(),
487
+ )
488
+ result.validate()
489
+ return result
490
+
491
+
492
+ class CallDrivenAutomaton(DeterministicVisiblyPushdownAutomaton):
493
+ """Deterministic modular VPA whose call target depends only on the call symbol."""
494
+
495
+ modules: dict[Hashable, frozenset[Hashable]]
496
+ base_module: Hashable
497
+ call_partition: dict[Any, Hashable]
498
+ call_entries: dict[Any, Hashable]
499
+
500
+ def __init__(
501
+ self,
502
+ *,
503
+ modules: Mapping[Hashable, Iterable[Hashable]] | None = None,
504
+ base_module: Hashable = 0,
505
+ call_partition: Mapping[Any, Hashable] | None = None,
506
+ call_entries: Mapping[Any, Hashable] | None = None,
507
+ **kwargs: Any,
508
+ ) -> None:
509
+ super().__init__(**kwargs)
510
+ self.modules = _normalize_modules(modules)
511
+ self.base_module = base_module
512
+ self.call_partition = dict(call_partition or {})
513
+ self.call_entries = dict(call_entries or {})
514
+
515
+ def validate(self) -> None:
516
+ super().validate()
517
+ state_modules = self._validate_modules()
518
+ self._validate_call_partition()
519
+ self._validate_internal_transitions_stay_in_module(state_modules)
520
+ self._validate_call_driven_transitions()
521
+
522
+ def call_entry_map(self) -> dict[Any, Hashable]:
523
+ """Return configured or inferred entries for each call symbol with transitions."""
524
+ entries = dict(self.call_entries)
525
+ for transition in self.transitions():
526
+ if transition.data.get(ATTR_KIND) != KIND_CALL:
527
+ continue
528
+ symbol = transition.data.get(ATTR_SYMBOL)
529
+ if symbol is None:
530
+ continue
531
+ existing = entries.get(symbol, _MISSING)
532
+ if existing is _MISSING:
533
+ entries[symbol] = transition.target
534
+ elif existing != transition.target:
535
+ raise NonDeterministicError(f"call target for {symbol!r} depends on source state")
536
+ return entries
537
+
538
+ @classmethod
539
+ def minimize(
540
+ cls,
541
+ vpa: VisiblyPushdownAutomaton,
542
+ *,
543
+ modules: Mapping[Hashable, Iterable[Hashable]] | None = None,
544
+ call_partition: Mapping[Any, Hashable] | None = None,
545
+ base_module: Hashable | None = None,
546
+ call_entries: Mapping[Any, Hashable] | None = None,
547
+ ) -> CallDrivenAutomaton:
548
+ """Return the module-aware deterministic quotient as a CDA."""
549
+ return _minimize_modular_vpa(
550
+ cls,
551
+ vpa,
552
+ modules=modules,
553
+ call_partition=call_partition,
554
+ base_module=base_module,
555
+ call_entries=call_entries,
556
+ entry_states=None,
557
+ form="cda",
558
+ )
559
+
560
+ def _validate_modules(self) -> dict[Hashable, Hashable]:
561
+ self._require(bool(self.modules), "modular VPA requires modules")
562
+ self._require(self.base_module in self.modules, "base_module must be present in modules")
563
+ all_states = set(self.states())
564
+ seen: dict[Hashable, Hashable] = {}
565
+ for module, states in self.modules.items():
566
+ self._require(bool(states), f"module {module!r} must contain at least one state")
567
+ for state in states:
568
+ self._require(state in all_states, f"module {module!r} contains unknown state {state!r}")
569
+ self._require(state not in seen, f"state {state!r} appears in multiple modules")
570
+ seen[state] = module
571
+ self._require(set(seen) == all_states, "modules must cover exactly the VPA states")
572
+ if self.initial_state is not None:
573
+ self._require(
574
+ self.initial_state in self.modules[self.base_module],
575
+ "initial_state must lie in the base module",
576
+ )
577
+ return seen
578
+
579
+ def _validate_call_partition(self) -> None:
580
+ missing = self.call_alphabet - set(self.call_partition)
581
+ extra = set(self.call_partition) - self.call_alphabet
582
+ self._require(not missing, f"call_partition missing calls {sorted(missing, key=repr)!r}")
583
+ self._require(not extra, f"call_partition contains non-call symbols {sorted(extra, key=repr)!r}")
584
+ for symbol, module in self.call_partition.items():
585
+ self._require(module in self.modules, f"call {symbol!r} targets unknown module {module!r}")
586
+
587
+ def _validate_internal_transitions_stay_in_module(self, state_modules: Mapping[Hashable, Hashable]) -> None:
588
+ for transition in self.transitions():
589
+ if transition.data.get(ATTR_KIND) == KIND_INTERNAL:
590
+ self._require(
591
+ state_modules[transition.source] == state_modules[transition.target],
592
+ "internal transitions must stay inside one module",
593
+ )
594
+
595
+ def _validate_call_driven_transitions(self) -> None:
596
+ entries = self.call_entry_map()
597
+ for symbol, target in entries.items():
598
+ module = self.call_partition[symbol]
599
+ self._require(target in self.modules[module], f"call {symbol!r} entry is not in module {module!r}")
600
+ for transition in self.transitions():
601
+ if transition.data.get(ATTR_KIND) != KIND_CALL:
602
+ continue
603
+ symbol = transition.data.get(ATTR_SYMBOL)
604
+ if symbol is None:
605
+ continue
606
+ self._require(
607
+ transition.target == entries[symbol],
608
+ f"call target for {symbol!r} must be independent of source state",
609
+ )
610
+
611
+
612
+ class MultipleEntryVisiblyPushdownAutomaton(CallDrivenAutomaton):
613
+ """Modular VPA with multiple module entries and source-determined call pushes."""
614
+
615
+ entry_states: dict[Hashable, frozenset[Hashable]]
616
+
617
+ def __init__(
618
+ self,
619
+ *,
620
+ entry_states: Mapping[Hashable, Iterable[Hashable] | Hashable] | None = None,
621
+ **kwargs: Any,
622
+ ) -> None:
623
+ super().__init__(**kwargs)
624
+ self.entry_states = _normalize_multi_entries(entry_states)
625
+
626
+ def validate(self) -> None:
627
+ super().validate()
628
+ self._validate_entry_states()
629
+ self._validate_call_targets_are_entries()
630
+ self._validate_source_determined_pushes()
631
+
632
+ @classmethod
633
+ def minimize(
634
+ cls,
635
+ vpa: VisiblyPushdownAutomaton,
636
+ *,
637
+ modules: Mapping[Hashable, Iterable[Hashable]] | None = None,
638
+ call_partition: Mapping[Any, Hashable] | None = None,
639
+ base_module: Hashable | None = None,
640
+ entry_states: Mapping[Hashable, Iterable[Hashable] | Hashable] | None = None,
641
+ call_entries: Mapping[Any, Hashable] | None = None,
642
+ ) -> MultipleEntryVisiblyPushdownAutomaton:
643
+ """Return the module-aware deterministic quotient as an MEVPA."""
644
+ return _minimize_modular_vpa(
645
+ cls,
646
+ vpa,
647
+ modules=modules,
648
+ call_partition=call_partition,
649
+ base_module=base_module,
650
+ call_entries=call_entries,
651
+ entry_states=entry_states,
652
+ form="mevpa",
653
+ )
654
+
655
+ def _validate_entry_states(self) -> None:
656
+ self._require(bool(self.entry_states), "MEVPA requires entry_states")
657
+ for module, states in self.entry_states.items():
658
+ self._require(module in self.modules, f"entry_states has unknown module {module!r}")
659
+ self._require(bool(states), f"module {module!r} must have at least one entry")
660
+ for state in states:
661
+ self._require(state in self.modules[module], f"entry {state!r} is not in module {module!r}")
662
+
663
+ def _validate_call_targets_are_entries(self) -> None:
664
+ entries = self.call_entry_map()
665
+ for symbol, target in entries.items():
666
+ module = self.call_partition[symbol]
667
+ self._require(
668
+ target in self.entry_states.get(module, frozenset()),
669
+ f"call {symbol!r} must enter one of module {module!r}'s entries",
670
+ )
671
+
672
+ def _validate_source_determined_pushes(self) -> None:
673
+ pushed_by_source: dict[Hashable, Any] = {}
674
+ for transition in self.transitions():
675
+ if transition.data.get(ATTR_KIND) != KIND_CALL:
676
+ continue
677
+ pushed = transition.data.get(ATTR_STACK_SYMBOL)
678
+ existing = pushed_by_source.get(transition.source, _MISSING)
679
+ if existing is _MISSING:
680
+ pushed_by_source[transition.source] = pushed
681
+ else:
682
+ self._require(existing == pushed, "MEVPA call push must depend only on the source state")
683
+
684
+
685
+ class SingleEntryVisiblyPushdownAutomaton(CallDrivenAutomaton):
686
+ """Modular VPA with one distinguished entry per non-base module."""
687
+
688
+ entry_states: dict[Hashable, Hashable]
689
+
690
+ def __init__(
691
+ self,
692
+ *,
693
+ entry_states: Mapping[Hashable, Hashable] | None = None,
694
+ **kwargs: Any,
695
+ ) -> None:
696
+ super().__init__(**kwargs)
697
+ self.entry_states = dict(entry_states or {})
698
+
699
+ def validate(self) -> None:
700
+ super().validate()
701
+ self._validate_single_entries()
702
+ self._validate_single_entry_calls()
703
+
704
+ @classmethod
705
+ def minimize(
706
+ cls,
707
+ vpa: VisiblyPushdownAutomaton,
708
+ *,
709
+ call_partition: Mapping[Any, Hashable] | None = None,
710
+ modules: Mapping[Hashable, Iterable[Hashable]] | None = None,
711
+ base_module: Hashable | None = None,
712
+ entry_states: Mapping[Hashable, Hashable] | None = None,
713
+ call_entries: Mapping[Any, Hashable] | None = None,
714
+ ) -> SingleEntryVisiblyPushdownAutomaton:
715
+ """Return the module-aware deterministic quotient as an SEVPA.
716
+
717
+ A fixed call partition and module structure are required. General VPA
718
+ minimization is intentionally not attempted here.
719
+ """
720
+ return _minimize_modular_vpa(
721
+ cls,
722
+ vpa,
723
+ modules=modules,
724
+ call_partition=call_partition,
725
+ base_module=base_module,
726
+ call_entries=call_entries,
727
+ entry_states=entry_states,
728
+ form="sevpa",
729
+ )
730
+
731
+ def _validate_single_entries(self) -> None:
732
+ missing = set(self.modules) - {self.base_module} - set(self.entry_states)
733
+ self._require(not missing, f"SEVPA missing entries for modules {sorted(missing, key=repr)!r}")
734
+ for module, state in self.entry_states.items():
735
+ self._require(module in self.modules, f"entry_states has unknown module {module!r}")
736
+ self._require(state in self.modules[module], f"entry {state!r} is not in module {module!r}")
737
+
738
+ def _validate_single_entry_calls(self) -> None:
739
+ for transition in self.transitions():
740
+ if transition.data.get(ATTR_KIND) != KIND_CALL:
741
+ continue
742
+ symbol = transition.data.get(ATTR_SYMBOL)
743
+ if symbol is None:
744
+ continue
745
+ module = self.call_partition[symbol]
746
+ expected_entry = self.entry_states.get(module)
747
+ self._require(
748
+ transition.target == expected_entry,
749
+ f"SEVPA call {symbol!r} must enter module {module!r}'s single entry",
750
+ )
751
+ self._require(
752
+ transition.data.get(ATTR_STACK_SYMBOL) == (transition.source, symbol),
753
+ "SEVPA call stack symbols must be (caller_state, call_symbol)",
754
+ )
755
+
756
+
757
+ class CanonicalVisiblyPushdownAutomaton(DeterministicVisiblyPushdownAutomaton):
758
+ """Canonical VPA built from the finite Myhill-Nerode summary algebra."""
759
+
760
+ summary_representatives: dict[Hashable, tuple[int | None, ...]]
761
+
762
+ def __init__(
763
+ self,
764
+ *,
765
+ summary_representatives: Mapping[Hashable, tuple[int | None, ...]] | None = None,
766
+ **kwargs: Any,
767
+ ) -> None:
768
+ super().__init__(**kwargs)
769
+ self.summary_representatives = dict(summary_representatives or {})
770
+
771
+ @classmethod
772
+ def from_vpa(cls, vpa: VisiblyPushdownAutomaton) -> CanonicalVisiblyPushdownAutomaton:
773
+ """Build the Myhill-Nerode canonical deterministic VPA for ``vpa``.
774
+
775
+ The construction is finite for deterministic VPAs because states are
776
+ summary classes of well-matched factors. With an empty call alphabet,
777
+ this specializes to the usual minimal DFA right-congruence construction.
778
+ """
779
+ det = DeterministicVisiblyPushdownAutomaton.from_vpa(vpa)
780
+ algebra = _SummaryAlgebra.from_vpa(det)
781
+ return algebra.to_canonical_vpa(cls, det)
782
+
783
+ @classmethod
784
+ def minimize(cls, vpa: VisiblyPushdownAutomaton) -> CanonicalVisiblyPushdownAutomaton:
785
+ """Alias for :meth:`from_vpa`."""
786
+ return cls.from_vpa(vpa)
787
+
788
+
789
+ def _normalize_modules(modules: Mapping[Hashable, Iterable[Hashable]] | None) -> dict[Hashable, frozenset[Hashable]]:
790
+ if modules is None:
791
+ return {}
792
+ return {module: frozenset(states) for module, states in modules.items()}
793
+
794
+
795
+ def _normalize_multi_entries(
796
+ entry_states: Mapping[Hashable, Iterable[Hashable] | Hashable] | None,
797
+ ) -> dict[Hashable, frozenset[Hashable]]:
798
+ if entry_states is None:
799
+ return {}
800
+ result: dict[Hashable, frozenset[Hashable]] = {}
801
+ for module, states in entry_states.items():
802
+ if isinstance(states, frozenset | set | list):
803
+ result[module] = frozenset(states)
804
+ else:
805
+ result[module] = frozenset({states})
806
+ return result
807
+
808
+
809
+ def _metadata_or_argument(vpa: VisiblyPushdownAutomaton, name: str, value: Any, default: Any) -> Any:
810
+ if value is not None:
811
+ return value
812
+ return getattr(vpa, name, default)
813
+
814
+
815
+ def _deterministic_view(vpa: VisiblyPushdownAutomaton) -> DeterministicVisiblyPushdownAutomaton:
816
+ return DeterministicVisiblyPushdownAutomaton.from_vpa(vpa)
817
+
818
+
819
+ def _minimize_modular_vpa(
820
+ target_cls: type[CallDrivenAutomaton],
821
+ vpa: VisiblyPushdownAutomaton,
822
+ *,
823
+ modules: Mapping[Hashable, Iterable[Hashable]] | None,
824
+ call_partition: Mapping[Any, Hashable] | None,
825
+ base_module: Hashable | None,
826
+ call_entries: Mapping[Any, Hashable] | None,
827
+ entry_states: Mapping[Hashable, Any] | None,
828
+ form: str,
829
+ ) -> Any:
830
+ modules = _metadata_or_argument(vpa, "modules", modules, None)
831
+ call_partition = _metadata_or_argument(vpa, "call_partition", call_partition, None)
832
+ base_module = _metadata_or_argument(vpa, "base_module", base_module, 0)
833
+ call_entries = _metadata_or_argument(vpa, "call_entries", call_entries, None)
834
+ entry_states = _metadata_or_argument(vpa, "entry_states", entry_states, None)
835
+
836
+ if modules is None or call_partition is None:
837
+ raise NotImplementedError("modular VPA minimization requires fixed modules and call_partition")
838
+
839
+ det = _deterministic_view(vpa)
840
+ modules = _normalize_modules(modules)
841
+ call_partition = dict(call_partition)
842
+ call_entries = dict(call_entries or _infer_call_entries(det, call_partition))
843
+ entry_states = _infer_entry_states(form, modules, base_module, call_partition, call_entries, entry_states, det)
844
+
845
+ source = _construct_modular_view(
846
+ target_cls,
847
+ det,
848
+ modules=modules,
849
+ base_module=base_module,
850
+ call_partition=call_partition,
851
+ call_entries=call_entries,
852
+ entry_states=entry_states,
853
+ )
854
+ source.validate()
855
+
856
+ partition = _refine_modular_partition(source, modules)
857
+ return _quotient_modular_vpa(
858
+ target_cls,
859
+ source,
860
+ partition,
861
+ modules=modules,
862
+ base_module=base_module,
863
+ call_partition=call_partition,
864
+ call_entries=call_entries,
865
+ entry_states=entry_states,
866
+ form=form,
867
+ )
868
+
869
+
870
+ def _construct_modular_view(
871
+ target_cls: type[CallDrivenAutomaton],
872
+ det: DeterministicVisiblyPushdownAutomaton,
873
+ *,
874
+ modules: Mapping[Hashable, frozenset[Hashable]],
875
+ base_module: Hashable,
876
+ call_partition: Mapping[Any, Hashable],
877
+ call_entries: Mapping[Any, Hashable],
878
+ entry_states: Mapping[Hashable, Any],
879
+ ) -> CallDrivenAutomaton:
880
+ kwargs = {
881
+ "input_alphabet": det.input_alphabet,
882
+ "call_alphabet": det.call_alphabet,
883
+ "return_alphabet": det.return_alphabet,
884
+ "internal_alphabet": det.internal_alphabet,
885
+ "stack_alphabet": det.stack_alphabet,
886
+ "bottom_stack_symbol": det.bottom_stack_symbol,
887
+ "initial_state": det.initial_state,
888
+ "accepting_states": det.accepting_states,
889
+ "graph": det.graph.copy(),
890
+ "modules": modules,
891
+ "base_module": base_module,
892
+ "call_partition": call_partition,
893
+ "call_entries": call_entries,
894
+ }
895
+ if issubclass(target_cls, (SingleEntryVisiblyPushdownAutomaton, MultipleEntryVisiblyPushdownAutomaton)):
896
+ kwargs["entry_states"] = entry_states
897
+ return target_cls(**kwargs)
898
+
899
+
900
+ def _infer_call_entries(
901
+ det: DeterministicVisiblyPushdownAutomaton,
902
+ call_partition: Mapping[Any, Hashable],
903
+ ) -> dict[Any, Hashable]:
904
+ entries: dict[Any, Hashable] = {}
905
+ for (_source, symbol), (target, _stack) in det.call_transition_map().items():
906
+ if symbol not in call_partition:
907
+ raise NotImplementedError(f"call_partition missing call symbol {symbol!r}")
908
+ existing = entries.get(symbol, _MISSING)
909
+ if existing is _MISSING:
910
+ entries[symbol] = target
911
+ elif existing != target:
912
+ raise NotImplementedError(f"call target for {symbol!r} depends on source state")
913
+ return entries
914
+
915
+
916
+ def _infer_entry_states(
917
+ form: str,
918
+ modules: Mapping[Hashable, frozenset[Hashable]],
919
+ base_module: Hashable,
920
+ call_partition: Mapping[Any, Hashable],
921
+ call_entries: Mapping[Any, Hashable],
922
+ entry_states: Mapping[Hashable, Any] | None,
923
+ det: DeterministicVisiblyPushdownAutomaton,
924
+ ) -> dict[Hashable, Any]:
925
+ if entry_states is not None:
926
+ if form == "mevpa":
927
+ return _normalize_multi_entries(entry_states)
928
+ return dict(entry_states)
929
+
930
+ by_module: dict[Hashable, set[Hashable]] = {module: set() for module in modules}
931
+ if det.initial_state is not None and base_module in by_module:
932
+ by_module[base_module].add(det.initial_state)
933
+ for symbol, entry in call_entries.items():
934
+ by_module[call_partition[symbol]].add(entry)
935
+
936
+ if form == "mevpa":
937
+ return {module: frozenset(states) for module, states in by_module.items() if states}
938
+ if form == "sevpa":
939
+ result: dict[Hashable, Hashable] = {}
940
+ for module, states in by_module.items():
941
+ if module == base_module:
942
+ continue
943
+ if len(states) != 1:
944
+ raise NotImplementedError("SEVPA minimization requires one inferred entry per non-base module")
945
+ result[module] = next(iter(states))
946
+ return result
947
+ return {}
948
+
949
+
950
+ def _refine_modular_partition(
951
+ vpa: CallDrivenAutomaton,
952
+ modules: Mapping[Hashable, frozenset[Hashable]],
953
+ ) -> list[frozenset[Hashable]]:
954
+ partition: list[frozenset[Hashable]] = []
955
+ for _module, states in sorted(modules.items(), key=lambda item: repr(item[0])):
956
+ accepting = frozenset(states & vpa.accepting_states)
957
+ rejecting = frozenset(states - vpa.accepting_states)
958
+ if accepting:
959
+ partition.append(accepting)
960
+ if rejecting:
961
+ partition.append(rejecting)
962
+
963
+ changed = True
964
+ while changed:
965
+ changed = False
966
+ block_of = _block_map(partition)
967
+ context_groups = _stack_context_groups(vpa, block_of)
968
+ new_partition: list[frozenset[Hashable]] = []
969
+ for block in partition:
970
+ pieces: dict[tuple[Any, ...], set[Hashable]] = {}
971
+ for state in block:
972
+ signature = _modular_state_signature(vpa, state, block_of, context_groups)
973
+ pieces.setdefault(signature, set()).add(state)
974
+ if len(pieces) > 1:
975
+ changed = True
976
+ new_partition.extend(frozenset(piece) for piece in pieces.values())
977
+ partition = new_partition
978
+ return partition
979
+
980
+
981
+ def _block_map(partition: Sequence[frozenset[Hashable]]) -> dict[Hashable, frozenset[Hashable]]:
982
+ return {state: block for block in partition for state in block}
983
+
984
+
985
+ def _stack_context_groups(
986
+ vpa: DeterministicVisiblyPushdownAutomaton,
987
+ block_of: Mapping[Hashable, Hashable],
988
+ ) -> list[tuple[Any, tuple[Any, ...]]]:
989
+ stack_symbols = {stack for _key, (_target, stack) in vpa.call_transition_map().items()}
990
+ if vpa.bottom_stack_symbol is not None:
991
+ stack_symbols.add(vpa.bottom_stack_symbol)
992
+ grouped: dict[Any, set[Any]] = {}
993
+ for stack_symbol in stack_symbols:
994
+ canonical = _canonical_stack_symbol(stack_symbol, block_of)
995
+ grouped.setdefault(canonical, set()).add(stack_symbol)
996
+ return [(canonical, tuple(sorted(actuals, key=repr))) for canonical, actuals in sorted(grouped.items(), key=repr)]
997
+
998
+
999
+ def _modular_state_signature(
1000
+ vpa: CallDrivenAutomaton,
1001
+ state: Hashable,
1002
+ block_of: Mapping[Hashable, Hashable],
1003
+ context_groups: Sequence[tuple[Any, tuple[Any, ...]]],
1004
+ ) -> tuple[Any, ...]:
1005
+ state_modules = {state: module for module, states in vpa.modules.items() for state in states}
1006
+ call_map = vpa.call_transition_map()
1007
+ internal_map = vpa.internal_transition_map()
1008
+ return_map = vpa.return_transition_map()
1009
+
1010
+ internal = tuple(
1011
+ (
1012
+ symbol,
1013
+ None if (target := internal_map.get((state, symbol))) is None else block_of[target],
1014
+ )
1015
+ for symbol in sorted(vpa.internal_alphabet, key=repr)
1016
+ )
1017
+ calls = tuple(
1018
+ (
1019
+ symbol,
1020
+ None
1021
+ if (call := call_map.get((state, symbol))) is None
1022
+ else (block_of[call[0]], _canonical_stack_symbol(call[1], block_of)),
1023
+ )
1024
+ for symbol in sorted(vpa.call_alphabet, key=repr)
1025
+ )
1026
+ returns = []
1027
+ for symbol in sorted(vpa.return_alphabet, key=repr):
1028
+ for canonical_stack, actual_stacks in context_groups:
1029
+ targets = []
1030
+ for stack_symbol in actual_stacks:
1031
+ target = return_map.get((state, symbol, stack_symbol), return_map.get((state, symbol, None)))
1032
+ targets.append(None if target is None else block_of[target])
1033
+ returns.append((symbol, canonical_stack, tuple(sorted(set(targets), key=repr))))
1034
+
1035
+ return (
1036
+ state_modules[state],
1037
+ state in vpa.accepting_states,
1038
+ internal,
1039
+ calls,
1040
+ tuple(returns),
1041
+ )
1042
+
1043
+
1044
+ def _quotient_modular_vpa(
1045
+ target_cls: type[CallDrivenAutomaton],
1046
+ source: CallDrivenAutomaton,
1047
+ partition: Sequence[frozenset[Hashable]],
1048
+ *,
1049
+ modules: Mapping[Hashable, frozenset[Hashable]],
1050
+ base_module: Hashable,
1051
+ call_partition: Mapping[Any, Hashable],
1052
+ call_entries: Mapping[Any, Hashable],
1053
+ entry_states: Mapping[Hashable, Any],
1054
+ form: str,
1055
+ ) -> Any:
1056
+ block_of = _block_map(partition)
1057
+ blocks = list(partition)
1058
+ stack_alphabet: set[Any] = set()
1059
+ if source.bottom_stack_symbol is not None:
1060
+ stack_alphabet.add(source.bottom_stack_symbol)
1061
+ transitions: set[tuple[Hashable, Hashable, Any, Any, Any | None]] = set()
1062
+ stack_symbol_rewrite: dict[Any, set[Any]] = {}
1063
+
1064
+ for transition in source.transitions():
1065
+ if transition.data.get(ATTR_KIND) != KIND_CALL:
1066
+ continue
1067
+ symbol = transition.data.get(ATTR_SYMBOL)
1068
+ stack_symbol = transition.data.get(ATTR_STACK_SYMBOL)
1069
+ quotient_stack_symbol = _quotient_call_stack_symbol(
1070
+ form,
1071
+ block_of[transition.source],
1072
+ symbol,
1073
+ stack_symbol,
1074
+ block_of,
1075
+ )
1076
+ stack_symbol_rewrite.setdefault(stack_symbol, set()).add(quotient_stack_symbol)
1077
+
1078
+ for block in blocks:
1079
+ representative = min(block, key=repr)
1080
+ source_block = block_of[representative]
1081
+ for transition in source.graph.out_transitions(representative):
1082
+ kind = transition.data.get(ATTR_KIND)
1083
+ symbol = transition.data.get(ATTR_SYMBOL)
1084
+ target_block = block_of[transition.target]
1085
+ stack_symbol = transition.data.get(ATTR_STACK_SYMBOL)
1086
+ if kind == KIND_CALL:
1087
+ stack_symbol = _quotient_call_stack_symbol(form, source_block, symbol, stack_symbol, block_of)
1088
+ stack_alphabet.add(stack_symbol)
1089
+ elif kind == KIND_RETURN and stack_symbol is not None:
1090
+ rewritten = stack_symbol_rewrite.get(stack_symbol, {_canonical_stack_symbol(stack_symbol, block_of)})
1091
+ for rewritten_stack_symbol in rewritten:
1092
+ stack_alphabet.add(rewritten_stack_symbol)
1093
+ transitions.add((source_block, target_block, kind, symbol, rewritten_stack_symbol))
1094
+ continue
1095
+ transitions.add((source_block, target_block, kind, symbol, stack_symbol))
1096
+
1097
+ quotient_modules = {
1098
+ module: frozenset(block for block in blocks if block & states)
1099
+ for module, states in modules.items()
1100
+ if any(block & states for block in blocks)
1101
+ }
1102
+ quotient_call_entries = {symbol: block_of[state] for symbol, state in call_entries.items() if state in block_of}
1103
+ quotient_entry_states = _quotient_entry_states(form, entry_states, block_of)
1104
+
1105
+ kwargs = {
1106
+ "input_alphabet": source.input_alphabet,
1107
+ "call_alphabet": source.call_alphabet,
1108
+ "return_alphabet": source.return_alphabet,
1109
+ "internal_alphabet": source.internal_alphabet,
1110
+ "stack_alphabet": frozenset(stack_alphabet),
1111
+ "bottom_stack_symbol": source.bottom_stack_symbol,
1112
+ "initial_state": block_of[source.initial_state],
1113
+ "accepting_states": frozenset(block for block in blocks if block & source.accepting_states),
1114
+ "modules": quotient_modules,
1115
+ "base_module": base_module,
1116
+ "call_partition": dict(call_partition),
1117
+ "call_entries": quotient_call_entries,
1118
+ }
1119
+ if issubclass(target_cls, (SingleEntryVisiblyPushdownAutomaton, MultipleEntryVisiblyPushdownAutomaton)):
1120
+ kwargs["entry_states"] = quotient_entry_states
1121
+
1122
+ result = target_cls(**kwargs)
1123
+ for block in blocks:
1124
+ result.graph.add_state(block)
1125
+ for source_block, target_block, kind, symbol, stack_symbol in sorted(transitions, key=repr):
1126
+ if kind == KIND_CALL:
1127
+ result.add_call_transition(source_block, target_block, symbol, stack_symbol)
1128
+ elif kind == KIND_RETURN:
1129
+ result.add_return_transition(source_block, target_block, symbol, stack_symbol)
1130
+ elif kind == KIND_INTERNAL:
1131
+ result.add_internal_transition(source_block, target_block, symbol)
1132
+ result.validate()
1133
+ return result
1134
+
1135
+
1136
+ def _quotient_call_stack_symbol(
1137
+ form: str,
1138
+ source_block: Hashable,
1139
+ symbol: Any,
1140
+ stack_symbol: Any,
1141
+ block_of: Mapping[Hashable, Hashable],
1142
+ ) -> Any:
1143
+ if form == "sevpa":
1144
+ return (source_block, symbol)
1145
+ if form == "mevpa":
1146
+ return source_block
1147
+ return _canonical_stack_symbol(stack_symbol, block_of)
1148
+
1149
+
1150
+ def _quotient_entry_states(
1151
+ form: str,
1152
+ entry_states: Mapping[Hashable, Any],
1153
+ block_of: Mapping[Hashable, Hashable],
1154
+ ) -> dict[Hashable, Any]:
1155
+ if form == "mevpa":
1156
+ normalized = _normalize_multi_entries(entry_states)
1157
+ return {
1158
+ module: frozenset(block_of[state] for state in states if state in block_of)
1159
+ for module, states in normalized.items()
1160
+ }
1161
+ if form == "sevpa":
1162
+ return {module: block_of[state] for module, state in entry_states.items() if state in block_of}
1163
+ return {}
1164
+
1165
+
1166
+ def _canonical_stack_symbol(stack_symbol: Any, block_of: Mapping[Hashable, Hashable]) -> Any:
1167
+ if stack_symbol in block_of:
1168
+ return block_of[stack_symbol]
1169
+ if isinstance(stack_symbol, tuple):
1170
+ return tuple(_canonical_stack_symbol(part, block_of) for part in stack_symbol)
1171
+ return stack_symbol
1172
+
1173
+
1174
+ class _SummaryAlgebra:
1175
+ def __init__(
1176
+ self,
1177
+ *,
1178
+ state_order: tuple[Hashable, ...],
1179
+ summaries: frozenset[tuple[int | None, ...]],
1180
+ identity: tuple[int | None, ...],
1181
+ internal_summaries: Mapping[Any, tuple[int | None, ...]],
1182
+ class_of: Mapping[tuple[int | None, ...], int],
1183
+ representatives: Mapping[int, tuple[int | None, ...]],
1184
+ ) -> None:
1185
+ self.state_order = state_order
1186
+ self.summaries = summaries
1187
+ self.identity = identity
1188
+ self.internal_summaries = dict(internal_summaries)
1189
+ self.class_of = dict(class_of)
1190
+ self.representatives = dict(representatives)
1191
+
1192
+ @classmethod
1193
+ def from_vpa(cls, vpa: DeterministicVisiblyPushdownAutomaton) -> _SummaryAlgebra:
1194
+ state_order = tuple(sorted(vpa.states(), key=repr))
1195
+ state_index = {state: index for index, state in enumerate(state_order)}
1196
+ internal_summaries = {
1197
+ symbol: _internal_summary(vpa, state_order, state_index, symbol)
1198
+ for symbol in sorted(vpa.internal_alphabet, key=repr)
1199
+ }
1200
+ identity = tuple(range(len(state_order)))
1201
+ summaries = _close_summary_algebra(vpa, state_order, state_index, identity, internal_summaries)
1202
+ class_of, representatives = _quotient_summaries(vpa, state_order, state_index, summaries, identity)
1203
+ return cls(
1204
+ state_order=state_order,
1205
+ summaries=frozenset(summaries),
1206
+ identity=identity,
1207
+ internal_summaries=internal_summaries,
1208
+ class_of=class_of,
1209
+ representatives=representatives,
1210
+ )
1211
+
1212
+ def to_canonical_vpa(
1213
+ self,
1214
+ cls: type[CanonicalVisiblyPushdownAutomaton],
1215
+ source: DeterministicVisiblyPushdownAutomaton,
1216
+ ) -> CanonicalVisiblyPushdownAutomaton:
1217
+ identity_class = self.class_of[self.identity]
1218
+ states = frozenset(self.representatives)
1219
+ stack_alphabet = frozenset(
1220
+ (summary_class, symbol) for summary_class in states for symbol in source.call_alphabet
1221
+ )
1222
+ accepting_states = frozenset(
1223
+ summary_class
1224
+ for summary_class, summary in self.representatives.items()
1225
+ if _summary_accepts(source, self.state_order, summary)
1226
+ )
1227
+ result = cls(
1228
+ input_alphabet=source.input_alphabet,
1229
+ call_alphabet=source.call_alphabet,
1230
+ return_alphabet=source.return_alphabet,
1231
+ internal_alphabet=source.internal_alphabet,
1232
+ stack_alphabet=stack_alphabet,
1233
+ bottom_stack_symbol=None,
1234
+ initial_state=identity_class,
1235
+ accepting_states=accepting_states,
1236
+ summary_representatives=self.representatives,
1237
+ )
1238
+ for state in states:
1239
+ result.graph.add_state(state)
1240
+
1241
+ for summary_class, summary in sorted(self.representatives.items(), key=repr):
1242
+ for symbol, internal in sorted(self.internal_summaries.items(), key=lambda item: repr(item[0])):
1243
+ target_summary = _compose_summary(summary, internal)
1244
+ result.add_internal_transition(summary_class, self.class_of[target_summary], symbol)
1245
+ for symbol in sorted(source.call_alphabet, key=repr):
1246
+ result.add_call_transition(summary_class, identity_class, symbol, (summary_class, symbol))
1247
+
1248
+ for inner_class, inner in sorted(self.representatives.items(), key=repr):
1249
+ for outer_class, outer in sorted(self.representatives.items(), key=repr):
1250
+ for call_symbol in sorted(source.call_alphabet, key=repr):
1251
+ for return_symbol in sorted(source.return_alphabet, key=repr):
1252
+ wrapped = _wrap_summary(source, self.state_order, inner, call_symbol, return_symbol)
1253
+ target = _compose_summary(outer, wrapped)
1254
+ result.add_return_transition(
1255
+ inner_class,
1256
+ self.class_of[target],
1257
+ return_symbol,
1258
+ (outer_class, call_symbol),
1259
+ )
1260
+
1261
+ result.validate()
1262
+ return result
1263
+
1264
+
1265
+ def _internal_summary(
1266
+ vpa: DeterministicVisiblyPushdownAutomaton,
1267
+ state_order: Sequence[Hashable],
1268
+ state_index: Mapping[Hashable, int],
1269
+ symbol: Any,
1270
+ ) -> tuple[int | None, ...]:
1271
+ transitions = vpa.internal_transition_map()
1272
+ summary: list[int | None] = []
1273
+ for state in state_order:
1274
+ target = transitions.get((state, symbol))
1275
+ summary.append(None if target is None else state_index[target])
1276
+ return tuple(summary)
1277
+
1278
+
1279
+ def _close_summary_algebra(
1280
+ vpa: DeterministicVisiblyPushdownAutomaton,
1281
+ state_order: Sequence[Hashable],
1282
+ state_index: Mapping[Hashable, int],
1283
+ identity: tuple[int | None, ...],
1284
+ internal_summaries: Mapping[Any, tuple[int | None, ...]],
1285
+ ) -> set[tuple[int | None, ...]]:
1286
+ summaries = {identity, *internal_summaries.values()}
1287
+ queue: deque[tuple[int | None, ...]] = deque(sorted(summaries, key=repr))
1288
+ while queue:
1289
+ summary = queue.popleft()
1290
+ current = list(summaries)
1291
+ candidates: list[tuple[int | None, ...]] = []
1292
+ for other in current:
1293
+ candidates.append(_compose_summary(summary, other))
1294
+ candidates.append(_compose_summary(other, summary))
1295
+ for call_symbol in sorted(vpa.call_alphabet, key=repr):
1296
+ for return_symbol in sorted(vpa.return_alphabet, key=repr):
1297
+ candidates.append(_wrap_summary(vpa, state_order, summary, call_symbol, return_symbol))
1298
+ for candidate in candidates:
1299
+ if candidate not in summaries:
1300
+ summaries.add(candidate)
1301
+ queue.append(candidate)
1302
+ return summaries
1303
+
1304
+
1305
+ def _compose_summary(
1306
+ first: tuple[int | None, ...],
1307
+ second: tuple[int | None, ...],
1308
+ ) -> tuple[int | None, ...]:
1309
+ return tuple(None if state is None else second[state] for state in first)
1310
+
1311
+
1312
+ def _wrap_summary(
1313
+ vpa: DeterministicVisiblyPushdownAutomaton,
1314
+ state_order: Sequence[Hashable],
1315
+ inner: tuple[int | None, ...],
1316
+ call_symbol: Any,
1317
+ return_symbol: Any,
1318
+ ) -> tuple[int | None, ...]:
1319
+ state_index = {state: index for index, state in enumerate(state_order)}
1320
+ call_map = vpa.call_transition_map()
1321
+ result: list[int | None] = []
1322
+ for state in state_order:
1323
+ call = call_map.get((state, call_symbol))
1324
+ if call is None:
1325
+ result.append(None)
1326
+ continue
1327
+ call_target, stack_symbol = call
1328
+ inner_target_index = inner[state_index[call_target]]
1329
+ if inner_target_index is None:
1330
+ result.append(None)
1331
+ continue
1332
+ return_target = vpa.return_successor(state_order[inner_target_index], return_symbol, stack_symbol)
1333
+ result.append(None if return_target is None else state_index[return_target])
1334
+ return tuple(result)
1335
+
1336
+
1337
+ def _quotient_summaries(
1338
+ vpa: DeterministicVisiblyPushdownAutomaton,
1339
+ state_order: Sequence[Hashable],
1340
+ state_index: Mapping[Hashable, int],
1341
+ summaries: set[tuple[int | None, ...]],
1342
+ identity: tuple[int | None, ...],
1343
+ ) -> tuple[dict[tuple[int | None, ...], int], dict[int, tuple[int | None, ...]]]:
1344
+ contexts = tuple(sorted(summaries, key=repr))
1345
+ signatures = {
1346
+ summary: tuple(_summary_accepts(vpa, state_order, _compose_summary(summary, context)) for context in contexts)
1347
+ for summary in summaries
1348
+ }
1349
+ identity_signature = signatures[identity]
1350
+ ordered_signatures = sorted(
1351
+ set(signatures.values()), key=lambda signature: (signature != identity_signature, signature)
1352
+ )
1353
+ signature_class = {signature: index for index, signature in enumerate(ordered_signatures)}
1354
+ class_of = {summary: signature_class[signature] for summary, signature in signatures.items()}
1355
+ representatives = {
1356
+ index: min(
1357
+ (summary for summary, signature in signatures.items() if signature_class[signature] == index), key=repr
1358
+ )
1359
+ for index in signature_class.values()
1360
+ }
1361
+ return class_of, representatives
1362
+
1363
+
1364
+ def _summary_accepts(
1365
+ vpa: DeterministicVisiblyPushdownAutomaton,
1366
+ state_order: Sequence[Hashable],
1367
+ summary: tuple[int | None, ...],
1368
+ ) -> bool:
1369
+ if vpa.initial_state is None:
1370
+ return False
1371
+ initial_index = {state: index for index, state in enumerate(state_order)}[vpa.initial_state]
1372
+ target = summary[initial_index]
1373
+ return target is not None and state_order[target] in vpa.accepting_states