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.
Files changed (46) hide show
  1. pyfcstm/__init__.py +0 -0
  2. pyfcstm/__main__.py +4 -0
  3. pyfcstm/config/__init__.py +0 -0
  4. pyfcstm/config/meta.py +20 -0
  5. pyfcstm/dsl/__init__.py +6 -0
  6. pyfcstm/dsl/error.py +226 -0
  7. pyfcstm/dsl/grammar/Grammar.g4 +190 -0
  8. pyfcstm/dsl/grammar/Grammar.interp +168 -0
  9. pyfcstm/dsl/grammar/Grammar.tokens +118 -0
  10. pyfcstm/dsl/grammar/GrammarLexer.interp +214 -0
  11. pyfcstm/dsl/grammar/GrammarLexer.py +523 -0
  12. pyfcstm/dsl/grammar/GrammarLexer.tokens +118 -0
  13. pyfcstm/dsl/grammar/GrammarListener.py +521 -0
  14. pyfcstm/dsl/grammar/GrammarParser.py +4373 -0
  15. pyfcstm/dsl/grammar/__init__.py +3 -0
  16. pyfcstm/dsl/listener.py +440 -0
  17. pyfcstm/dsl/node.py +1581 -0
  18. pyfcstm/dsl/parse.py +155 -0
  19. pyfcstm/entry/__init__.py +1 -0
  20. pyfcstm/entry/base.py +126 -0
  21. pyfcstm/entry/cli.py +12 -0
  22. pyfcstm/entry/dispatch.py +46 -0
  23. pyfcstm/entry/generate.py +83 -0
  24. pyfcstm/entry/plantuml.py +67 -0
  25. pyfcstm/model/__init__.py +3 -0
  26. pyfcstm/model/base.py +51 -0
  27. pyfcstm/model/expr.py +764 -0
  28. pyfcstm/model/model.py +1392 -0
  29. pyfcstm/render/__init__.py +3 -0
  30. pyfcstm/render/env.py +36 -0
  31. pyfcstm/render/expr.py +180 -0
  32. pyfcstm/render/func.py +77 -0
  33. pyfcstm/render/render.py +279 -0
  34. pyfcstm/utils/__init__.py +6 -0
  35. pyfcstm/utils/binary.py +38 -0
  36. pyfcstm/utils/doc.py +64 -0
  37. pyfcstm/utils/jinja2.py +121 -0
  38. pyfcstm/utils/json.py +125 -0
  39. pyfcstm/utils/text.py +91 -0
  40. pyfcstm/utils/validate.py +102 -0
  41. pyfcstm-0.0.1.dist-info/LICENSE +165 -0
  42. pyfcstm-0.0.1.dist-info/METADATA +205 -0
  43. pyfcstm-0.0.1.dist-info/RECORD +46 -0
  44. pyfcstm-0.0.1.dist-info/WHEEL +5 -0
  45. pyfcstm-0.0.1.dist-info/entry_points.txt +2 -0
  46. 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
+ )