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.
- sofic/__init__.py +185 -0
- sofic/automata/__init__.py +207 -0
- sofic/automata/_config_simulation.py +40 -0
- sofic/automata/active.py +611 -0
- sofic/automata/alergia.py +222 -0
- sofic/automata/algorithms.py +376 -0
- sofic/automata/atomaton.py +58 -0
- sofic/automata/base.py +161 -0
- sofic/automata/buchi.py +23 -0
- sofic/automata/buchi_simulation.py +67 -0
- sofic/automata/canonical_dual.py +18 -0
- sofic/automata/canonical_extraction.py +122 -0
- sofic/automata/dfa.py +85 -0
- sofic/automata/dfasat.py +195 -0
- sofic/automata/edsm.py +219 -0
- sofic/automata/enumeration.py +44 -0
- sofic/automata/icdfa.py +421 -0
- sofic/automata/idfa.py +363 -0
- sofic/automata/languages/__init__.py +39 -0
- sofic/automata/languages/_quotient_utils.py +64 -0
- sofic/automata/languages/atoms.py +31 -0
- sofic/automata/languages/automaton_ops.py +243 -0
- sofic/automata/languages/base.py +67 -0
- sofic/automata/languages/operations.py +78 -0
- sofic/automata/languages/quotients.py +66 -0
- sofic/automata/languages/residuals.py +25 -0
- sofic/automata/learning.py +79 -0
- sofic/automata/nfa.py +39 -0
- sofic/automata/nwa.py +343 -0
- sofic/automata/nwa_simulation.py +56 -0
- sofic/automata/observation.py +40 -0
- sofic/automata/papni.py +301 -0
- sofic/automata/regex.py +128 -0
- sofic/automata/rfsa.py +35 -0
- sofic/automata/rpni.py +193 -0
- sofic/automata/subsequential.py +201 -0
- sofic/automata/transducer_operations.py +350 -0
- sofic/automata/transducer_simulation.py +150 -0
- sofic/automata/transducers.py +365 -0
- sofic/automata/unifilar.py +107 -0
- sofic/automata/vpa.py +1373 -0
- sofic/automata/vpa_simulation.py +53 -0
- sofic/base.py +153 -0
- sofic/core.py +47 -0
- sofic/examples/__init__.py +86 -0
- sofic/examples/epsilon_machines.py +1089 -0
- sofic/examples/processes.py +1491 -0
- sofic/examples/shifts.py +144 -0
- sofic/exceptions.py +33 -0
- sofic/generators/__init__.py +115 -0
- sofic/generators/_word_measures.py +94 -0
- sofic/generators/alternative_complexity.py +104 -0
- sofic/generators/base.py +327 -0
- sofic/generators/bidirectional_construction.py +717 -0
- sofic/generators/bidirectional_epsilon_machine.py +689 -0
- sofic/generators/block_convergence.py +668 -0
- sofic/generators/block_entropy.py +578 -0
- sofic/generators/channel_measures.py +75 -0
- sofic/generators/conversions.py +182 -0
- sofic/generators/directional_flow.py +245 -0
- sofic/generators/edge_emissions.py +36 -0
- sofic/generators/edge_machine.py +178 -0
- sofic/generators/epsilon_construction.py +193 -0
- sofic/generators/epsilon_inference.py +703 -0
- sofic/generators/epsilon_machine.py +557 -0
- sofic/generators/epsilon_transducer.py +168 -0
- sofic/generators/epsilon_transducer_construction.py +185 -0
- sofic/generators/epsilon_transducer_inference.py +499 -0
- sofic/generators/hmm_inference.py +719 -0
- sofic/generators/information_diagram.py +428 -0
- sofic/generators/lumping.py +447 -0
- sofic/generators/markov.py +100 -0
- sofic/generators/mealy.py +156 -0
- sofic/generators/measures.py +257 -0
- sofic/generators/minimal_generative_model.py +821 -0
- sofic/generators/mixed_state.py +250 -0
- sofic/generators/mixed_state_construction.py +163 -0
- sofic/generators/moore.py +75 -0
- sofic/generators/nmachine.py +78 -0
- sofic/generators/nmachine_construction.py +70 -0
- sofic/generators/pfa.py +100 -0
- sofic/generators/prob.py +291 -0
- sofic/generators/process_equivalence.py +207 -0
- sofic/generators/quasi_inference.py +74 -0
- sofic/generators/quasi_realization.py +97 -0
- sofic/generators/reversal.py +66 -0
- sofic/generators/stack_hmm.py +426 -0
- sofic/generators/stack_inference.py +509 -0
- sofic/generators/stationary.py +134 -0
- sofic/generators/stochastic.py +65 -0
- sofic/generators/synchronization.py +407 -0
- sofic/generators/topological_epsilon_enumeration.py +349 -0
- sofic/generators/words.py +226 -0
- sofic/graph.py +135 -0
- sofic/indexing.py +31 -0
- sofic/inference/__init__.py +45 -0
- sofic/inference/bayesian/__init__.py +68 -0
- sofic/inference/bayesian/comparison.py +199 -0
- sofic/inference/bayesian/counts.py +219 -0
- sofic/inference/bayesian/diversity.py +254 -0
- sofic/inference/bayesian/epsilon.py +270 -0
- sofic/inference/bayesian/hdp_hmm.py +340 -0
- sofic/inference/bayesian/markov.py +294 -0
- sofic/inference/bayesian/pymc_backend.py +71 -0
- sofic/inference/bayesian/stack_hmm.py +215 -0
- sofic/inference/model_selection.py +365 -0
- sofic/inference/spectral.py +564 -0
- sofic/operations.py +16 -0
- sofic/properties.py +339 -0
- sofic/serialization.py +450 -0
- sofic/shifts/__init__.py +48 -0
- sofic/shifts/algorithms.py +84 -0
- sofic/shifts/base.py +49 -0
- sofic/shifts/cover_construction.py +76 -0
- sofic/shifts/covers.py +47 -0
- sofic/shifts/dyck_algorithms.py +100 -0
- sofic/shifts/dyck_enumeration.py +275 -0
- sofic/shifts/markov_dyck.py +172 -0
- sofic/shifts/parry_construction.py +82 -0
- sofic/shifts/sft.py +104 -0
- sofic/shifts/sft_construction.py +52 -0
- sofic/shifts/sliding_block_code.py +156 -0
- sofic/shifts/sofic.py +111 -0
- sofic/shifts/sofic_dyck.py +110 -0
- sofic/shifts/sofic_relation.py +64 -0
- sofic/shifts/textile.py +104 -0
- sofic/shifts/tmc.py +46 -0
- sofic/shifts/tmc_construction.py +58 -0
- sofic/shifts/topological_anatomy.py +150 -0
- sofic/states.py +27 -0
- sofic/testing/__init__.py +8 -0
- sofic/testing/strategies.py +154 -0
- sofic/viz/__init__.py +16 -0
- sofic/viz/_context.py +345 -0
- sofic/viz/_edge.py +216 -0
- sofic/viz/_format.py +89 -0
- sofic/viz/_labels.py +34 -0
- sofic/viz/_names.py +17 -0
- sofic/viz/_rational.py +20 -0
- sofic/viz/_tikz_compile.py +177 -0
- sofic/viz/_tikz_format.py +122 -0
- sofic/viz/_tikz_layout.py +218 -0
- sofic/viz/assets/vaucanson.tikz +71 -0
- sofic/viz/graphviz.py +158 -0
- sofic/viz/idiagram.py +350 -0
- sofic/viz/tikz.py +381 -0
- sofic-0.1.0.dist-info/METADATA +444 -0
- sofic-0.1.0.dist-info/RECORD +150 -0
- sofic-0.1.0.dist-info/WHEEL +4 -0
- 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
|