flakepl 0.3.1__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.
flake/runtime.py ADDED
@@ -0,0 +1,370 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any
5
+
6
+ from .actions import Action, Receive, Send, Tau
7
+ from .agent import Agent
8
+ from .process import Process
9
+ from .sos import transitions
10
+
11
+
12
+ @dataclass(frozen=True, slots=True)
13
+ class GlobalState:
14
+ """
15
+ Environment-level state.
16
+
17
+ Agent-local state is stored in Agent.state.
18
+ """
19
+
20
+ values: tuple[tuple[str, Any], ...] = ()
21
+
22
+ @classmethod
23
+ def from_dict(
24
+ cls,
25
+ values: dict[str, Any],
26
+ ) -> GlobalState:
27
+
28
+ return cls(
29
+ values=tuple(
30
+ sorted(
31
+ values.items(),
32
+ key=lambda item: item[0],
33
+ )
34
+ )
35
+ )
36
+
37
+ def to_dict(self) -> dict[str, Any]:
38
+ return dict(
39
+ self.values
40
+ )
41
+
42
+ def get(
43
+ self,
44
+ name: str,
45
+ default: Any = None,
46
+ ) -> Any:
47
+
48
+ return dict(
49
+ self.values
50
+ ).get(
51
+ name,
52
+ default,
53
+ )
54
+
55
+ def with_value(
56
+ self,
57
+ name: str,
58
+ value: Any,
59
+ ) -> GlobalState:
60
+
61
+ values = self.to_dict()
62
+
63
+ values[name] = value
64
+
65
+ return GlobalState.from_dict(
66
+ values
67
+ )
68
+
69
+
70
+ @dataclass(frozen=True, slots=True)
71
+ class Configuration:
72
+ """
73
+ Global system configuration.
74
+
75
+ Conceptually:
76
+
77
+ C = (s, A)
78
+
79
+ where:
80
+
81
+ s = global state
82
+ A = tuple of agents
83
+ """
84
+
85
+ state: GlobalState
86
+ agents: tuple[Agent, ...]
87
+
88
+ def __post_init__(self) -> None:
89
+
90
+ names = [
91
+ agent.name
92
+ for agent in self.agents
93
+ ]
94
+
95
+ if len(names) != len(set(names)):
96
+ raise ValueError(
97
+ "Configuration contains "
98
+ "duplicate agent names"
99
+ )
100
+
101
+ def agent(
102
+ self,
103
+ name: str,
104
+ ) -> Agent:
105
+
106
+ for agent in self.agents:
107
+
108
+ if agent.name == name:
109
+ return agent
110
+
111
+ raise KeyError(
112
+ f"Unknown agent: {name}"
113
+ )
114
+
115
+ @property
116
+ def processes(
117
+ self,
118
+ ) -> tuple[Process, ...]:
119
+
120
+ return tuple(
121
+ agent.process
122
+ for agent in self.agents
123
+ )
124
+
125
+ def with_agent(
126
+ self,
127
+ updated: Agent,
128
+ ) -> Configuration:
129
+
130
+ if not any(
131
+ agent.name == updated.name
132
+ for agent in self.agents
133
+ ):
134
+ raise KeyError(
135
+ f"Unknown agent: "
136
+ f"{updated.name}"
137
+ )
138
+
139
+ updated_agents = tuple(
140
+ updated
141
+ if agent.name == updated.name
142
+ else agent
143
+ for agent in self.agents
144
+ )
145
+
146
+ return Configuration(
147
+ state=self.state,
148
+ agents=updated_agents,
149
+ )
150
+
151
+
152
+ @dataclass(frozen=True, slots=True)
153
+ class SystemTransition:
154
+ """
155
+ Global transition:
156
+
157
+ C --action--> C'
158
+ """
159
+
160
+ source: Configuration
161
+ action: Action
162
+ target: Configuration
163
+
164
+
165
+ def _replace_two_agents(
166
+ agents: tuple[Agent, ...],
167
+ first: Agent,
168
+ second: Agent,
169
+ ) -> tuple[Agent, ...]:
170
+
171
+ if first.name == second.name:
172
+ raise ValueError(
173
+ "Cannot replace the same agent twice"
174
+ )
175
+
176
+ result: list[Agent] = []
177
+
178
+ found_first = False
179
+ found_second = False
180
+
181
+ for agent in agents:
182
+
183
+ if agent.name == first.name:
184
+
185
+ result.append(first)
186
+ found_first = True
187
+
188
+ elif agent.name == second.name:
189
+
190
+ result.append(second)
191
+ found_second = True
192
+
193
+ else:
194
+
195
+ result.append(agent)
196
+
197
+ if not found_first:
198
+ raise KeyError(
199
+ f"Unknown agent: {first.name}"
200
+ )
201
+
202
+ if not found_second:
203
+ raise KeyError(
204
+ f"Unknown agent: {second.name}"
205
+ )
206
+
207
+ return tuple(result)
208
+
209
+
210
+ def system_transitions(
211
+ configuration: Configuration,
212
+ ) -> tuple[SystemTransition, ...]:
213
+ """
214
+ Generate all system-level transitions.
215
+
216
+ Includes:
217
+
218
+ 1. Independent agent actions.
219
+ 2. Synchronous communication.
220
+
221
+ Communication:
222
+
223
+ send(c, v) | recv(c, x)
224
+ ---------------------
225
+ τ
226
+
227
+ The sender advances its process.
228
+
229
+ The receiver advances its process and stores
230
+ v in local variable x.
231
+ """
232
+
233
+ result: list[SystemTransition] = []
234
+
235
+ agents = configuration.agents
236
+
237
+ # ========================================================
238
+ # Independent actions
239
+ # ========================================================
240
+
241
+ for index, agent in enumerate(agents):
242
+
243
+ for transition in transitions(
244
+ agent.process
245
+ ):
246
+
247
+ updated_agent = (
248
+ agent.with_process(
249
+ transition.target
250
+ )
251
+ )
252
+
253
+ updated_agents = tuple(
254
+ updated_agent
255
+ if i == index
256
+ else other
257
+ for i, other in enumerate(
258
+ agents
259
+ )
260
+ )
261
+
262
+ target = Configuration(
263
+ state=configuration.state,
264
+ agents=updated_agents,
265
+ )
266
+
267
+ result.append(
268
+ SystemTransition(
269
+ source=configuration,
270
+ action=transition.action,
271
+ target=target,
272
+ )
273
+ )
274
+
275
+ # ========================================================
276
+ # Synchronous communication
277
+ # ========================================================
278
+
279
+ for sender_index, sender in enumerate(
280
+ agents
281
+ ):
282
+
283
+ sender_transitions = transitions(
284
+ sender.process
285
+ )
286
+
287
+ for sender_transition in (
288
+ sender_transitions
289
+ ):
290
+
291
+ if not isinstance(
292
+ sender_transition.action,
293
+ Send,
294
+ ):
295
+ continue
296
+
297
+ for receiver_index, receiver in enumerate(
298
+ agents
299
+ ):
300
+
301
+ if sender_index == receiver_index:
302
+ continue
303
+
304
+ receiver_transitions = transitions(
305
+ receiver.process
306
+ )
307
+
308
+ for receiver_transition in (
309
+ receiver_transitions
310
+ ):
311
+
312
+ if not isinstance(
313
+ receiver_transition.action,
314
+ Receive,
315
+ ):
316
+ continue
317
+
318
+ send_action = (
319
+ sender_transition.action
320
+ )
321
+
322
+ receive_action = (
323
+ receiver_transition.action
324
+ )
325
+
326
+ if (
327
+ send_action.channel
328
+ != receive_action.channel
329
+ ):
330
+ continue
331
+
332
+ updated_sender = (
333
+ sender.with_process(
334
+ sender_transition.target
335
+ )
336
+ )
337
+
338
+ updated_receiver = (
339
+ receiver
340
+ .with_process(
341
+ receiver_transition.target
342
+ )
343
+ .with_state(
344
+ receive_action.variable,
345
+ send_action.value,
346
+ )
347
+ )
348
+
349
+ updated_agents = (
350
+ _replace_two_agents(
351
+ agents,
352
+ updated_sender,
353
+ updated_receiver,
354
+ )
355
+ )
356
+
357
+ target = Configuration(
358
+ state=configuration.state,
359
+ agents=updated_agents,
360
+ )
361
+
362
+ result.append(
363
+ SystemTransition(
364
+ source=configuration,
365
+ action=Tau(),
366
+ target=target,
367
+ )
368
+ )
369
+
370
+ return tuple(result)
flake/semantics.py ADDED
@@ -0,0 +1,222 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Iterable
4
+
5
+ from .actions import (
6
+ Receive,
7
+ Send,
8
+ Tau,
9
+ )
10
+ from .agent import Agent
11
+ from .runtime import (
12
+ Configuration,
13
+ SystemTransition,
14
+ )
15
+ from .sos import transitions
16
+
17
+
18
+ def _replace_agents(
19
+ configuration: Configuration,
20
+ updates: Iterable[Agent],
21
+ ) -> Configuration:
22
+ """
23
+ Return a configuration with the supplied agents replaced.
24
+ """
25
+
26
+ result = configuration
27
+
28
+ for agent in updates:
29
+ result = result.with_agent(agent)
30
+
31
+ return result
32
+
33
+
34
+ def synchronous_system_transitions(
35
+ configuration: Configuration,
36
+ ) -> tuple[SystemTransition, ...]:
37
+ """
38
+ Strict synchronous process semantics.
39
+
40
+ Tau transitions are executed independently.
41
+
42
+ Send and Receive transitions never execute independently.
43
+ They must synchronize on the same channel.
44
+
45
+ Therefore:
46
+
47
+ send(c, v) | recv(c, x)
48
+
49
+ becomes one transition where:
50
+
51
+ receiver.x = v
52
+
53
+ while the two processes advance simultaneously.
54
+
55
+ This semantics is useful for modeling protocols where
56
+ communication is synchronous, such as the examples in
57
+ the FlakePL benchmark suite.
58
+ """
59
+
60
+ result: list[SystemTransition] = []
61
+
62
+ agents = configuration.agents
63
+
64
+ # ------------------------------------------------------------------
65
+ # Independent internal transitions.
66
+ # ------------------------------------------------------------------
67
+
68
+ for agent in agents:
69
+ for transition in transitions(
70
+ agent.process
71
+ ):
72
+ if not isinstance(
73
+ transition.action,
74
+ Tau,
75
+ ):
76
+ continue
77
+
78
+ updated_agent = agent.with_process(
79
+ transition.target
80
+ )
81
+
82
+ target = configuration.with_agent(
83
+ updated_agent
84
+ )
85
+
86
+ result.append(
87
+ SystemTransition(
88
+ source=configuration,
89
+ action=transition.action,
90
+ target=target,
91
+ )
92
+ )
93
+
94
+ # ------------------------------------------------------------------
95
+ # Synchronous communication.
96
+ #
97
+ # We inspect each pair only once.
98
+ # ------------------------------------------------------------------
99
+
100
+ for left_index in range(
101
+ len(agents)
102
+ ):
103
+ for right_index in range(
104
+ left_index + 1,
105
+ len(agents),
106
+ ):
107
+ left_agent = agents[left_index]
108
+ right_agent = agents[right_index]
109
+
110
+ left_transitions = transitions(
111
+ left_agent.process
112
+ )
113
+
114
+ right_transitions = transitions(
115
+ right_agent.process
116
+ )
117
+
118
+ for left_transition in left_transitions:
119
+ left_action = left_transition.action
120
+
121
+ for right_transition in right_transitions:
122
+ right_action = right_transition.action
123
+
124
+ # --------------------------------------------------
125
+ # left sends, right receives
126
+ # --------------------------------------------------
127
+
128
+ if (
129
+ isinstance(
130
+ left_action,
131
+ Send,
132
+ )
133
+ and isinstance(
134
+ right_action,
135
+ Receive,
136
+ )
137
+ and left_action.channel
138
+ == right_action.channel
139
+ ):
140
+ updated_left = (
141
+ left_agent.with_process(
142
+ left_transition.target
143
+ )
144
+ )
145
+
146
+ updated_right = (
147
+ right_agent
148
+ .with_process(
149
+ right_transition.target
150
+ )
151
+ .with_state(
152
+ right_action.variable,
153
+ left_action.value,
154
+ )
155
+ )
156
+
157
+ target = _replace_agents(
158
+ configuration,
159
+ (
160
+ updated_left,
161
+ updated_right,
162
+ ),
163
+ )
164
+
165
+ result.append(
166
+ SystemTransition(
167
+ source=configuration,
168
+ action=Tau(),
169
+ target=target,
170
+ )
171
+ )
172
+
173
+ # --------------------------------------------------
174
+ # left receives, right sends
175
+ # --------------------------------------------------
176
+
177
+ elif (
178
+ isinstance(
179
+ left_action,
180
+ Receive,
181
+ )
182
+ and isinstance(
183
+ right_action,
184
+ Send,
185
+ )
186
+ and left_action.channel
187
+ == right_action.channel
188
+ ):
189
+ updated_left = (
190
+ left_agent
191
+ .with_process(
192
+ left_transition.target
193
+ )
194
+ .with_state(
195
+ left_action.variable,
196
+ right_action.value,
197
+ )
198
+ )
199
+
200
+ updated_right = (
201
+ right_agent.with_process(
202
+ right_transition.target
203
+ )
204
+ )
205
+
206
+ target = _replace_agents(
207
+ configuration,
208
+ (
209
+ updated_left,
210
+ updated_right,
211
+ ),
212
+ )
213
+
214
+ result.append(
215
+ SystemTransition(
216
+ source=configuration,
217
+ action=Tau(),
218
+ target=target,
219
+ )
220
+ )
221
+
222
+ return tuple(result)
flake/sos.py ADDED
@@ -0,0 +1,97 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from .actions import Action, Receive, Send, Tau
6
+ from .process import Choice, Nil, Parallel, Prefix, Process
7
+
8
+
9
+ @dataclass(frozen=True, slots=True)
10
+ class Transition:
11
+ """One process-level operational transition: P --action--> P'."""
12
+
13
+ action: Action
14
+ target: Process
15
+
16
+
17
+ def transitions(process: Process) -> tuple[Transition, ...]:
18
+ """Generate all one-step SOS transitions for a process.
19
+
20
+ The MVP supports prefix, nondeterministic choice, parallel composition,
21
+ and synchronous communication between matching send/receive prefixes.
22
+ """
23
+
24
+ if isinstance(process, Nil):
25
+ return ()
26
+
27
+ if isinstance(process, Prefix):
28
+ return (Transition(process.action, process.continuation),)
29
+
30
+ if isinstance(process, Choice):
31
+ result: list[Transition] = []
32
+ for option in process.options:
33
+ result.extend(transitions(option))
34
+ return tuple(result)
35
+
36
+ if isinstance(process, Parallel):
37
+ result: list[Transition] = []
38
+ processes = process.processes
39
+
40
+ # Independent actions.
41
+ for index, current in enumerate(processes):
42
+ for transition in transitions(current):
43
+ updated = list(processes)
44
+ updated[index] = transition.target
45
+ result.append(
46
+ Transition(
47
+ transition.action,
48
+ Parallel(tuple(updated)),
49
+ )
50
+ )
51
+
52
+ # Pairwise synchronous communication.
53
+ for left_index in range(len(processes)):
54
+ for right_index in range(left_index + 1, len(processes)):
55
+ left = processes[left_index]
56
+ right = processes[right_index]
57
+
58
+ left_transitions = transitions(left)
59
+ right_transitions = transitions(right)
60
+
61
+ for left_transition in left_transitions:
62
+ left_action = left_transition.action
63
+ for right_transition in right_transitions:
64
+ right_action = right_transition.action
65
+
66
+ if isinstance(left_action, Send) and isinstance(right_action, Receive):
67
+ if left_action.channel != right_action.channel:
68
+ continue
69
+ updated = list(processes)
70
+ updated[left_index] = left_transition.target
71
+ updated[right_index] = right_transition.target
72
+ result.append(
73
+ Transition(
74
+ Tau(),
75
+ Parallel(tuple(updated)),
76
+ )
77
+ )
78
+
79
+ elif isinstance(left_action, Receive) and isinstance(right_action, Send):
80
+ if left_action.channel != right_action.channel:
81
+ continue
82
+ updated = list(processes)
83
+ updated[left_index] = left_transition.target
84
+ updated[right_index] = right_transition.target
85
+ result.append(
86
+ Transition(
87
+ Tau(),
88
+ Parallel(tuple(updated)),
89
+ )
90
+ )
91
+
92
+ return tuple(result)
93
+
94
+ raise TypeError(f"Unsupported process type: {type(process).__name__}")
95
+
96
+
97
+ __all__ = ["Transition", "transitions"]