pyfcstm 0.0.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.
- pyfcstm/__init__.py +0 -0
- pyfcstm/__main__.py +4 -0
- pyfcstm/config/__init__.py +0 -0
- pyfcstm/config/meta.py +20 -0
- pyfcstm/dsl/__init__.py +6 -0
- pyfcstm/dsl/error.py +226 -0
- pyfcstm/dsl/grammar/Grammar.g4 +190 -0
- pyfcstm/dsl/grammar/Grammar.interp +168 -0
- pyfcstm/dsl/grammar/Grammar.tokens +118 -0
- pyfcstm/dsl/grammar/GrammarLexer.interp +214 -0
- pyfcstm/dsl/grammar/GrammarLexer.py +523 -0
- pyfcstm/dsl/grammar/GrammarLexer.tokens +118 -0
- pyfcstm/dsl/grammar/GrammarListener.py +521 -0
- pyfcstm/dsl/grammar/GrammarParser.py +4373 -0
- pyfcstm/dsl/grammar/__init__.py +3 -0
- pyfcstm/dsl/listener.py +440 -0
- pyfcstm/dsl/node.py +1581 -0
- pyfcstm/dsl/parse.py +155 -0
- pyfcstm/entry/__init__.py +1 -0
- pyfcstm/entry/base.py +126 -0
- pyfcstm/entry/cli.py +12 -0
- pyfcstm/entry/dispatch.py +46 -0
- pyfcstm/entry/generate.py +83 -0
- pyfcstm/entry/plantuml.py +67 -0
- pyfcstm/model/__init__.py +3 -0
- pyfcstm/model/base.py +51 -0
- pyfcstm/model/expr.py +764 -0
- pyfcstm/model/model.py +1392 -0
- pyfcstm/render/__init__.py +3 -0
- pyfcstm/render/env.py +36 -0
- pyfcstm/render/expr.py +180 -0
- pyfcstm/render/func.py +77 -0
- pyfcstm/render/render.py +279 -0
- pyfcstm/utils/__init__.py +6 -0
- pyfcstm/utils/binary.py +38 -0
- pyfcstm/utils/doc.py +64 -0
- pyfcstm/utils/jinja2.py +121 -0
- pyfcstm/utils/json.py +125 -0
- pyfcstm/utils/text.py +91 -0
- pyfcstm/utils/validate.py +102 -0
- pyfcstm-0.0.1.dist-info/LICENSE +165 -0
- pyfcstm-0.0.1.dist-info/METADATA +205 -0
- pyfcstm-0.0.1.dist-info/RECORD +46 -0
- pyfcstm-0.0.1.dist-info/WHEEL +5 -0
- pyfcstm-0.0.1.dist-info/entry_points.txt +2 -0
- pyfcstm-0.0.1.dist-info/top_level.txt +1 -0
pyfcstm/model/model.py
ADDED
@@ -0,0 +1,1392 @@
|
|
1
|
+
"""
|
2
|
+
State machine module for parsing and representing hierarchical state machines.
|
3
|
+
|
4
|
+
This module provides classes and functions for working with state machines, including:
|
5
|
+
|
6
|
+
- Representation of states, transitions, events, and operations
|
7
|
+
- Parsing state machine DSL nodes into state machine objects
|
8
|
+
- Exporting state machines to AST nodes and PlantUML diagrams
|
9
|
+
|
10
|
+
The module implements a hierarchical state machine model with support for:
|
11
|
+
|
12
|
+
- Nested states
|
13
|
+
- Entry, during, and exit actions
|
14
|
+
- Guards and effects on transitions
|
15
|
+
- Abstract function declarations
|
16
|
+
- Variable definitions
|
17
|
+
"""
|
18
|
+
import io
|
19
|
+
import json
|
20
|
+
import weakref
|
21
|
+
from dataclasses import dataclass
|
22
|
+
from textwrap import indent
|
23
|
+
from typing import Optional, Union, List, Dict, Tuple
|
24
|
+
|
25
|
+
from .base import AstExportable, PlantUMLExportable
|
26
|
+
from .expr import Expr, parse_expr_node_to_expr
|
27
|
+
from ..dsl import node as dsl_nodes, INIT_STATE, EXIT_STATE
|
28
|
+
|
29
|
+
__all__ = [
|
30
|
+
'Operation',
|
31
|
+
'Event',
|
32
|
+
'Transition',
|
33
|
+
'OnStage',
|
34
|
+
'OnAspect',
|
35
|
+
'State',
|
36
|
+
'VarDefine',
|
37
|
+
'StateMachine',
|
38
|
+
'parse_dsl_node_to_state_machine',
|
39
|
+
]
|
40
|
+
|
41
|
+
|
42
|
+
@dataclass
|
43
|
+
class Operation(AstExportable):
|
44
|
+
"""
|
45
|
+
Represents an operation that assigns a value to a variable.
|
46
|
+
|
47
|
+
An operation consists of a variable name and an expression that will be
|
48
|
+
assigned to the variable when the operation is executed.
|
49
|
+
|
50
|
+
:param var_name: The name of the variable to assign to
|
51
|
+
:type var_name: str
|
52
|
+
:param expr: The expression to evaluate and assign to the variable
|
53
|
+
:type expr: Expr
|
54
|
+
"""
|
55
|
+
var_name: str
|
56
|
+
expr: Expr
|
57
|
+
|
58
|
+
def to_ast_node(self) -> dsl_nodes.OperationAssignment:
|
59
|
+
"""
|
60
|
+
Convert this operation to an AST node.
|
61
|
+
|
62
|
+
:return: An operation assignment AST node
|
63
|
+
:rtype: dsl_nodes.OperationAssignment
|
64
|
+
"""
|
65
|
+
return dsl_nodes.OperationAssignment(
|
66
|
+
name=self.var_name,
|
67
|
+
expr=self.expr.to_ast_node(),
|
68
|
+
)
|
69
|
+
|
70
|
+
def var_name_to_ast_node(self) -> dsl_nodes.Name:
|
71
|
+
"""
|
72
|
+
Convert the variable name to an AST node.
|
73
|
+
|
74
|
+
:return: A name AST node
|
75
|
+
:rtype: dsl_nodes.Name
|
76
|
+
"""
|
77
|
+
return dsl_nodes.Name(name=self.var_name)
|
78
|
+
|
79
|
+
|
80
|
+
@dataclass
|
81
|
+
class Event:
|
82
|
+
"""
|
83
|
+
Represents an event that can trigger state transitions.
|
84
|
+
|
85
|
+
An event has a name and is associated with a specific state path in the
|
86
|
+
state machine hierarchy.
|
87
|
+
|
88
|
+
:param name: The name of the event
|
89
|
+
:type name: str
|
90
|
+
:param state_path: The path to the state that owns this event
|
91
|
+
:type state_path: Tuple[str, ...]
|
92
|
+
"""
|
93
|
+
name: str
|
94
|
+
state_path: Tuple[str, ...]
|
95
|
+
|
96
|
+
@property
|
97
|
+
def path(self) -> Tuple[str, ...]:
|
98
|
+
"""
|
99
|
+
Get the full path of the event including the state path and event name.
|
100
|
+
|
101
|
+
:return: The full path to the event
|
102
|
+
:rtype: Tuple[str, ...]
|
103
|
+
"""
|
104
|
+
return tuple((*self.state_path, self.name))
|
105
|
+
|
106
|
+
|
107
|
+
@dataclass
|
108
|
+
class Transition(AstExportable):
|
109
|
+
"""
|
110
|
+
Represents a transition between states in a state machine.
|
111
|
+
|
112
|
+
A transition defines how the state machine moves from one state to another,
|
113
|
+
potentially triggered by an event, guarded by a condition, and with effects
|
114
|
+
that execute when the transition occurs.
|
115
|
+
|
116
|
+
:param from_state: The source state name or special state marker
|
117
|
+
:type from_state: Union[str, dsl_nodes._StateSingletonMark]
|
118
|
+
:param to_state: The target state name or special state marker
|
119
|
+
:type to_state: Union[str, dsl_nodes._StateSingletonMark]
|
120
|
+
:param event: The event that triggers this transition, if any
|
121
|
+
:type event: Optional[Event]
|
122
|
+
:param guard: The condition that must be true for the transition to occur, if any
|
123
|
+
:type guard: Optional[Expr]
|
124
|
+
:param effects: Operations to execute when the transition occurs
|
125
|
+
:type effects: List[Operation]
|
126
|
+
:param parent_ref: Weak reference to the parent state
|
127
|
+
:type parent_ref: Optional[weakref.ReferenceType]
|
128
|
+
"""
|
129
|
+
from_state: Union[str, dsl_nodes._StateSingletonMark]
|
130
|
+
to_state: Union[str, dsl_nodes._StateSingletonMark]
|
131
|
+
event: Optional[Event]
|
132
|
+
guard: Optional[Expr]
|
133
|
+
effects: List[Operation]
|
134
|
+
parent_ref: Optional[weakref.ReferenceType] = None
|
135
|
+
|
136
|
+
@property
|
137
|
+
def parent(self) -> Optional['State']:
|
138
|
+
"""
|
139
|
+
Get the parent state of this transition.
|
140
|
+
|
141
|
+
:return: The parent state or None if no parent is set
|
142
|
+
:rtype: Optional['State']
|
143
|
+
"""
|
144
|
+
if self.parent_ref is None:
|
145
|
+
return None
|
146
|
+
else:
|
147
|
+
return self.parent_ref()
|
148
|
+
|
149
|
+
@parent.setter
|
150
|
+
def parent(self, new_parent: Optional['State']):
|
151
|
+
"""
|
152
|
+
Set the parent state of this transition.
|
153
|
+
|
154
|
+
:param new_parent: The new parent state or None to clear the parent
|
155
|
+
:type new_parent: Optional['State']
|
156
|
+
"""
|
157
|
+
if new_parent is None:
|
158
|
+
self.parent_ref = None # pragma: no cover
|
159
|
+
else:
|
160
|
+
self.parent_ref = weakref.ref(new_parent)
|
161
|
+
|
162
|
+
def to_ast_node(self) -> dsl_nodes.ASTNode:
|
163
|
+
"""
|
164
|
+
Convert this transition to an AST node.
|
165
|
+
|
166
|
+
:return: A transition definition AST node
|
167
|
+
:rtype: dsl_nodes.ASTNode
|
168
|
+
"""
|
169
|
+
return State.transition_to_ast_node(self.parent, self)
|
170
|
+
|
171
|
+
|
172
|
+
@dataclass
|
173
|
+
class OnStage(AstExportable):
|
174
|
+
"""
|
175
|
+
Represents an action that occurs during a specific stage of a state's lifecycle.
|
176
|
+
|
177
|
+
OnStage can represent enter, during, or exit actions, and can be either concrete
|
178
|
+
operations or abstract function declarations.
|
179
|
+
|
180
|
+
:param stage: The lifecycle stage ('enter', 'during', or 'exit')
|
181
|
+
:type stage: str
|
182
|
+
:param aspect: For 'during' actions in composite states, specifies if the action occurs 'before' or 'after' substates
|
183
|
+
:type aspect: Optional[str]
|
184
|
+
:param name: For abstract functions, the name of the function
|
185
|
+
:type name: Optional[str]
|
186
|
+
:param doc: For abstract functions, the documentation string
|
187
|
+
:type doc: Optional[str]
|
188
|
+
:param operations: For concrete actions, the list of operations to execute
|
189
|
+
:type operations: List[Operation]
|
190
|
+
"""
|
191
|
+
stage: str
|
192
|
+
aspect: Optional[str]
|
193
|
+
name: Optional[str]
|
194
|
+
doc: Optional[str]
|
195
|
+
operations: List[Operation]
|
196
|
+
|
197
|
+
@property
|
198
|
+
def is_abstract(self) -> bool:
|
199
|
+
"""
|
200
|
+
Check if this is an abstract function declaration.
|
201
|
+
|
202
|
+
:return: True if this is an abstract function, False otherwise
|
203
|
+
:rtype: bool
|
204
|
+
"""
|
205
|
+
return self.name is not None or self.doc is not None
|
206
|
+
|
207
|
+
@property
|
208
|
+
def is_aspect(self) -> bool:
|
209
|
+
"""
|
210
|
+
Check if this is an aspect-oriented action.
|
211
|
+
|
212
|
+
:return: False for OnStage instances (always)
|
213
|
+
:rtype: bool
|
214
|
+
"""
|
215
|
+
return False
|
216
|
+
|
217
|
+
def to_ast_node(self) -> Union[dsl_nodes.EnterStatement, dsl_nodes.DuringStatement, dsl_nodes.ExitStatement]:
|
218
|
+
"""
|
219
|
+
Convert this OnStage to an appropriate AST node based on the stage.
|
220
|
+
|
221
|
+
:return: An enter, during, or exit statement AST node
|
222
|
+
:rtype: Union[dsl_nodes.EnterStatement, dsl_nodes.DuringStatement, dsl_nodes.ExitStatement]
|
223
|
+
:raises ValueError: If the stage is not one of 'enter', 'during', or 'exit'
|
224
|
+
"""
|
225
|
+
if self.stage == 'enter':
|
226
|
+
if self.name or self.doc is not None:
|
227
|
+
return dsl_nodes.EnterAbstractFunction(
|
228
|
+
name=self.name,
|
229
|
+
doc=self.doc,
|
230
|
+
)
|
231
|
+
else:
|
232
|
+
return dsl_nodes.EnterOperations(
|
233
|
+
name=self.name,
|
234
|
+
operations=[item.to_ast_node() for item in self.operations],
|
235
|
+
)
|
236
|
+
|
237
|
+
elif self.stage == 'during':
|
238
|
+
if self.name or self.doc is not None:
|
239
|
+
return dsl_nodes.DuringAbstractFunction(
|
240
|
+
name=self.name,
|
241
|
+
aspect=self.aspect,
|
242
|
+
doc=self.doc,
|
243
|
+
)
|
244
|
+
else:
|
245
|
+
return dsl_nodes.DuringOperations(
|
246
|
+
name=self.name,
|
247
|
+
aspect=self.aspect,
|
248
|
+
operations=[item.to_ast_node() for item in self.operations],
|
249
|
+
)
|
250
|
+
|
251
|
+
elif self.stage == 'exit':
|
252
|
+
if self.name or self.doc is not None:
|
253
|
+
return dsl_nodes.ExitAbstractFunction(
|
254
|
+
name=self.name,
|
255
|
+
doc=self.doc,
|
256
|
+
)
|
257
|
+
else:
|
258
|
+
return dsl_nodes.ExitOperations(
|
259
|
+
name=self.name,
|
260
|
+
operations=[item.to_ast_node() for item in self.operations],
|
261
|
+
)
|
262
|
+
else:
|
263
|
+
raise ValueError(f'Unknown stage - {self.stage!r}.') # pragma: no cover
|
264
|
+
|
265
|
+
|
266
|
+
@dataclass
|
267
|
+
class OnAspect(AstExportable):
|
268
|
+
"""
|
269
|
+
Represents an aspect-oriented action that occurs during a specific stage of a state's lifecycle.
|
270
|
+
|
271
|
+
OnAspect is specifically used for aspect-oriented programming features in the state machine,
|
272
|
+
allowing actions to be defined that apply across multiple states.
|
273
|
+
|
274
|
+
:param stage: The lifecycle stage (currently only supports 'during')
|
275
|
+
:type stage: str
|
276
|
+
:param aspect: Specifies if the action occurs 'before' or 'after' substates
|
277
|
+
:type aspect: Optional[str]
|
278
|
+
:param name: For abstract functions, the name of the function
|
279
|
+
:type name: Optional[str]
|
280
|
+
:param doc: For abstract functions, the documentation string
|
281
|
+
:type doc: Optional[str]
|
282
|
+
:param operations: For concrete actions, the list of operations to execute
|
283
|
+
:type operations: List[Operation]
|
284
|
+
"""
|
285
|
+
stage: str
|
286
|
+
aspect: Optional[str]
|
287
|
+
name: Optional[str]
|
288
|
+
doc: Optional[str]
|
289
|
+
operations: List[Operation]
|
290
|
+
|
291
|
+
@property
|
292
|
+
def is_abstract(self) -> bool:
|
293
|
+
"""
|
294
|
+
Check if this is an abstract function declaration.
|
295
|
+
|
296
|
+
:return: True if this is an abstract function, False otherwise
|
297
|
+
:rtype: bool
|
298
|
+
"""
|
299
|
+
return self.name is not None or self.doc is not None
|
300
|
+
|
301
|
+
@property
|
302
|
+
def is_aspect(self) -> bool:
|
303
|
+
"""
|
304
|
+
Check if this is an aspect-oriented action.
|
305
|
+
|
306
|
+
:return: True for OnAspect instances (always)
|
307
|
+
:rtype: bool
|
308
|
+
"""
|
309
|
+
return True
|
310
|
+
|
311
|
+
def to_ast_node(self) -> Union[dsl_nodes.DuringAspectStatement]:
|
312
|
+
"""
|
313
|
+
Convert this OnAspect to an appropriate AST node based on the stage.
|
314
|
+
|
315
|
+
:return: A during aspect statement AST node
|
316
|
+
:rtype: Union[dsl_nodes.DuringAspectStatement]
|
317
|
+
:raises ValueError: If the stage is not 'during'
|
318
|
+
"""
|
319
|
+
if self.stage == 'during':
|
320
|
+
if self.name or self.doc is not None:
|
321
|
+
return dsl_nodes.DuringAspectAbstractFunction(
|
322
|
+
name=self.name,
|
323
|
+
aspect=self.aspect,
|
324
|
+
doc=self.doc,
|
325
|
+
)
|
326
|
+
else:
|
327
|
+
return dsl_nodes.DuringAspectOperations(
|
328
|
+
name=self.name,
|
329
|
+
aspect=self.aspect,
|
330
|
+
operations=[item.to_ast_node() for item in self.operations],
|
331
|
+
)
|
332
|
+
|
333
|
+
else:
|
334
|
+
raise ValueError(f'Unknown aspect - {self.stage!r}.') # pragma: no cover
|
335
|
+
|
336
|
+
|
337
|
+
@dataclass
|
338
|
+
class State(AstExportable, PlantUMLExportable):
|
339
|
+
"""
|
340
|
+
Represents a state in a hierarchical state machine.
|
341
|
+
|
342
|
+
A state can contain substates, transitions between those substates, and actions
|
343
|
+
that execute on enter, during, or exit of the state.
|
344
|
+
|
345
|
+
:param name: The name of the state
|
346
|
+
:type name: str
|
347
|
+
:param path: The full path to this state in the hierarchy
|
348
|
+
:type path: Tuple[str, ...]
|
349
|
+
:param substates: Dictionary mapping substate names to State objects
|
350
|
+
:type substates: Dict[str, 'State']
|
351
|
+
:param events: Dictionary mapping event names to Event objects
|
352
|
+
:type events: Dict[str, Event]
|
353
|
+
:param transitions: List of transitions between substates
|
354
|
+
:type transitions: List[Transition]
|
355
|
+
:param on_enters: List of actions to execute when entering the state
|
356
|
+
:type on_enters: List[OnStage]
|
357
|
+
:param on_durings: List of actions to execute while in the state
|
358
|
+
:type on_durings: List[OnStage]
|
359
|
+
:param on_exits: List of actions to execute when exiting the state
|
360
|
+
:type on_exits: List[OnStage]
|
361
|
+
:param on_during_aspects: List of aspect-oriented actions for the during stage
|
362
|
+
:type on_during_aspects: List[OnAspect]
|
363
|
+
:param parent_ref: Weak reference to the parent state
|
364
|
+
:type parent_ref: Optional[weakref.ReferenceType]
|
365
|
+
:param substate_name_to_id: Dictionary mapping substate names to numeric IDs
|
366
|
+
:type substate_name_to_id: Dict[str, int]
|
367
|
+
"""
|
368
|
+
name: str
|
369
|
+
path: Tuple[str, ...]
|
370
|
+
substates: Dict[str, 'State']
|
371
|
+
events: Dict[str, Event] = None
|
372
|
+
transitions: List[Transition] = None
|
373
|
+
on_enters: List[OnStage] = None
|
374
|
+
on_durings: List[OnStage] = None
|
375
|
+
on_exits: List[OnStage] = None
|
376
|
+
on_during_aspects: List[OnAspect] = None
|
377
|
+
parent_ref: Optional[weakref.ReferenceType] = None
|
378
|
+
substate_name_to_id: Dict[str, int] = None
|
379
|
+
|
380
|
+
def __post_init__(self):
|
381
|
+
"""
|
382
|
+
Initialize the substate_name_to_id dictionary after instance creation.
|
383
|
+
"""
|
384
|
+
self.events = self.events or {}
|
385
|
+
self.transitions = self.transitions or []
|
386
|
+
self.on_enters = self.on_enters or []
|
387
|
+
self.on_durings = self.on_durings or []
|
388
|
+
self.on_exits = self.on_exits or []
|
389
|
+
self.on_during_aspects = self.on_during_aspects or []
|
390
|
+
self.substate_name_to_id = {name: i for i, (name, _) in enumerate(self.substates.items())}
|
391
|
+
|
392
|
+
@property
|
393
|
+
def is_leaf_state(self) -> bool:
|
394
|
+
"""
|
395
|
+
Check if this state is a leaf state (has no substates).
|
396
|
+
|
397
|
+
:return: True if this is a leaf state, False otherwise
|
398
|
+
:rtype: bool
|
399
|
+
"""
|
400
|
+
return len(self.substates) == 0
|
401
|
+
|
402
|
+
@property
|
403
|
+
def parent(self) -> Optional['State']:
|
404
|
+
"""
|
405
|
+
Get the parent state of this state.
|
406
|
+
|
407
|
+
:return: The parent state or None if this is the root state
|
408
|
+
:rtype: Optional['State']
|
409
|
+
"""
|
410
|
+
if self.parent_ref is None:
|
411
|
+
return None
|
412
|
+
else:
|
413
|
+
return self.parent_ref()
|
414
|
+
|
415
|
+
@parent.setter
|
416
|
+
def parent(self, new_parent: Optional['State']):
|
417
|
+
"""
|
418
|
+
Set the parent state of this state.
|
419
|
+
|
420
|
+
:param new_parent: The new parent state or None to clear the parent
|
421
|
+
:type new_parent: Optional['State']
|
422
|
+
"""
|
423
|
+
if new_parent is None:
|
424
|
+
self.parent_ref = None # pragma: no cover
|
425
|
+
else:
|
426
|
+
self.parent_ref = weakref.ref(new_parent)
|
427
|
+
|
428
|
+
@property
|
429
|
+
def is_root_state(self) -> bool:
|
430
|
+
"""
|
431
|
+
Check if this state is the root state (has no parent).
|
432
|
+
|
433
|
+
:return: True if this is the root state, False otherwise
|
434
|
+
:rtype: bool
|
435
|
+
"""
|
436
|
+
return self.parent is None
|
437
|
+
|
438
|
+
@property
|
439
|
+
def transitions_from(self) -> List[Transition]:
|
440
|
+
"""
|
441
|
+
Get all transitions that start from this state.
|
442
|
+
|
443
|
+
For non-root states, these are transitions in the parent state where this state
|
444
|
+
is the source. For the root state, a synthetic transition to EXIT_STATE is returned.
|
445
|
+
|
446
|
+
:return: List of transitions from this state
|
447
|
+
:rtype: List[Transition]
|
448
|
+
"""
|
449
|
+
parent = self.parent
|
450
|
+
retval = []
|
451
|
+
if parent is not None:
|
452
|
+
for transition in parent.transitions:
|
453
|
+
if transition.from_state == self.name:
|
454
|
+
retval.append(transition)
|
455
|
+
else:
|
456
|
+
retval.append(Transition(
|
457
|
+
from_state=self.name,
|
458
|
+
to_state=EXIT_STATE,
|
459
|
+
event=None,
|
460
|
+
guard=None,
|
461
|
+
effects=[],
|
462
|
+
parent_ref=self.parent_ref,
|
463
|
+
))
|
464
|
+
return retval
|
465
|
+
|
466
|
+
@property
|
467
|
+
def transitions_to(self) -> List[Transition]:
|
468
|
+
"""
|
469
|
+
Get all transitions that end at this state.
|
470
|
+
|
471
|
+
For non-root states, these are transitions in the parent state where this state
|
472
|
+
is the target. For the root state, a synthetic transition from INIT_STATE is returned.
|
473
|
+
|
474
|
+
:return: List of transitions to this state
|
475
|
+
:rtype: List[Transition]
|
476
|
+
"""
|
477
|
+
parent = self.parent
|
478
|
+
retval = []
|
479
|
+
if parent is not None:
|
480
|
+
for transition in parent.transitions:
|
481
|
+
if transition.to_state == self.name:
|
482
|
+
retval.append(transition)
|
483
|
+
else:
|
484
|
+
retval.append(Transition(
|
485
|
+
from_state=INIT_STATE,
|
486
|
+
to_state=self.name,
|
487
|
+
event=None,
|
488
|
+
guard=None,
|
489
|
+
effects=[],
|
490
|
+
parent_ref=self.parent_ref,
|
491
|
+
))
|
492
|
+
|
493
|
+
return retval
|
494
|
+
|
495
|
+
@property
|
496
|
+
def transitions_entering_children(self) -> List[Transition]:
|
497
|
+
"""
|
498
|
+
Get all transitions that start from the initial state (INIT_STATE).
|
499
|
+
|
500
|
+
These are the transitions that define the initial substate when entering this state.
|
501
|
+
|
502
|
+
:return: List of transitions from INIT_STATE
|
503
|
+
:rtype: List[Transition]
|
504
|
+
"""
|
505
|
+
return [
|
506
|
+
transition for transition in self.transitions
|
507
|
+
if transition.from_state is INIT_STATE
|
508
|
+
]
|
509
|
+
|
510
|
+
@property
|
511
|
+
def transitions_entering_children_simplified(self) -> List[Optional[Transition]]:
|
512
|
+
"""
|
513
|
+
Get a simplified list of transitions entering child states.
|
514
|
+
|
515
|
+
If there's a default transition (no event or guard), only include that one.
|
516
|
+
Otherwise include all transitions and add None at the end.
|
517
|
+
|
518
|
+
:return: List of transitions, possibly with None at the end
|
519
|
+
:rtype: List[Optional[Transition]]
|
520
|
+
"""
|
521
|
+
retval = []
|
522
|
+
for transition in self.transitions:
|
523
|
+
if transition.from_state is INIT_STATE:
|
524
|
+
retval.append(transition)
|
525
|
+
if transition.event is None and transition.guard is None:
|
526
|
+
break
|
527
|
+
if not retval or (retval and not (retval[-1].event is None and retval[-1].guard is None)):
|
528
|
+
retval.append(None)
|
529
|
+
return retval
|
530
|
+
|
531
|
+
def list_on_enters(self, is_abstract: Optional[bool] = None, with_ids: bool = False) \
|
532
|
+
-> List[Union[Tuple[int, OnStage], OnStage]]:
|
533
|
+
"""
|
534
|
+
Get a list of enter actions, optionally filtered by abstract status and with IDs.
|
535
|
+
|
536
|
+
:param is_abstract: If provided, filter to only abstract (True) or non-abstract (False) actions
|
537
|
+
:type is_abstract: Optional[bool]
|
538
|
+
:param with_ids: Whether to include numeric IDs with the actions
|
539
|
+
:type with_ids: bool
|
540
|
+
:return: List of enter actions, optionally with IDs
|
541
|
+
:rtype: List[Union[Tuple[int, OnStage], OnStage]]
|
542
|
+
"""
|
543
|
+
retval = []
|
544
|
+
for id_, item in enumerate(self.on_enters, 1):
|
545
|
+
if (is_abstract is not None and
|
546
|
+
((item.is_abstract and not is_abstract) or (not item.is_abstract and is_abstract))):
|
547
|
+
continue
|
548
|
+
if with_ids:
|
549
|
+
retval.append((id_, item))
|
550
|
+
else:
|
551
|
+
retval.append(item)
|
552
|
+
return retval
|
553
|
+
|
554
|
+
@property
|
555
|
+
def abstract_on_enters(self) -> List[OnStage]:
|
556
|
+
"""
|
557
|
+
Get all abstract enter actions.
|
558
|
+
|
559
|
+
:return: List of abstract enter actions
|
560
|
+
:rtype: List[OnStage]
|
561
|
+
"""
|
562
|
+
return self.list_on_enters(is_abstract=True, with_ids=False)
|
563
|
+
|
564
|
+
@property
|
565
|
+
def non_abstract_on_enters(self) -> List[OnStage]:
|
566
|
+
"""
|
567
|
+
Get all non-abstract enter actions.
|
568
|
+
|
569
|
+
:return: List of non-abstract enter actions
|
570
|
+
:rtype: List[OnStage]
|
571
|
+
"""
|
572
|
+
return self.list_on_enters(is_abstract=False, with_ids=False)
|
573
|
+
|
574
|
+
def list_on_durings(self, is_abstract: Optional[bool] = None, aspect: Optional[str] = None,
|
575
|
+
with_ids: bool = False) -> List[Union[Tuple[int, OnStage], OnStage]]:
|
576
|
+
"""
|
577
|
+
Get a list of during actions, optionally filtered by abstract status, aspect, and with IDs.
|
578
|
+
|
579
|
+
:param is_abstract: If provided, filter to only abstract (True) or non-abstract (False) actions
|
580
|
+
:type is_abstract: Optional[bool]
|
581
|
+
:param aspect: If provided, filter to only actions with the given aspect ('before' or 'after')
|
582
|
+
:type aspect: Optional[str]
|
583
|
+
:param with_ids: Whether to include numeric IDs with the actions
|
584
|
+
:type with_ids: bool
|
585
|
+
:return: List of during actions, optionally with IDs
|
586
|
+
:rtype: List[Union[Tuple[int, OnStage], OnStage]]
|
587
|
+
"""
|
588
|
+
retval = []
|
589
|
+
for id_, item in enumerate(self.on_durings, 1):
|
590
|
+
if (is_abstract is not None and
|
591
|
+
((item.is_abstract and not is_abstract) or (not item.is_abstract and is_abstract))):
|
592
|
+
continue
|
593
|
+
if aspect is not None and item.aspect != aspect:
|
594
|
+
continue
|
595
|
+
|
596
|
+
if with_ids:
|
597
|
+
retval.append((id_, item))
|
598
|
+
else:
|
599
|
+
retval.append(item)
|
600
|
+
return retval
|
601
|
+
|
602
|
+
@property
|
603
|
+
def abstract_on_durings(self) -> List[OnStage]:
|
604
|
+
"""
|
605
|
+
Get all abstract during actions.
|
606
|
+
|
607
|
+
:return: List of abstract during actions
|
608
|
+
:rtype: List[OnStage]
|
609
|
+
"""
|
610
|
+
return self.list_on_durings(is_abstract=True, with_ids=False)
|
611
|
+
|
612
|
+
@property
|
613
|
+
def non_abstract_on_durings(self) -> List[OnStage]:
|
614
|
+
"""
|
615
|
+
Get all non-abstract during actions.
|
616
|
+
|
617
|
+
:return: List of non-abstract during actions
|
618
|
+
:rtype: List[OnStage]
|
619
|
+
"""
|
620
|
+
return self.list_on_durings(is_abstract=False, with_ids=False)
|
621
|
+
|
622
|
+
def list_on_exits(self, is_abstract: Optional[bool] = None, with_ids: bool = False) \
|
623
|
+
-> List[Union[Tuple[int, OnStage], OnStage]]:
|
624
|
+
"""
|
625
|
+
Get a list of exit actions, optionally filtered by abstract status and with IDs.
|
626
|
+
|
627
|
+
:param is_abstract: If provided, filter to only abstract (True) or non-abstract (False) actions
|
628
|
+
:type is_abstract: Optional[bool]
|
629
|
+
:param with_ids: Whether to include numeric IDs with the actions
|
630
|
+
:type with_ids: bool
|
631
|
+
:return: List of exit actions, optionally with IDs
|
632
|
+
:rtype: List[Union[Tuple[int, OnStage], OnStage]]
|
633
|
+
"""
|
634
|
+
retval = []
|
635
|
+
for id_, item in enumerate(self.on_exits, 1):
|
636
|
+
if (is_abstract is not None and
|
637
|
+
((item.is_abstract and not is_abstract) or (not item.is_abstract and is_abstract))):
|
638
|
+
continue
|
639
|
+
if with_ids:
|
640
|
+
retval.append((id_, item))
|
641
|
+
else:
|
642
|
+
retval.append(item)
|
643
|
+
return retval
|
644
|
+
|
645
|
+
@property
|
646
|
+
def abstract_on_exits(self) -> List[OnStage]:
|
647
|
+
"""
|
648
|
+
Get all abstract exit actions.
|
649
|
+
|
650
|
+
:return: List of abstract exit actions
|
651
|
+
:rtype: List[OnStage]
|
652
|
+
"""
|
653
|
+
return self.list_on_exits(is_abstract=True, with_ids=False)
|
654
|
+
|
655
|
+
@property
|
656
|
+
def non_abstract_on_exits(self) -> List[OnStage]:
|
657
|
+
"""
|
658
|
+
Get all non-abstract exit actions.
|
659
|
+
|
660
|
+
:return: List of non-abstract exit actions
|
661
|
+
:rtype: List[OnStage]
|
662
|
+
"""
|
663
|
+
return self.list_on_exits(is_abstract=False, with_ids=False)
|
664
|
+
|
665
|
+
def list_on_during_aspects(self, is_abstract: Optional[bool] = None, aspect: Optional[str] = None,
|
666
|
+
with_ids: bool = False) -> List[Union[Tuple[int, OnAspect], OnAspect]]:
|
667
|
+
"""
|
668
|
+
Get a list of during aspect actions, optionally filtered by abstract status, aspect, and with IDs.
|
669
|
+
|
670
|
+
:param is_abstract: If provided, filter to only abstract (True) or non-abstract (False) actions
|
671
|
+
:type is_abstract: Optional[bool]
|
672
|
+
:param aspect: If provided, filter to only actions with the given aspect ('before' or 'after')
|
673
|
+
:type aspect: Optional[str]
|
674
|
+
:param with_ids: Whether to include numeric IDs with the actions
|
675
|
+
:type with_ids: bool
|
676
|
+
:return: List of during aspect actions, optionally with IDs
|
677
|
+
:rtype: List[Union[Tuple[int, OnAspect], OnAspect]]
|
678
|
+
"""
|
679
|
+
retval = []
|
680
|
+
for id_, item in enumerate(self.on_during_aspects, 1):
|
681
|
+
if (is_abstract is not None and
|
682
|
+
((item.is_abstract and not is_abstract) or (not item.is_abstract and is_abstract))):
|
683
|
+
continue
|
684
|
+
if aspect is not None and item.aspect != aspect:
|
685
|
+
continue
|
686
|
+
|
687
|
+
if with_ids:
|
688
|
+
retval.append((id_, item))
|
689
|
+
else:
|
690
|
+
retval.append(item)
|
691
|
+
return retval
|
692
|
+
|
693
|
+
@property
|
694
|
+
def abstract_on_during_aspects(self) -> List[OnAspect]:
|
695
|
+
"""
|
696
|
+
Get all abstract during aspect actions.
|
697
|
+
|
698
|
+
:return: List of abstract during aspect actions
|
699
|
+
:rtype: List[OnAspect]
|
700
|
+
"""
|
701
|
+
return self.list_on_during_aspects(is_abstract=True, with_ids=False)
|
702
|
+
|
703
|
+
@property
|
704
|
+
def non_abstract_on_during_aspects(self) -> List[OnAspect]:
|
705
|
+
"""
|
706
|
+
Get all non-abstract during aspect actions.
|
707
|
+
|
708
|
+
:return: List of non-abstract during aspect actions
|
709
|
+
:rtype: List[OnAspect]
|
710
|
+
"""
|
711
|
+
return self.list_on_during_aspects(is_abstract=False, with_ids=False)
|
712
|
+
|
713
|
+
def iter_on_during_before_aspect_recursively(self, is_abstract: Optional[bool] = None, with_ids: bool = False) \
|
714
|
+
-> List[Union[Tuple[int, 'State', Union[OnAspect, OnStage]], Tuple['State', Union[OnAspect, OnStage]]]]:
|
715
|
+
"""
|
716
|
+
Recursively iterate through 'before' aspect during actions from parent states to this state.
|
717
|
+
|
718
|
+
This method traverses the state hierarchy from the root state to this state,
|
719
|
+
yielding all 'before' aspect during actions along the way.
|
720
|
+
|
721
|
+
:param is_abstract: If provided, filter to only abstract (True) or non-abstract (False) actions
|
722
|
+
:type is_abstract: Optional[bool]
|
723
|
+
:param with_ids: Whether to include numeric IDs with the actions
|
724
|
+
:type with_ids: bool
|
725
|
+
:yield: Tuples of (state, action) or (id, state, action) if with_ids is True
|
726
|
+
:rtype: List[Union[Tuple[int, 'State', Union[OnAspect, OnStage]], Tuple['State', Union[OnAspect, OnStage]]]]
|
727
|
+
"""
|
728
|
+
if self.parent is not None:
|
729
|
+
yield from self.parent.iter_on_during_before_aspect_recursively(is_abstract=is_abstract, with_ids=with_ids)
|
730
|
+
if with_ids:
|
731
|
+
for id_, item in self.list_on_during_aspects(is_abstract=is_abstract, aspect='before', with_ids=with_ids):
|
732
|
+
yield id_, self, item
|
733
|
+
else:
|
734
|
+
for item in self.list_on_during_aspects(is_abstract=is_abstract, aspect='before', with_ids=with_ids):
|
735
|
+
yield self, item
|
736
|
+
|
737
|
+
def iter_on_during_after_aspect_recursively(self, is_abstract: Optional[bool] = None, with_ids: bool = False) \
|
738
|
+
-> List[Union[Tuple[int, 'State', Union[OnAspect, OnStage]], Tuple['State', Union[OnAspect, OnStage]]]]:
|
739
|
+
"""
|
740
|
+
Recursively iterate through 'after' aspect during actions from this state to the root state.
|
741
|
+
|
742
|
+
This method traverses the state hierarchy from this state to the root state,
|
743
|
+
yielding all 'after' aspect during actions along the way.
|
744
|
+
|
745
|
+
:param is_abstract: If provided, filter to only abstract (True) or non-abstract (False) actions
|
746
|
+
:type is_abstract: Optional[bool]
|
747
|
+
:param with_ids: Whether to include numeric IDs with the actions
|
748
|
+
:type with_ids: bool
|
749
|
+
:yield: Tuples of (state, action) or (id, state, action) if with_ids is True
|
750
|
+
:rtype: List[Union[Tuple[int, 'State', Union[OnAspect, OnStage]], Tuple['State', Union[OnAspect, OnStage]]]]
|
751
|
+
"""
|
752
|
+
if with_ids:
|
753
|
+
for id_, item in self.list_on_during_aspects(is_abstract=is_abstract, aspect='after', with_ids=with_ids):
|
754
|
+
yield id_, self, item
|
755
|
+
else:
|
756
|
+
for item in self.list_on_during_aspects(is_abstract=is_abstract, aspect='after', with_ids=with_ids):
|
757
|
+
yield self, item
|
758
|
+
if self.parent is not None:
|
759
|
+
yield from self.parent.iter_on_during_after_aspect_recursively(is_abstract=is_abstract, with_ids=with_ids)
|
760
|
+
|
761
|
+
def iter_on_during_aspect_recursively(self, is_abstract: Optional[bool] = None, with_ids: bool = False) \
|
762
|
+
-> List[Union[Tuple[int, 'State', Union[OnAspect, OnStage]], Tuple['State', Union[OnAspect, OnStage]]]]:
|
763
|
+
"""
|
764
|
+
Recursively iterate through all during actions in the proper execution order.
|
765
|
+
|
766
|
+
This method yields actions in the following order:
|
767
|
+
|
768
|
+
1. 'Before' aspect actions from root state to this state
|
769
|
+
2. Regular during actions for this state
|
770
|
+
3. 'After' aspect actions from this state to root state
|
771
|
+
|
772
|
+
:param is_abstract: If provided, filter to only abstract (True) or non-abstract (False) actions
|
773
|
+
:type is_abstract: Optional[bool]
|
774
|
+
:param with_ids: Whether to include numeric IDs with the actions
|
775
|
+
:type with_ids: bool
|
776
|
+
:yield: Tuples of (state, action) or (id, state, action) if with_ids is True
|
777
|
+
:rtype: List[Union[Tuple[int, 'State', Union[OnAspect, OnStage]], Tuple['State', Union[OnAspect, OnStage]]]]
|
778
|
+
"""
|
779
|
+
yield from self.iter_on_during_before_aspect_recursively(is_abstract=is_abstract, with_ids=with_ids)
|
780
|
+
if with_ids:
|
781
|
+
for id_, item in self.list_on_durings(is_abstract=is_abstract, aspect=None, with_ids=with_ids):
|
782
|
+
yield id_, self, item
|
783
|
+
else:
|
784
|
+
for item in self.list_on_durings(is_abstract=is_abstract, aspect=None, with_ids=with_ids):
|
785
|
+
yield self, item
|
786
|
+
yield from self.iter_on_during_after_aspect_recursively(is_abstract=is_abstract, with_ids=with_ids)
|
787
|
+
|
788
|
+
def list_on_during_aspect_recursively(self, is_abstract: Optional[bool] = None, with_ids: bool = False) \
|
789
|
+
-> List[Union[Tuple[int, 'State', Union[OnAspect, OnStage]], Tuple['State', Union[OnAspect, OnStage]]]]:
|
790
|
+
"""
|
791
|
+
Get a list of all during actions in the proper execution order.
|
792
|
+
|
793
|
+
This is a convenience method that collects the results of iter_on_during_aspect_recursively.
|
794
|
+
|
795
|
+
:param is_abstract: If provided, filter to only abstract (True) or non-abstract (False) actions
|
796
|
+
:type is_abstract: Optional[bool]
|
797
|
+
:param with_ids: Whether to include numeric IDs with the actions
|
798
|
+
:type with_ids: bool
|
799
|
+
:return: List of during actions in execution order
|
800
|
+
:rtype: List[Union[Tuple[int, 'State', Union[OnAspect, OnStage]], Tuple['State', Union[OnAspect, OnStage]]]]
|
801
|
+
"""
|
802
|
+
return list(self.iter_on_during_aspect_recursively(is_abstract, with_ids))
|
803
|
+
|
804
|
+
@classmethod
|
805
|
+
def transition_to_ast_node(cls, self: Optional['State'], transition: Transition):
|
806
|
+
"""
|
807
|
+
Convert a transition to an AST node, considering the context of its parent state.
|
808
|
+
|
809
|
+
:param self: The parent state, or None
|
810
|
+
:type self: Optional['State']
|
811
|
+
:param transition: The transition to convert
|
812
|
+
:type transition: Transition
|
813
|
+
:return: A transition definition AST node
|
814
|
+
:rtype: dsl_nodes.TransitionDefinition
|
815
|
+
"""
|
816
|
+
if self:
|
817
|
+
cur_path = self.path
|
818
|
+
else:
|
819
|
+
cur_path = ()
|
820
|
+
|
821
|
+
if transition.event:
|
822
|
+
if len(transition.event.path) > len(cur_path) and transition.event.path[:len(cur_path)] == cur_path:
|
823
|
+
# is relative path
|
824
|
+
event_id = dsl_nodes.ChainID(path=list(transition.event.path[len(cur_path):]), is_absolute=False)
|
825
|
+
else:
|
826
|
+
# use absolute path
|
827
|
+
event_id = dsl_nodes.ChainID(path=list(transition.event.path[1:]), is_absolute=True)
|
828
|
+
else:
|
829
|
+
event_id = None
|
830
|
+
|
831
|
+
return dsl_nodes.TransitionDefinition(
|
832
|
+
from_state=transition.from_state,
|
833
|
+
to_state=transition.to_state,
|
834
|
+
event_id=event_id,
|
835
|
+
condition_expr=transition.guard.to_ast_node() if transition.guard is not None else None,
|
836
|
+
post_operations=[
|
837
|
+
item.to_ast_node()
|
838
|
+
for item in transition.effects
|
839
|
+
]
|
840
|
+
)
|
841
|
+
|
842
|
+
def to_transition_ast_node(self, transition: Transition) -> dsl_nodes.TransitionDefinition:
|
843
|
+
"""
|
844
|
+
Convert a transition to an AST node in the context of this state.
|
845
|
+
|
846
|
+
:param transition: The transition to convert
|
847
|
+
:type transition: Transition
|
848
|
+
:return: A transition definition AST node
|
849
|
+
:rtype: dsl_nodes.TransitionDefinition
|
850
|
+
"""
|
851
|
+
return self.transition_to_ast_node(self, transition)
|
852
|
+
|
853
|
+
def to_ast_node(self) -> dsl_nodes.StateDefinition:
|
854
|
+
"""
|
855
|
+
Convert this state to an AST node.
|
856
|
+
|
857
|
+
:return: A state definition AST node
|
858
|
+
:rtype: dsl_nodes.StateDefinition
|
859
|
+
"""
|
860
|
+
return dsl_nodes.StateDefinition(
|
861
|
+
name=self.name,
|
862
|
+
substates=[
|
863
|
+
substate.to_ast_node()
|
864
|
+
for _, substate in self.substates.items()
|
865
|
+
],
|
866
|
+
transitions=[self.to_transition_ast_node(trans) for trans in self.transitions],
|
867
|
+
enters=[item.to_ast_node() for item in self.on_enters],
|
868
|
+
durings=[item.to_ast_node() for item in self.on_durings],
|
869
|
+
exits=[item.to_ast_node() for item in self.on_exits],
|
870
|
+
during_aspects=[item.to_ast_node() for item in self.on_during_aspects],
|
871
|
+
)
|
872
|
+
|
873
|
+
def to_plantuml(self) -> str:
|
874
|
+
"""
|
875
|
+
Convert this state to PlantUML notation.
|
876
|
+
|
877
|
+
:return: PlantUML representation of the state
|
878
|
+
:rtype: str
|
879
|
+
"""
|
880
|
+
with io.StringIO() as sf:
|
881
|
+
if self.is_leaf_state:
|
882
|
+
print(f'state {self.name}', file=sf, end='')
|
883
|
+
else:
|
884
|
+
print(f'state {self.name} {{', file=sf)
|
885
|
+
for state in self.substates.values():
|
886
|
+
print(indent(state.to_plantuml(), prefix=' '), file=sf)
|
887
|
+
for trans in self.transitions:
|
888
|
+
with io.StringIO() as tf:
|
889
|
+
print('[*]' if trans.from_state is dsl_nodes.INIT_STATE else trans.from_state, file=tf, end='')
|
890
|
+
print(' --> ', file=tf, end='')
|
891
|
+
print('[*]' if trans.to_state is dsl_nodes.EXIT_STATE else trans.to_state, file=tf, end='')
|
892
|
+
|
893
|
+
if trans.event is not None:
|
894
|
+
print(f' : {".".join(list(trans.event.path[len(self.path):]))}', file=tf, end='')
|
895
|
+
elif trans.guard is not None:
|
896
|
+
print(f' : {trans.guard.to_ast_node()}', file=tf, end='')
|
897
|
+
|
898
|
+
if len(trans.effects) > 0:
|
899
|
+
print('', file=tf)
|
900
|
+
print('note on link', file=tf)
|
901
|
+
print('effect {', file=tf)
|
902
|
+
for operation in trans.effects:
|
903
|
+
print(f' {operation.to_ast_node()}', file=tf)
|
904
|
+
print('}', file=tf)
|
905
|
+
print('end note', file=tf, end='')
|
906
|
+
|
907
|
+
trans_text = tf.getvalue()
|
908
|
+
print(indent(trans_text, prefix=' '), file=sf)
|
909
|
+
print(f'}}', file=sf, end='')
|
910
|
+
|
911
|
+
if self.on_enters or self.on_durings or self.on_exits:
|
912
|
+
print('', file=sf)
|
913
|
+
with io.StringIO() as tf:
|
914
|
+
for enter_item in self.on_enters:
|
915
|
+
print(enter_item.to_ast_node(), file=tf)
|
916
|
+
for during_item in self.on_durings:
|
917
|
+
print(during_item.to_ast_node(), file=tf)
|
918
|
+
for exit_item in self.on_exits:
|
919
|
+
print(exit_item.to_ast_node(), file=tf)
|
920
|
+
for during_aspect_item in self.on_during_aspects:
|
921
|
+
print(during_aspect_item.to_ast_node(), file=tf)
|
922
|
+
text = json.dumps(tf.getvalue().rstrip()).strip("\"")
|
923
|
+
print(f'{self.name} : {text}', file=sf, end='')
|
924
|
+
|
925
|
+
return sf.getvalue()
|
926
|
+
|
927
|
+
def walk_states(self):
|
928
|
+
"""
|
929
|
+
Iterate through this state and all its substates recursively.
|
930
|
+
|
931
|
+
:yield: Each state in the hierarchy, starting with this one
|
932
|
+
:rtype: Iterator['State']
|
933
|
+
"""
|
934
|
+
yield self
|
935
|
+
for _, substate in self.substates.items():
|
936
|
+
yield from substate.walk_states()
|
937
|
+
|
938
|
+
|
939
|
+
@dataclass
|
940
|
+
class VarDefine(AstExportable):
|
941
|
+
"""
|
942
|
+
Represents a variable definition in a state machine.
|
943
|
+
|
944
|
+
:param name: The name of the variable
|
945
|
+
:type name: str
|
946
|
+
:param type: The type of the variable
|
947
|
+
:type type: str
|
948
|
+
:param init: The initial value expression
|
949
|
+
:type init: Expr
|
950
|
+
"""
|
951
|
+
name: str
|
952
|
+
type: str
|
953
|
+
init: Expr
|
954
|
+
|
955
|
+
def to_ast_node(self) -> dsl_nodes.DefAssignment:
|
956
|
+
"""
|
957
|
+
Convert this variable definition to an AST node.
|
958
|
+
|
959
|
+
:return: A definition assignment AST node
|
960
|
+
:rtype: dsl_nodes.DefAssignment
|
961
|
+
"""
|
962
|
+
return dsl_nodes.DefAssignment(
|
963
|
+
name=self.name,
|
964
|
+
type=self.type,
|
965
|
+
expr=self.init.to_ast_node(),
|
966
|
+
)
|
967
|
+
|
968
|
+
def name_ast_node(self) -> dsl_nodes.Name:
|
969
|
+
"""
|
970
|
+
Convert the variable name to an AST node.
|
971
|
+
|
972
|
+
:return: A name AST node
|
973
|
+
:rtype: dsl_nodes.Name
|
974
|
+
"""
|
975
|
+
return dsl_nodes.Name(self.name)
|
976
|
+
|
977
|
+
|
978
|
+
@dataclass
|
979
|
+
class StateMachine(AstExportable, PlantUMLExportable):
|
980
|
+
"""
|
981
|
+
Represents a complete state machine with variable definitions and a root state.
|
982
|
+
|
983
|
+
:param defines: Dictionary mapping variable names to their definitions
|
984
|
+
:type defines: Dict[str, VarDefine]
|
985
|
+
:param root_state: The root state of the state machine
|
986
|
+
:type root_state: State
|
987
|
+
"""
|
988
|
+
defines: Dict[str, VarDefine]
|
989
|
+
root_state: State
|
990
|
+
|
991
|
+
def to_ast_node(self) -> dsl_nodes.StateMachineDSLProgram:
|
992
|
+
"""
|
993
|
+
Convert this state machine to an AST node.
|
994
|
+
|
995
|
+
:return: A state machine DSL program AST node
|
996
|
+
:rtype: dsl_nodes.StateMachineDSLProgram
|
997
|
+
"""
|
998
|
+
return dsl_nodes.StateMachineDSLProgram(
|
999
|
+
definitions=[
|
1000
|
+
def_item.to_ast_node()
|
1001
|
+
for _, def_item in self.defines.items()
|
1002
|
+
],
|
1003
|
+
root_state=self.root_state.to_ast_node(),
|
1004
|
+
)
|
1005
|
+
|
1006
|
+
def to_plantuml(self) -> str:
|
1007
|
+
"""
|
1008
|
+
Convert this state machine to PlantUML notation.
|
1009
|
+
|
1010
|
+
:return: PlantUML representation of the state machine
|
1011
|
+
:rtype: str
|
1012
|
+
"""
|
1013
|
+
with io.StringIO() as sf:
|
1014
|
+
print('@startuml', file=sf)
|
1015
|
+
if self.defines:
|
1016
|
+
print('note as DefinitionNote', file=sf)
|
1017
|
+
print('defines {', file=sf)
|
1018
|
+
for def_item in self.defines.values():
|
1019
|
+
print(f' {def_item.to_ast_node()}', file=sf)
|
1020
|
+
print('}', file=sf)
|
1021
|
+
print('end note', file=sf)
|
1022
|
+
print('', file=sf)
|
1023
|
+
print(self.root_state.to_plantuml(), file=sf)
|
1024
|
+
print(f'[*] --> {self.root_state.name}', file=sf)
|
1025
|
+
print(f'{self.root_state.name} --> [*]', file=sf)
|
1026
|
+
print('@enduml', file=sf, end='')
|
1027
|
+
return sf.getvalue()
|
1028
|
+
|
1029
|
+
def walk_states(self):
|
1030
|
+
"""
|
1031
|
+
Iterate through all states in the state machine.
|
1032
|
+
|
1033
|
+
:yield: Each state in the hierarchy
|
1034
|
+
:rtype: Iterator[State]
|
1035
|
+
"""
|
1036
|
+
yield from self.root_state.walk_states()
|
1037
|
+
|
1038
|
+
|
1039
|
+
def parse_dsl_node_to_state_machine(dnode: dsl_nodes.StateMachineDSLProgram) -> StateMachine:
|
1040
|
+
"""
|
1041
|
+
Parse a state machine DSL program AST node into a StateMachine object.
|
1042
|
+
|
1043
|
+
This function validates the state machine structure and builds a complete
|
1044
|
+
StateMachine object with all states, transitions, events, and variable definitions.
|
1045
|
+
|
1046
|
+
:param dnode: The state machine DSL program AST node to parse
|
1047
|
+
:type dnode: dsl_nodes.StateMachineDSLProgram
|
1048
|
+
|
1049
|
+
:return: The parsed state machine
|
1050
|
+
:rtype: StateMachine
|
1051
|
+
|
1052
|
+
:raises SyntaxError: If there are syntax errors in the state machine definition,
|
1053
|
+
such as duplicate variable definitions, unknown states in
|
1054
|
+
transitions, missing entry transitions, etc.
|
1055
|
+
"""
|
1056
|
+
d_defines = {}
|
1057
|
+
for def_item in dnode.definitions:
|
1058
|
+
if def_item.name not in d_defines:
|
1059
|
+
d_defines[def_item.name] = VarDefine(
|
1060
|
+
name=def_item.name,
|
1061
|
+
type=def_item.type,
|
1062
|
+
init=parse_expr_node_to_expr(def_item.expr),
|
1063
|
+
)
|
1064
|
+
else:
|
1065
|
+
raise SyntaxError(f'Duplicated variable definition - {def_item}.')
|
1066
|
+
|
1067
|
+
def _recursive_build_states(node: dsl_nodes.StateDefinition, current_path: Tuple[str, ...]):
|
1068
|
+
current_path = tuple((*current_path, node.name))
|
1069
|
+
d_substates = {}
|
1070
|
+
|
1071
|
+
for subnode in node.substates:
|
1072
|
+
if subnode.name not in d_substates:
|
1073
|
+
d_substates[subnode.name] = _recursive_build_states(subnode, current_path=current_path)
|
1074
|
+
else:
|
1075
|
+
raise SyntaxError(f'Duplicate state name in namespace {".".join(current_path)!r}:\n{subnode}')
|
1076
|
+
|
1077
|
+
my_state = State(
|
1078
|
+
name=node.name,
|
1079
|
+
path=current_path,
|
1080
|
+
substates=d_substates,
|
1081
|
+
)
|
1082
|
+
for _, substate in d_substates.items():
|
1083
|
+
substate.parent = my_state
|
1084
|
+
return my_state
|
1085
|
+
|
1086
|
+
root_state = _recursive_build_states(dnode.root_state, current_path=())
|
1087
|
+
|
1088
|
+
def _recursive_finish_states(node: dsl_nodes.StateDefinition, current_state: State, current_path: Tuple[str, ...],
|
1089
|
+
force_transitions: List[dsl_nodes.ForceTransitionDefinition] = None):
|
1090
|
+
current_path = tuple((*current_path, current_state.name))
|
1091
|
+
force_transitions = list(force_transitions or [])
|
1092
|
+
|
1093
|
+
force_transition_tuples_to_inherit = []
|
1094
|
+
for f_transnode in [*force_transitions, *node.force_transitions]:
|
1095
|
+
if f_transnode.from_state == dsl_nodes.ALL:
|
1096
|
+
from_state = dsl_nodes.ALL
|
1097
|
+
else:
|
1098
|
+
from_state = f_transnode.from_state
|
1099
|
+
if from_state not in current_state.substates:
|
1100
|
+
raise SyntaxError(f'Unknown from state {from_state!r} of force transition:\n{f_transnode}')
|
1101
|
+
|
1102
|
+
if f_transnode.to_state is dsl_nodes.EXIT_STATE:
|
1103
|
+
to_state = dsl_nodes.EXIT_STATE
|
1104
|
+
else:
|
1105
|
+
to_state = f_transnode.to_state
|
1106
|
+
if to_state not in current_state.substates:
|
1107
|
+
raise SyntaxError(f'Unknown to state {to_state!r} of force transition:\n{f_transnode}')
|
1108
|
+
|
1109
|
+
my_event_id, trans_event = None, None
|
1110
|
+
if f_transnode.event_id is not None:
|
1111
|
+
my_event_id = f_transnode.event_id
|
1112
|
+
if not my_event_id.is_absolute:
|
1113
|
+
my_event_id = dsl_nodes.ChainID(
|
1114
|
+
path=[*current_state.path[1:], *my_event_id.path],
|
1115
|
+
is_absolute=True
|
1116
|
+
)
|
1117
|
+
start_state = root_state
|
1118
|
+
base_path = (root_state.name,)
|
1119
|
+
for seg in my_event_id.path[:-1]:
|
1120
|
+
if seg in start_state.substates:
|
1121
|
+
start_state = start_state.substates[seg]
|
1122
|
+
else:
|
1123
|
+
raise SyntaxError(
|
1124
|
+
f'Cannot find state {".".join((*base_path, *my_event_id.path[:-1]))} for transition:\n{f_transnode}')
|
1125
|
+
|
1126
|
+
suffix_name = my_event_id.path[-1]
|
1127
|
+
if suffix_name not in start_state.events:
|
1128
|
+
start_state.events[suffix_name] = Event(
|
1129
|
+
name=suffix_name,
|
1130
|
+
state_path=start_state.path,
|
1131
|
+
)
|
1132
|
+
trans_event = start_state.events[suffix_name]
|
1133
|
+
|
1134
|
+
condition_expr, guard = f_transnode.condition_expr, None
|
1135
|
+
if f_transnode.condition_expr is not None:
|
1136
|
+
guard = parse_expr_node_to_expr(f_transnode.condition_expr)
|
1137
|
+
unknown_vars = []
|
1138
|
+
for var in guard.list_variables():
|
1139
|
+
if var.name not in d_defines:
|
1140
|
+
unknown_vars.append(var.name)
|
1141
|
+
if unknown_vars:
|
1142
|
+
raise SyntaxError(
|
1143
|
+
f'Unknown guard variable {", ".join(unknown_vars)} in force transition:\n{f_transnode}')
|
1144
|
+
|
1145
|
+
force_transition_tuples_to_inherit.append(
|
1146
|
+
(from_state, to_state, my_event_id, trans_event, condition_expr, guard))
|
1147
|
+
|
1148
|
+
transitions = current_state.transitions
|
1149
|
+
for subnode in node.substates:
|
1150
|
+
_inner_force_transitions = [*force_transitions]
|
1151
|
+
for from_state, to_state, my_event_id, trans_event, condition_expr, guard in force_transition_tuples_to_inherit:
|
1152
|
+
if from_state is dsl_nodes.ALL or from_state == subnode.name:
|
1153
|
+
transitions.append(Transition(
|
1154
|
+
from_state=subnode.name,
|
1155
|
+
to_state=to_state,
|
1156
|
+
event=trans_event,
|
1157
|
+
guard=guard,
|
1158
|
+
effects=[],
|
1159
|
+
))
|
1160
|
+
_inner_force_transitions.append(dsl_nodes.ForceTransitionDefinition(
|
1161
|
+
from_state=dsl_nodes.ALL,
|
1162
|
+
to_state=dsl_nodes.EXIT_STATE,
|
1163
|
+
event_id=my_event_id,
|
1164
|
+
condition_expr=condition_expr,
|
1165
|
+
))
|
1166
|
+
|
1167
|
+
_recursive_finish_states(
|
1168
|
+
node=subnode,
|
1169
|
+
current_state=current_state.substates[subnode.name],
|
1170
|
+
current_path=current_path,
|
1171
|
+
force_transitions=_inner_force_transitions,
|
1172
|
+
)
|
1173
|
+
|
1174
|
+
has_entry_trans = False
|
1175
|
+
for transnode in node.transitions:
|
1176
|
+
if transnode.from_state is dsl_nodes.INIT_STATE:
|
1177
|
+
from_state = dsl_nodes.INIT_STATE
|
1178
|
+
has_entry_trans = True
|
1179
|
+
else:
|
1180
|
+
from_state = transnode.from_state
|
1181
|
+
if from_state not in current_state.substates:
|
1182
|
+
raise SyntaxError(f'Unknown from state {from_state!r} of transition:\n{transnode}')
|
1183
|
+
|
1184
|
+
if transnode.to_state is dsl_nodes.EXIT_STATE:
|
1185
|
+
to_state = dsl_nodes.EXIT_STATE
|
1186
|
+
else:
|
1187
|
+
to_state = transnode.to_state
|
1188
|
+
if to_state not in current_state.substates:
|
1189
|
+
raise SyntaxError(f'Unknown to state {to_state!r} of transition:\n{transnode}')
|
1190
|
+
|
1191
|
+
trans_event, guard = None, None
|
1192
|
+
if transnode.event_id is not None:
|
1193
|
+
if transnode.event_id.is_absolute:
|
1194
|
+
start_state = root_state
|
1195
|
+
base_path = (root_state.name,)
|
1196
|
+
else:
|
1197
|
+
start_state = current_state
|
1198
|
+
base_path = current_state.path
|
1199
|
+
for seg in transnode.event_id.path[:-1]:
|
1200
|
+
if seg in start_state.substates:
|
1201
|
+
start_state = start_state.substates[seg]
|
1202
|
+
else:
|
1203
|
+
raise SyntaxError(
|
1204
|
+
f'Cannot find state {".".join((*base_path, *transnode.event_id.path[:-1]))} for transition:\n{transnode}')
|
1205
|
+
|
1206
|
+
suffix_name = transnode.event_id.path[-1]
|
1207
|
+
if suffix_name not in start_state.events:
|
1208
|
+
start_state.events[suffix_name] = Event(
|
1209
|
+
name=suffix_name,
|
1210
|
+
state_path=start_state.path,
|
1211
|
+
)
|
1212
|
+
trans_event = start_state.events[suffix_name]
|
1213
|
+
|
1214
|
+
if transnode.condition_expr is not None:
|
1215
|
+
guard = parse_expr_node_to_expr(transnode.condition_expr)
|
1216
|
+
unknown_vars = []
|
1217
|
+
for var in guard.list_variables():
|
1218
|
+
if var.name not in d_defines:
|
1219
|
+
unknown_vars.append(var.name)
|
1220
|
+
if unknown_vars:
|
1221
|
+
raise SyntaxError(f'Unknown guard variable {", ".join(unknown_vars)} in transition:\n{transnode}')
|
1222
|
+
|
1223
|
+
post_operations = []
|
1224
|
+
for op_item in transnode.post_operations:
|
1225
|
+
operation_val = parse_expr_node_to_expr(op_item.expr)
|
1226
|
+
unknown_vars = []
|
1227
|
+
for var in operation_val.list_variables():
|
1228
|
+
if var.name not in d_defines:
|
1229
|
+
unknown_vars.append(var.name)
|
1230
|
+
if op_item.name not in d_defines and op_item.name not in unknown_vars:
|
1231
|
+
unknown_vars.append(op_item.name)
|
1232
|
+
if unknown_vars:
|
1233
|
+
raise SyntaxError(
|
1234
|
+
f'Unknown transition operation variable {", ".join(unknown_vars)} in transition:\n{transnode}')
|
1235
|
+
post_operations.append(Operation(var_name=op_item.name, expr=operation_val))
|
1236
|
+
|
1237
|
+
transition = Transition(
|
1238
|
+
from_state=from_state,
|
1239
|
+
to_state=to_state,
|
1240
|
+
event=trans_event,
|
1241
|
+
guard=guard,
|
1242
|
+
effects=post_operations,
|
1243
|
+
)
|
1244
|
+
transitions.append(transition)
|
1245
|
+
|
1246
|
+
if current_state.substates and not has_entry_trans:
|
1247
|
+
raise SyntaxError(
|
1248
|
+
f'At least 1 entry transition should be assigned in non-leaf state {node.name!r}:\n{node}')
|
1249
|
+
|
1250
|
+
on_enters = current_state.on_enters
|
1251
|
+
for enter_item in node.enters:
|
1252
|
+
if isinstance(enter_item, dsl_nodes.EnterOperations):
|
1253
|
+
enter_operations = []
|
1254
|
+
for op_item in enter_item.operations:
|
1255
|
+
operation_val = parse_expr_node_to_expr(op_item.expr)
|
1256
|
+
unknown_vars = []
|
1257
|
+
for var in operation_val.list_variables():
|
1258
|
+
if var.name not in d_defines:
|
1259
|
+
unknown_vars.append(var.name)
|
1260
|
+
if op_item.name not in d_defines and op_item.name not in unknown_vars:
|
1261
|
+
unknown_vars.append(op_item.name)
|
1262
|
+
if unknown_vars:
|
1263
|
+
raise SyntaxError(
|
1264
|
+
f'Unknown enter operation variable {", ".join(unknown_vars)} in transition:\n{enter_item}')
|
1265
|
+
enter_operations.append(Operation(var_name=op_item.name, expr=operation_val))
|
1266
|
+
on_enters.append(OnStage(
|
1267
|
+
stage='enter',
|
1268
|
+
aspect=None,
|
1269
|
+
name=enter_item.name,
|
1270
|
+
doc=None,
|
1271
|
+
operations=enter_operations,
|
1272
|
+
))
|
1273
|
+
elif isinstance(enter_item, dsl_nodes.EnterAbstractFunction):
|
1274
|
+
on_enters.append(OnStage(
|
1275
|
+
stage='enter',
|
1276
|
+
aspect=None,
|
1277
|
+
name=enter_item.name,
|
1278
|
+
doc=enter_item.doc,
|
1279
|
+
operations=[],
|
1280
|
+
))
|
1281
|
+
|
1282
|
+
on_durings = current_state.on_durings
|
1283
|
+
for during_item in node.durings:
|
1284
|
+
if not current_state.substates and during_item.aspect is not None:
|
1285
|
+
raise SyntaxError(
|
1286
|
+
f'For leaf state {node.name!r}, during cannot assign aspect {during_item.aspect!r}:\n{during_item}')
|
1287
|
+
if current_state.substates and during_item.aspect is None:
|
1288
|
+
raise SyntaxError(
|
1289
|
+
f'For composite state {node.name!r}, during must assign aspect to either \'before\' or \'after\':\n{during_item}')
|
1290
|
+
|
1291
|
+
if isinstance(during_item, dsl_nodes.DuringOperations):
|
1292
|
+
during_operations = []
|
1293
|
+
for op_item in during_item.operations:
|
1294
|
+
operation_val = parse_expr_node_to_expr(op_item.expr)
|
1295
|
+
unknown_vars = []
|
1296
|
+
for var in operation_val.list_variables():
|
1297
|
+
if var.name not in d_defines:
|
1298
|
+
unknown_vars.append(var.name)
|
1299
|
+
if op_item.name not in d_defines and op_item.name not in unknown_vars:
|
1300
|
+
unknown_vars.append(op_item.name)
|
1301
|
+
if unknown_vars:
|
1302
|
+
raise SyntaxError(
|
1303
|
+
f'Unknown during operation variable {", ".join(unknown_vars)} in transition:\n{during_item}')
|
1304
|
+
during_operations.append(Operation(var_name=op_item.name, expr=operation_val))
|
1305
|
+
on_durings.append(OnStage(
|
1306
|
+
stage='during',
|
1307
|
+
aspect=during_item.aspect,
|
1308
|
+
name=during_item.name,
|
1309
|
+
doc=None,
|
1310
|
+
operations=during_operations,
|
1311
|
+
))
|
1312
|
+
elif isinstance(during_item, dsl_nodes.DuringAbstractFunction):
|
1313
|
+
on_durings.append(OnStage(
|
1314
|
+
stage='during',
|
1315
|
+
aspect=during_item.aspect,
|
1316
|
+
name=during_item.name,
|
1317
|
+
doc=during_item.doc,
|
1318
|
+
operations=[],
|
1319
|
+
))
|
1320
|
+
|
1321
|
+
on_exits = current_state.on_exits
|
1322
|
+
for exit_item in node.exits:
|
1323
|
+
if isinstance(exit_item, dsl_nodes.ExitOperations):
|
1324
|
+
exit_operations = []
|
1325
|
+
for op_item in exit_item.operations:
|
1326
|
+
operation_val = parse_expr_node_to_expr(op_item.expr)
|
1327
|
+
unknown_vars = []
|
1328
|
+
for var in operation_val.list_variables():
|
1329
|
+
if var.name not in d_defines:
|
1330
|
+
unknown_vars.append(var.name)
|
1331
|
+
if op_item.name not in d_defines and op_item.name not in unknown_vars:
|
1332
|
+
unknown_vars.append(op_item.name)
|
1333
|
+
if unknown_vars:
|
1334
|
+
raise SyntaxError(
|
1335
|
+
f'Unknown exit operation variable {", ".join(unknown_vars)} in transition:\n{exit_item}')
|
1336
|
+
exit_operations.append(Operation(var_name=op_item.name, expr=operation_val))
|
1337
|
+
on_exits.append(OnStage(
|
1338
|
+
stage='exit',
|
1339
|
+
aspect=None,
|
1340
|
+
name=exit_item.name,
|
1341
|
+
doc=None,
|
1342
|
+
operations=exit_operations,
|
1343
|
+
))
|
1344
|
+
elif isinstance(exit_item, dsl_nodes.ExitAbstractFunction):
|
1345
|
+
on_exits.append(OnStage(
|
1346
|
+
stage='exit',
|
1347
|
+
aspect=None,
|
1348
|
+
name=exit_item.name,
|
1349
|
+
doc=exit_item.doc,
|
1350
|
+
operations=[],
|
1351
|
+
))
|
1352
|
+
|
1353
|
+
on_during_aspects = current_state.on_during_aspects
|
1354
|
+
for during_aspect_item in node.during_aspects:
|
1355
|
+
if isinstance(during_aspect_item, dsl_nodes.DuringAspectOperations):
|
1356
|
+
during_operations = []
|
1357
|
+
for op_item in during_aspect_item.operations:
|
1358
|
+
operation_val = parse_expr_node_to_expr(op_item.expr)
|
1359
|
+
unknown_vars = []
|
1360
|
+
for var in operation_val.list_variables():
|
1361
|
+
if var.name not in d_defines:
|
1362
|
+
unknown_vars.append(var.name)
|
1363
|
+
if op_item.name not in d_defines and op_item.name not in unknown_vars:
|
1364
|
+
unknown_vars.append(op_item.name)
|
1365
|
+
if unknown_vars:
|
1366
|
+
raise SyntaxError(
|
1367
|
+
f'Unknown during aspect variable {", ".join(unknown_vars)} in transition:\n{during_aspect_item}')
|
1368
|
+
during_operations.append(Operation(var_name=op_item.name, expr=operation_val))
|
1369
|
+
on_during_aspects.append(OnAspect(
|
1370
|
+
stage='during',
|
1371
|
+
aspect=during_aspect_item.aspect,
|
1372
|
+
name=during_aspect_item.name,
|
1373
|
+
doc=None,
|
1374
|
+
operations=during_operations,
|
1375
|
+
))
|
1376
|
+
elif isinstance(during_aspect_item, dsl_nodes.DuringAspectAbstractFunction):
|
1377
|
+
on_during_aspects.append(OnAspect(
|
1378
|
+
stage='during',
|
1379
|
+
aspect=during_aspect_item.aspect,
|
1380
|
+
name=during_aspect_item.name,
|
1381
|
+
doc=during_aspect_item.doc,
|
1382
|
+
operations=[],
|
1383
|
+
))
|
1384
|
+
|
1385
|
+
for transition in current_state.transitions:
|
1386
|
+
transition.parent = current_state
|
1387
|
+
|
1388
|
+
_recursive_finish_states(dnode.root_state, current_state=root_state, current_path=())
|
1389
|
+
return StateMachine(
|
1390
|
+
defines=d_defines,
|
1391
|
+
root_state=root_state,
|
1392
|
+
)
|