statable 3.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. codegen/__init__.py +38 -0
  2. codegen/c_code_generator.py +1752 -0
  3. codegen/code_merger.py +371 -0
  4. codegen/code_templates.py +509 -0
  5. codegen/config.py +167 -0
  6. codegen/enum_generator.py +377 -0
  7. codegen/event_queue_generator.py +303 -0
  8. codegen/interrupt_generator.py +345 -0
  9. codegen/naming_convention.py +171 -0
  10. codegen/osal_generator.py +282 -0
  11. codegen/role_function_generator.py +1244 -0
  12. codegen/sample_data.py +269 -0
  13. codegen/state_actions_generator.py +395 -0
  14. codegen/struct_generator.py +721 -0
  15. codegen/timer_generator.py +299 -0
  16. codegen/transition_generator.py +907 -0
  17. codegen/type_mapper.py +69 -0
  18. codegen/validate/__init__.py +38 -0
  19. codegen/validate/change_actions.py +90 -0
  20. codegen/validate/change_applier.py +416 -0
  21. codegen/validate/clipboard_manager.py +47 -0
  22. codegen/validate/data/__init__.py +30 -0
  23. codegen/validate/data/action_definitions.py +200 -0
  24. codegen/validate/data/keywords.py +46 -0
  25. codegen/validate/data/prompt_templates.py +176 -0
  26. codegen/validate/data/validation_rules.py +205 -0
  27. codegen/validate/items/__init__.py +30 -0
  28. codegen/validate/items/base_validator.py +25 -0
  29. codegen/validate/items/cell_validator.py +275 -0
  30. codegen/validate/items/custom_type_validator.py +75 -0
  31. codegen/validate/items/event_validator.py +78 -0
  32. codegen/validate/items/flag_validator.py +74 -0
  33. codegen/validate/items/interrupt_validator.py +75 -0
  34. codegen/validate/items/queue_validator.py +73 -0
  35. codegen/validate/items/role_function_validator.py +175 -0
  36. codegen/validate/items/state_validator.py +88 -0
  37. codegen/validate/items/timer_validator.py +120 -0
  38. codegen/validate/items/transition_validator.py +113 -0
  39. codegen/validate/items/variable_validator.py +132 -0
  40. codegen/validate/logger.py +52 -0
  41. codegen/validate/models.py +177 -0
  42. codegen/validate/prompt_generator.py +724 -0
  43. codegen/validate/response_parser.py +154 -0
  44. codegen/validate/response_validator.py +275 -0
  45. codegen/validate/validation_dialog.py +322 -0
  46. codegen/validate/validator.py +94 -0
  47. codegen/variable_generator.py +419 -0
  48. statable/__init__.py +71 -0
  49. statable/__main__.py +49 -0
  50. statable/cli.py +251 -0
  51. statable/global_defs.py +256 -0
  52. statable/mermaid_gen.py +144 -0
  53. statable/model.py +426 -0
  54. statable/parser.py +64 -0
  55. statable/sample_data.py +435 -0
  56. statable/shared/__init__.py +24 -0
  57. statable/shared/condition_library.py +63 -0
  58. statable/shared/literal_library.py +66 -0
  59. statable/shared/role_function_library.py +150 -0
  60. statable/state_machine.py +121 -0
  61. statable/xml_io.py +1164 -0
  62. statable-3.1.0.dist-info/METADATA +151 -0
  63. statable-3.1.0.dist-info/RECORD +117 -0
  64. statable-3.1.0.dist-info/WHEEL +5 -0
  65. statable-3.1.0.dist-info/entry_points.txt +2 -0
  66. statable-3.1.0.dist-info/top_level.txt +3 -0
  67. statable_gui/__init__.py +23 -0
  68. statable_gui/action_edit_dialog.py +214 -0
  69. statable_gui/code_generation_dialog.py +486 -0
  70. statable_gui/code_generation_settings_dialog.py +522 -0
  71. statable_gui/common_widgets.py +537 -0
  72. statable_gui/condition_builder_dialog.py +500 -0
  73. statable_gui/condition_edit_dialog.py +123 -0
  74. statable_gui/condition_template_dialog.py +58 -0
  75. statable_gui/config.py +21 -0
  76. statable_gui/dialogs.py +439 -0
  77. statable_gui/event_definition_dialog.py +605 -0
  78. statable_gui/event_delivery_settings_dialog.py +191 -0
  79. statable_gui/event_queue_dialog.py +245 -0
  80. statable_gui/global_defs.py +167 -0
  81. statable_gui/global_defs_dialog.py +796 -0
  82. statable_gui/i18n/__init__.py +118 -0
  83. statable_gui/interrupt_handler_edit_dialog.py +810 -0
  84. statable_gui/layer_settings_dialog.py +180 -0
  85. statable_gui/libcntrl/__init__.py +23 -0
  86. statable_gui/libcntrl/condition_library.py +13 -0
  87. statable_gui/libcntrl/literal_library.py +13 -0
  88. statable_gui/libcntrl/literal_management_dialog.py +185 -0
  89. statable_gui/libcntrl/role_function_edit_dialog.py +145 -0
  90. statable_gui/libcntrl/role_function_library.py +13 -0
  91. statable_gui/literal_definition_dialog.py +62 -0
  92. statable_gui/logger.py +83 -0
  93. statable_gui/main_window.py +1380 -0
  94. statable_gui/matrix_table.py +413 -0
  95. statable_gui/preference_keys.py +18 -0
  96. statable_gui/preferences.py +74 -0
  97. statable_gui/role_function_dialog.py +244 -0
  98. statable_gui/sample_data.py +11 -0
  99. statable_gui/state_actions_dialog.py +398 -0
  100. statable_gui/symbol_picker.py +170 -0
  101. statable_gui/traceball.py +37 -0
  102. statable_gui/transition_editor_direct/__init__.py +15 -0
  103. statable_gui/transition_editor_direct/actions_tab.py +856 -0
  104. statable_gui/transition_editor_direct/code_widget.py +344 -0
  105. statable_gui/transition_editor_direct/coverage_analyzer.py +163 -0
  106. statable_gui/transition_editor_direct/dialog.py +335 -0
  107. statable_gui/transition_editor_direct/draft.py +270 -0
  108. statable_gui/transition_editor_direct/edit_dialogs.py +160 -0
  109. statable_gui/transition_editor_direct/overview_tab.py +173 -0
  110. statable_gui/transition_editor_direct/palette_widget.py +233 -0
  111. statable_gui/transition_editor_direct/relations_edit_dialog.py +156 -0
  112. statable_gui/transition_editor_direct/relations_tab.py +230 -0
  113. statable_gui/transition_editor_direct/system_global_dialog.py +52 -0
  114. statable_gui/transition_editor_direct/transition_actions_dialog.py +109 -0
  115. statable_gui/transition_editor_direct/transitions_tab.py +620 -0
  116. statable_gui/validation_dialog.py +285 -0
  117. statable_gui/widgets.py +1315 -0
@@ -0,0 +1,907 @@
1
+ # codegen/transition_generator.py
2
+ """
3
+ State transition function generation module (v2.6 multi-transition + nesting).
4
+
5
+ Version: 2.6 (2026-09-20 / MISRA 17.3 final fix)
6
+ - Fix (MISRA C:2012 Rule 17.3): the LOG_ERROR call in the generated
7
+ StateMachine_GetNextEvent_<Layer> body was reported as an implicit
8
+ function declaration by cppcheck even after statable_types_common.h
9
+ (which defines LOG_ERROR as a no-op variadic macro) was included in
10
+ the .c file. cppcheck apparently does not expand the variadic macro
11
+ and treats the call site as an undeclared function.
12
+ The call has therefore been removed and replaced with a comment;
13
+ consecutive_count is still used by the surrounding code so no
14
+ unused-variable warning is introduced.
15
+
16
+ Version: 2.5 (2026-09-20 / MISRA fixes + _handled emission fix)
17
+ Version: 2.4 (2026-09-20 / condition-aware role function calls)
18
+ Version: 2.2.1 (2026-09-19)
19
+ """
20
+
21
+ import sys
22
+ import os
23
+ import logging
24
+ from string import Template
25
+ from typing import Dict, List, Optional
26
+
27
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
28
+
29
+ from statable.model import State, Event, Transition
30
+ from statable.state_machine import StateMachine
31
+
32
+ try:
33
+ from .type_mapper import CTypeMapper
34
+ from .naming_convention import CNamingConvention
35
+ from .code_templates import CodeTemplates
36
+ except ImportError:
37
+ from type_mapper import CTypeMapper
38
+ from naming_convention import CNamingConvention
39
+ from code_templates import CodeTemplates
40
+
41
+ logger = logging.getLogger(__name__)
42
+
43
+
44
+ def ensure_list(value) -> List[str]:
45
+ """Normalize str/None/list to List[str]."""
46
+ if value is None:
47
+ return []
48
+ if isinstance(value, str):
49
+ s = value.strip()
50
+ return [s] if s else []
51
+ if isinstance(value, list):
52
+ return [str(x) for x in value if str(x).strip()]
53
+ return [str(value)]
54
+
55
+
56
+ class TransitionGenerator:
57
+ """State transition function generator (v2.6 multi-transition support)"""
58
+
59
+ SHORT_CELL_NAMES = True
60
+ TABLE_MIN_COL_WIDTH = 14
61
+ DEFAULT_TABLE_TYPE = 'array'
62
+ DEFAULT_GENERATION_STYLE = 'table_driven'
63
+
64
+ CELL_TEMPLATES = {
65
+ 'header': Template(
66
+ '/**\n'
67
+ ' * @brief Cell transition: $state_enum -[$event_enum]-> (multi)\n'
68
+ ' * @note Transition count: $transition_count\n'
69
+ ' */\n'
70
+ ),
71
+ 'signature': Template(
72
+ 'static $state_type $func_name(\n'
73
+ ' const $context_type *transition,\n'
74
+ ' SystemContext_t *ctx\n'
75
+ ')\n'
76
+ '{\n'
77
+ ),
78
+ 'body_open': Template(
79
+ ' $state_type next_state = transition->from_state;\n'
80
+ ' bool _handled = false;\n'
81
+ ),
82
+ 'body_open_simple': Template(
83
+ ' $state_type next_state = transition->from_state;\n'
84
+ ),
85
+ 'cell_actions_header': Template(
86
+ '\n'
87
+ ' /* ===== Cell actions ($trigger) ===== */\n'
88
+ ),
89
+ 'cell_action_call': Template(' $call;\n'),
90
+ 'body_close': (
91
+ '\n'
92
+ ' return next_state;\n'
93
+ '}\n'
94
+ ),
95
+ }
96
+
97
+ CELL_PROTO_TEMPLATES = {
98
+ 'section_comment': '/* ===== Cell transition function forward declarations ===== */\n',
99
+ 'prototype': Template(
100
+ 'static $state_type $func_name(\n'
101
+ ' const $context_type *transition,\n'
102
+ ' SystemContext_t *ctx);\n'
103
+ ),
104
+ }
105
+
106
+ TABLE_TEMPLATES = {
107
+ 'func_ptr_comment': '/* Transition function pointer type */\n',
108
+ 'func_ptr_typedef': Template(
109
+ 'typedef $state_type (*$func_type)(\n'
110
+ ' const $context_type *, SystemContext_t *);\n'
111
+ ),
112
+ 'table_comment': Template(
113
+ '\n'
114
+ '/* ============================================================== */\n'
115
+ '/* State transition table: rows=$row_desc, cols=$col_desc */\n'
116
+ '/* Global variable (externally accessible) */\n'
117
+ '/* ============================================================== */\n'
118
+ ),
119
+ 'table_open': Template(
120
+ 'const $func_type $table_name\n'
121
+ ' [$state_max][$event_max] = {\n'
122
+ ),
123
+ 'table_close': '};\n',
124
+ }
125
+
126
+ TABLE_HEADER_TEMPLATES = {
127
+ 'header_comment': Template(
128
+ '/**\n'
129
+ ' * @brief Transition table ($layer layer)\n'
130
+ ' * @note rows=$row_desc, cols=$col_desc\n'
131
+ ' */\n'
132
+ ),
133
+ 'func_ptr_typedef': Template(
134
+ 'typedef $state_type (*$func_type)(\n'
135
+ ' const $context_type *, SystemContext_t *);\n'
136
+ ),
137
+ 'extern_decl': Template(
138
+ 'extern const $func_type $table_name\n'
139
+ ' [$state_max][$event_max];\n'
140
+ ),
141
+ }
142
+
143
+ DICT_TEMPLATES = {
144
+ 'struct_comment': (
145
+ '/* Transition function dictionary entry */\n'
146
+ '/* Used for debugging, logging, and dynamic dispatch */\n'
147
+ ),
148
+ 'struct_open': (
149
+ 'typedef struct {\n'
150
+ ' const char *name;\n'
151
+ ),
152
+ 'struct_field_from': Template(' $state_type from_state;\n'),
153
+ 'struct_field_event': Template(' $event_type event;\n'),
154
+ 'struct_field_target': Template(' $state_type default_target;\n'),
155
+ 'struct_field_condition': ' const char *condition;\n',
156
+ 'struct_field_func': Template(' $func_type func;\n'),
157
+ 'struct_close': Template('}} $dict_type;\n'),
158
+ 'dict_comment': '\n/* Transition function dictionary */\n',
159
+ 'dict_open': Template('static const $dict_type $dict_name[] = {\n'),
160
+ 'dict_entry': Template(
161
+ ' { "$trans_name",\n'
162
+ ' $state_enum, $event_enum, $target_enum,\n'
163
+ ' "$condition", $func_name },\n'
164
+ ),
165
+ 'dict_close': '};\n',
166
+ 'dict_size': Template(
167
+ '\n'
168
+ '#define $size_macro \\\n'
169
+ ' (sizeof($dict_name) / sizeof($dict_name[0]))\n'
170
+ ),
171
+ }
172
+
173
+ PROCESS_TEMPLATES = {
174
+ 'comment': Template(
175
+ '/**\n'
176
+ ' * @brief $layer layer state transition processing\n'
177
+ ' * @param current_state Current state\n'
178
+ ' * @param event Event that occurred\n'
179
+ ' * @param ctx System context pointer\n'
180
+ ' * @return State after transition\n'
181
+ ' */\n'
182
+ ),
183
+ 'signature': Template(
184
+ '$state_type $func_name(\n'
185
+ ' $state_type current_state,\n'
186
+ ' $event_type event,\n'
187
+ ' SystemContext_t *ctx\n'
188
+ ')\n'
189
+ ),
190
+ 'body': Template(
191
+ '{\n'
192
+ ' $context_type transition = {\n'
193
+ ' .from_state = current_state,\n'
194
+ ' .event = event,\n'
195
+ ' };\n'
196
+ ' $func_type func = $table_name[current_state][event];\n'
197
+ ' $state_type next_state = current_state;\n'
198
+ ' if (func != NULL) {\n'
199
+ ' next_state = func(&transition, ctx);\n'
200
+ ' }\n'
201
+ ' /* [v2.7.0] State actions: Exit(old) / Entry(new) */\n'
202
+ ' if (next_state != current_state) {\n'
203
+ ' ${layer}_Exit(current_state, ctx);\n'
204
+ ' ${layer}_Entry(next_state, ctx);\n'
205
+ ' }\n'
206
+ ' return next_state;\n'
207
+ '}\n'
208
+ ),
209
+ }
210
+
211
+ # [v2.6 / MISRA 17.3] LOG_ERROR removed from the generated body.
212
+ # cppcheck does not expand the variadic LOG_ERROR macro from
213
+ # statable_types_common.h and reports it as an implicit function.
214
+ GET_NEXT_TEMPLATES = {
215
+ 'comment': Template(
216
+ '/**\n'
217
+ ' * @brief Get next event for $layer layer\n'
218
+ ' * @param ctx System context pointer\n'
219
+ ' * @return Next event ($event_none if no pending event)\n'
220
+ ' */\n'
221
+ ),
222
+ 'signature': Template('$event_type $func_name(SystemContext_t *ctx)\n'),
223
+ 'body': Template(
224
+ '{\n'
225
+ ' static uint8_t consecutive_count = 0;\n'
226
+ ' $event_type evt = $event_none;\n'
227
+ '\n'
228
+ ' /* [F-3 S2] Read-then-clear own layer\'s pending slot. */\n'
229
+ ' STATABLE_ENTER_CRITICAL();\n'
230
+ ' if (ctx->pending_event_valid_$layer) {\n'
231
+ ' evt = ($event_type)ctx->pending_event_$layer;\n'
232
+ ' ctx->pending_event_valid_$layer = false;\n'
233
+ ' }\n'
234
+ ' STATABLE_EXIT_CRITICAL();\n'
235
+ '\n'
236
+ ' if (evt != $event_none) {\n'
237
+ ' consecutive_count++;\n'
238
+ ' if (consecutive_count > MAX_CONSECUTIVE_PENDING_EVENTS) {\n'
239
+ ' /* Pending event chain too long: force reset */\n'
240
+ ' consecutive_count = 0;\n'
241
+ ' return $event_none;\n'
242
+ ' }\n'
243
+ ' return evt;\n'
244
+ ' }\n'
245
+ '\n'
246
+ ' /* [C-52/C-54] Drain this layer\'s queue (protected). */\n'
247
+ ' STATABLE_ENTER_CRITICAL();\n'
248
+ ' if (ctx->queue_$layer.count > 0U) {\n'
249
+ ' evt = ($event_type)ctx->queue_$layer.buffer[ctx->queue_$layer.head];\n'
250
+ ' ctx->queue_$layer.head = (uint16_t)((ctx->queue_$layer.head + 1U) % STATABLE_LAYER_QUEUE_SIZE);\n'
251
+ ' ctx->queue_$layer.count--;\n'
252
+ ' STATABLE_EXIT_CRITICAL();\n'
253
+ ' consecutive_count = 0;\n'
254
+ ' return evt;\n'
255
+ ' }\n'
256
+ ' STATABLE_EXIT_CRITICAL();\n'
257
+ '\n'
258
+ ' consecutive_count = 0;\n'
259
+ ' return $event_none;\n'
260
+ '}\n'
261
+ ),
262
+ }
263
+
264
+ def __init__(self):
265
+ self.mapper = CTypeMapper()
266
+ self.naming = CNamingConvention()
267
+ self.templates = CodeTemplates()
268
+ self.strings = self.templates.STRINGS
269
+ self.formats = self.templates.FORMATS
270
+ self.layer_name: str = ""
271
+ self._current_state_machine: Optional[StateMachine] = None
272
+
273
+ self.table_generators: Dict[str, callable] = {
274
+ 'array': self._generate_table_array,
275
+ 'switch': self._generate_table_switch,
276
+ 'dictionary': self._generate_table_dictionary,
277
+ }
278
+ self.process_generators: Dict[str, callable] = {
279
+ 'table_driven': self._generate_process_table_driven,
280
+ 'switch_case': self._generate_process_switch_case,
281
+ }
282
+
283
+ def set_layer(self, layer_name: str):
284
+ self.layer_name = layer_name
285
+
286
+ def _log_debug(self, message, level='debug'):
287
+ log_func = getattr(logger, level, logger.debug)
288
+ log_func(message)
289
+
290
+ def _state_enum(self, state_name: str) -> str:
291
+ s = self.naming.to_pascal_case(state_name) if state_name else "Unknown"
292
+ return f"STATE_{self.layer_name}_{s}" if self.layer_name else f"STATE_{s}"
293
+
294
+ def _event_enum(self, event_name: str) -> str:
295
+ e = self.naming.to_upper_snake(event_name) if event_name else "NONE"
296
+ return f"EVENT_{self.layer_name}_{e}" if self.layer_name else f"EVENT_{e}"
297
+
298
+ def _state_type(self) -> str:
299
+ return f"STATE_{self.layer_name}_t" if self.layer_name else "STATE_t"
300
+
301
+ def _event_type(self) -> str:
302
+ return f"EVENT_{self.layer_name}_t" if self.layer_name else "EVENT_t"
303
+
304
+ def _context_type(self) -> str:
305
+ return f"TransitionContext_{self.layer_name}_t" if self.layer_name else "TransitionContext_t"
306
+
307
+ def _func_type(self) -> str:
308
+ return f"TransitionFunc_{self.layer_name}_t" if self.layer_name else "TransitionFunc_t"
309
+
310
+ def _table_name(self) -> str:
311
+ return f"transition_table_{self.layer_name}" if self.layer_name else "transition_matrix"
312
+
313
+ def _state_max(self) -> str:
314
+ return f"STATE_{self.layer_name}_MAX" if self.layer_name else "STATE_MAX"
315
+
316
+ def _event_max(self) -> str:
317
+ return f"EVENT_{self.layer_name}_MAX" if self.layer_name else "EVENT_MAX"
318
+
319
+ def _cell_func_name(self, state_name: str, event_name: str) -> str:
320
+ s = self.naming.to_pascal_case(state_name) if state_name else "Unknown"
321
+ e = self.naming.to_upper_snake(event_name) if event_name else "NONE"
322
+ if self.SHORT_CELL_NAMES:
323
+ return f"t_{s}_{e}"
324
+ return f"transition_{self.layer_name}_{s}_{e}" if self.layer_name \
325
+ else f"transition_{s}_{e}"
326
+
327
+ # ==================================================================
328
+ # Role function call generation (context-aware)
329
+ # ==================================================================
330
+ def _resolve_role_func_name(self, func_name: str) -> str:
331
+ if not func_name or not func_name.strip():
332
+ return ""
333
+ if func_name.startswith("RoleFunc_"):
334
+ return func_name
335
+ if "." in func_name:
336
+ ns, name = func_name.split(".", 1)
337
+ ns_pascal = self.naming.to_pascal_case(ns)
338
+ name_pascal = self.naming.to_pascal_case(name)
339
+ return f"RoleFunc_{ns_pascal}_{name_pascal}"
340
+ pascal = self.naming.to_pascal_case(func_name)
341
+ if self.layer_name:
342
+ return f"RoleFunc_{self.layer_name}_{pascal}"
343
+ return f"RoleFunc_{pascal}"
344
+
345
+ def _role_func_call_expr(self, func_name: str) -> str:
346
+ full = self._resolve_role_func_name(func_name)
347
+ if not full:
348
+ return "/* empty role function reference */"
349
+ return f"{full}(transition, ctx)"
350
+
351
+ def _role_func_call_action(self, func_name: str) -> str:
352
+ full = self._resolve_role_func_name(func_name)
353
+ if not full:
354
+ return "/* empty role function reference */"
355
+ return f"(void){full}(transition, ctx)"
356
+
357
+ def _role_func_call_bare(self, func_name: str) -> str:
358
+ return self._resolve_role_func_name(func_name)
359
+
360
+ def _extract_rf_name(self, action) -> str:
361
+ """[v2.7.0] Extract role function name from ActionStep or str.
362
+
363
+ state.entry / state.exit are List[ActionStep] since v2.7.0,
364
+ but may still hold plain strings from legacy data.
365
+ """
366
+ if action is None:
367
+ return ""
368
+ if isinstance(action, str):
369
+ return action
370
+ return getattr(action, "role_function", "") or ""
371
+
372
+ def _role_func_call(self, func_name: str) -> str:
373
+ return self._role_func_call_action(func_name)
374
+
375
+ # ==================================================================
376
+ # State entry / exit call generation
377
+ # ==================================================================
378
+ def _build_exit_call(self, state_name: str) -> str:
379
+ if not state_name or self._current_state_machine is None:
380
+ return ''
381
+ state = self._current_state_machine.states.get(state_name)
382
+ if state is None:
383
+ return ''
384
+ # [v2.7.0] state.exit is List[ActionStep]; extract role_function
385
+ exit_list = getattr(state, 'exit', []) or []
386
+ if not exit_list:
387
+ return ''
388
+ lines = []
389
+ for action in exit_list:
390
+ fn = self._extract_rf_name(action)
391
+ if not fn:
392
+ continue
393
+ call = self._role_func_call_action(fn)
394
+ if call.startswith("/*"):
395
+ continue
396
+ lines.append(f' {call}; /* state exit */\n')
397
+ return ''.join(lines)
398
+
399
+ def _build_entry_call(self, state_name: str) -> str:
400
+ if not state_name or self._current_state_machine is None:
401
+ return ''
402
+ state = self._current_state_machine.states.get(state_name)
403
+ if state is None:
404
+ return ''
405
+ # [v2.7.0] state.entry is List[ActionStep]; extract role_function
406
+ entry_list = getattr(state, 'entry', []) or []
407
+ if not entry_list:
408
+ return ''
409
+ lines = []
410
+ for action in entry_list:
411
+ fn = self._extract_rf_name(action)
412
+ if not fn:
413
+ continue
414
+ call = self._role_func_call_action(fn)
415
+ if call.startswith("/*"):
416
+ continue
417
+ lines.append(f' {call}; /* state entry */\n')
418
+ return ''.join(lines)
419
+
420
+ # ==================================================================
421
+ # Transition block generation
422
+ # ==================================================================
423
+ def _build_transition_block(self, trans, idx: int,
424
+ use_handled: bool = True) -> str:
425
+ condition = getattr(trans, 'condition', '')
426
+ early_return = getattr(trans, 'early_return', False)
427
+ target = getattr(trans, 'target', '')
428
+ else_target = getattr(trans, 'else_target', '')
429
+ pre_actions = ensure_list(getattr(trans, 'pre_actions', []))
430
+ else_actions = ensure_list(getattr(trans, 'else_actions', []))
431
+ has_else = getattr(trans, 'has_else', True)
432
+ label = getattr(trans, 'label', '') or f"T{idx + 1}"
433
+
434
+ has_else_body = has_else and bool(else_target or else_actions)
435
+ cond_expr = condition if condition else "1"
436
+ commit_str = "Commit" if early_return else "Tentative"
437
+
438
+ emit_handled = early_return and use_handled
439
+
440
+ exit_calls = self._build_exit_call(trans.source)
441
+ entry_calls = self._build_entry_call(target)
442
+ else_entry_calls = self._build_entry_call(else_target)
443
+
444
+ pre_lines = ''.join(
445
+ f' {self._role_func_call_action(a)};\n'
446
+ for a in pre_actions
447
+ )
448
+ else_lines = ''.join(
449
+ f' {self._role_func_call_action(a)};\n'
450
+ for a in else_actions
451
+ )
452
+
453
+ target_line = (
454
+ f' next_state = {self._state_enum(target)};\n'
455
+ if target else ''
456
+ )
457
+ else_target_line = (
458
+ f' next_state = {self._state_enum(else_target)};\n'
459
+ if else_target else ' /* else target not set */\n'
460
+ )
461
+
462
+ lines = []
463
+ lines.append(f'\n /* ===== Transition[{label}] ({commit_str}) ===== */\n')
464
+
465
+ if emit_handled:
466
+ lines.append(f' if ((!_handled) && ({cond_expr})) {{\n')
467
+ else:
468
+ lines.append(f' if ({cond_expr}) {{\n')
469
+
470
+ lines.append(exit_calls)
471
+ lines.append(pre_lines)
472
+ lines.append(target_line)
473
+ lines.append(entry_calls)
474
+ if emit_handled:
475
+ lines.append(' _handled = true;\n')
476
+
477
+ if has_else_body:
478
+ if emit_handled:
479
+ lines.append(
480
+ f' }} else if ((!_handled) && (!({cond_expr}))) {{\n'
481
+ )
482
+ else:
483
+ lines.append(' } else {\n')
484
+
485
+ lines.append(exit_calls)
486
+ lines.append(else_lines)
487
+ lines.append(else_target_line)
488
+ lines.append(else_entry_calls)
489
+ if emit_handled:
490
+ lines.append(' _handled = true;\n')
491
+
492
+ lines.append(' }\n')
493
+ return ''.join(lines)
494
+
495
+ def _build_transitions_block(self, state, transitions, relations,
496
+ use_handled: bool = True) -> str:
497
+ parts = []
498
+
499
+ by_label = {}
500
+ for t in transitions:
501
+ lbl = getattr(t, 'label', '') or ''
502
+ if lbl:
503
+ by_label[lbl] = t
504
+
505
+ mentioned = set()
506
+
507
+ def collect(r):
508
+ for m in (getattr(r, 'members', None) or []):
509
+ mentioned.add(m)
510
+ for c in (getattr(r, 'children', None) or []):
511
+ collect(c)
512
+
513
+ for r in relations:
514
+ collect(r)
515
+
516
+ for r in relations:
517
+ parts.append(self._emit_relation(
518
+ r, by_label, indent_level=1, use_handled=use_handled,
519
+ ))
520
+
521
+ for t in transitions:
522
+ lbl = getattr(t, 'label', '') or ''
523
+ if lbl and lbl in mentioned:
524
+ continue
525
+ parts.append(self._build_transition_block(
526
+ t, 0, use_handled=use_handled,
527
+ ))
528
+
529
+ return ''.join(parts)
530
+
531
+ def _collect_child_labels(self, rel) -> set:
532
+ labels = set()
533
+ for child in (getattr(rel, 'children', None) or []):
534
+ labels.update(getattr(child, 'members', None) or [])
535
+ labels.update(self._collect_child_labels(child))
536
+ return labels
537
+
538
+ def _emit_relation(self, rel, by_label, indent_level: int = 1,
539
+ use_handled: bool = True) -> str:
540
+ parts = []
541
+ pad = ' ' * indent_level
542
+
543
+ shared_cond = (getattr(rel, 'shared_condition', '') or '').strip()
544
+ has_cond = bool(shared_cond)
545
+
546
+ if has_cond:
547
+ parts.append(
548
+ f'\n{pad}/* ===== Group (shared_condition) ===== */\n')
549
+ parts.append(f'{pad}if ({shared_cond}) {{\n')
550
+ inner_indent = indent_level + 1
551
+ else:
552
+ inner_indent = indent_level
553
+
554
+ child_labels = self._collect_child_labels(rel)
555
+
556
+ for label in (getattr(rel, 'members', None) or []):
557
+ if label in child_labels:
558
+ continue
559
+ t = by_label.get(label)
560
+ if t is None:
561
+ continue
562
+ block = self._build_transition_block(
563
+ t, 0, use_handled=use_handled,
564
+ )
565
+ extra = inner_indent - 1
566
+ if extra > 0:
567
+ block = self._indent_block(block, extra_indent=extra)
568
+ parts.append(block)
569
+
570
+ for child in (getattr(rel, 'children', None) or []):
571
+ parts.append(self._emit_relation(
572
+ child, by_label,
573
+ indent_level=inner_indent,
574
+ use_handled=use_handled,
575
+ ))
576
+
577
+ if has_cond:
578
+ parts.append(f'{pad}}}\n')
579
+
580
+ return ''.join(parts)
581
+
582
+ def _indent_block(self, block: str, extra_indent: int = 1) -> str:
583
+ pad = ' ' * extra_indent
584
+ lines = block.split('\n')
585
+ return '\n'.join(
586
+ pad + line if line.strip() else line for line in lines
587
+ )
588
+
589
+ def generate_transition_cell_functions(self, state_machine: StateMachine) -> str:
590
+ self._current_state_machine = state_machine
591
+
592
+ cell_blocks = []
593
+ for state in state_machine.states.values():
594
+ for event in state_machine.events.values():
595
+ transitions = state_machine.get_transitions_for_cell(
596
+ state.name, event.name)
597
+ if not transitions:
598
+ continue
599
+ cell_blocks.append(
600
+ self._build_cell_function(state, event, transitions)
601
+ )
602
+
603
+ self._current_state_machine = None
604
+ return '\n'.join(cell_blocks)
605
+
606
+ def _build_cell_function(self, state, event, transitions) -> str:
607
+ T = self.CELL_TEMPLATES
608
+
609
+ sm = self._current_state_machine
610
+ cell_actions = sm.get_actions_for_cell(state.name, event.name) if sm else []
611
+ cell_relations = sm.get_relations_for_cell(state.name, event.name) if sm else []
612
+
613
+ early_return_count = sum(
614
+ 1 for t in transitions if getattr(t, 'early_return', False)
615
+ )
616
+ needs_handled = early_return_count >= 2
617
+
618
+ parts = []
619
+
620
+ parts.append(T['header'].substitute(
621
+ state_enum=self._state_enum(state.name),
622
+ event_enum=self._event_enum(event.name),
623
+ transition_count=len(transitions),
624
+ ))
625
+
626
+ parts.append(T['signature'].substitute(
627
+ state_type=self._state_type(),
628
+ func_name=self._cell_func_name(state.name, event.name),
629
+ context_type=self._context_type(),
630
+ ))
631
+
632
+ if needs_handled:
633
+ parts.append(T['body_open'].substitute(state_type=self._state_type()))
634
+ else:
635
+ parts.append(T['body_open_simple'].substitute(
636
+ state_type=self._state_type()))
637
+
638
+ pre_actions_for_cell = [
639
+ a for a in cell_actions
640
+ if a.trigger in ("before_transitions", "always")
641
+ ]
642
+ if pre_actions_for_cell:
643
+ parts.append(T['cell_actions_header'].substitute(
644
+ trigger="before_transitions"))
645
+ for a in pre_actions_for_cell:
646
+ parts.append(T['cell_action_call'].substitute(
647
+ call=self._role_func_call_action(a.role_function)
648
+ ))
649
+
650
+ parts.append(self._build_transitions_block(
651
+ state, transitions, cell_relations,
652
+ use_handled=needs_handled,
653
+ ))
654
+
655
+ after_actions = [a for a in cell_actions
656
+ if a.trigger == "after_transitions"]
657
+ if after_actions:
658
+ parts.append(T['cell_actions_header'].substitute(
659
+ trigger="after_transitions"))
660
+ for a in after_actions:
661
+ parts.append(T['cell_action_call'].substitute(
662
+ call=self._role_func_call_action(a.role_function)
663
+ ))
664
+
665
+ parts.append(T['body_close'])
666
+ return ''.join(parts)
667
+
668
+ def generate_transition_cell_prototypes(self, state_machine: StateMachine) -> str:
669
+ T = self.CELL_PROTO_TEMPLATES
670
+ parts = [T['section_comment']]
671
+ for state in state_machine.states.values():
672
+ for event in state_machine.events.values():
673
+ transitions = state_machine.get_transitions_for_cell(
674
+ state.name, event.name)
675
+ if not transitions:
676
+ continue
677
+ parts.append(T['prototype'].substitute(
678
+ state_type=self._state_type(),
679
+ func_name=self._cell_func_name(state.name, event.name),
680
+ context_type=self._context_type(),
681
+ ))
682
+ return ''.join(parts)
683
+
684
+ def generate_transition_table(self, state_machine: StateMachine,
685
+ table_type: str = None) -> str:
686
+ if table_type is None:
687
+ table_type = self.DEFAULT_TABLE_TYPE
688
+ generator = self.table_generators.get(table_type)
689
+ if generator is None or table_type != 'array':
690
+ generator = self.table_generators['array']
691
+ return generator(state_machine)
692
+
693
+ def _generate_table_array(self, state_machine: StateMachine) -> str:
694
+ T = self.TABLE_TEMPLATES
695
+ parts = []
696
+ states = list(state_machine.states.values())
697
+ events = list(state_machine.events.values())
698
+ func_type = self._func_type()
699
+ table_name = self._table_name()
700
+
701
+ cell_values = []
702
+ for state in states:
703
+ row = []
704
+ for event in events:
705
+ transitions = state_machine.get_transitions_for_cell(
706
+ state.name, event.name)
707
+ row.append(self._cell_func_name(state.name, event.name)
708
+ if transitions else "NULL")
709
+ cell_values.append(row)
710
+
711
+ state_col_width = max((len(s.name) for s in states), default=5)
712
+ event_headers = []
713
+ col_widths = []
714
+ for event_idx, event in enumerate(events):
715
+ event_disp = event.name if event.name else "NONE"
716
+ event_headers.append(event_disp)
717
+ w = max(len(event_disp), self.TABLE_MIN_COL_WIDTH)
718
+ for state_idx in range(len(states)):
719
+ w = max(w, len(cell_values[state_idx][event_idx]))
720
+ col_widths.append(w)
721
+
722
+ parts.append(T['func_ptr_comment'])
723
+ parts.append(T['func_ptr_typedef'].substitute(
724
+ state_type=self._state_type(),
725
+ func_type=func_type,
726
+ context_type=self._context_type(),
727
+ ))
728
+ parts.append(T['table_comment'].substitute(
729
+ row_desc="state", col_desc="event"))
730
+ parts.append(T['table_open'].substitute(
731
+ func_type=func_type, table_name=table_name,
732
+ state_max=self._state_max(), event_max=self._event_max(),
733
+ ))
734
+
735
+ header_state_pad = " " * (state_col_width + 2)
736
+ header_cells = [event_headers[i].ljust(col_widths[i])
737
+ for i in range(len(events))]
738
+ parts.append(
739
+ " /*" + " " + header_state_pad + " | " +
740
+ " | ".join(header_cells) + " */\n"
741
+ )
742
+ sep = " /* " + "-" * (state_col_width + 2) + "+"
743
+ for w in col_widths:
744
+ sep += "-" * w + "+"
745
+ sep = sep[:-1] + "*/\n"
746
+ parts.append(sep)
747
+
748
+ for state_idx, state in enumerate(states):
749
+ state_padded = state.name.ljust(state_col_width + 2)
750
+ cells = [cell_values[state_idx][event_idx].ljust(col_widths[event_idx])
751
+ for event_idx in range(len(events))]
752
+ row = f" /* {state_padded}*/ {{ "
753
+ row += ", ".join(cells)
754
+ row += " },\n"
755
+ parts.append(row)
756
+
757
+ parts.append(T['table_close'])
758
+ return ''.join(parts)
759
+
760
+ def _generate_table_switch(self, sm): return self._generate_table_array(sm)
761
+ def _generate_table_dictionary(self, sm): return self._generate_table_array(sm)
762
+
763
+ def generate_transition_table_header(self, state_machine: StateMachine) -> str:
764
+ T = self.TABLE_HEADER_TEMPLATES
765
+ parts = []
766
+ parts.append(T['header_comment'].substitute(
767
+ layer=self.layer_name or "system",
768
+ row_desc="state", col_desc="event",
769
+ ))
770
+ parts.append(T['func_ptr_typedef'].substitute(
771
+ state_type=self._state_type(),
772
+ func_type=self._func_type(),
773
+ context_type=self._context_type(),
774
+ ))
775
+ parts.append("")
776
+ parts.append(T['extern_decl'].substitute(
777
+ func_type=self._func_type(),
778
+ table_name=self._table_name(),
779
+ state_max=self._state_max(),
780
+ event_max=self._event_max(),
781
+ ))
782
+ return ''.join(parts)
783
+
784
+ def generate_function_dictionary(self, state_machine: StateMachine) -> str:
785
+ T = self.DICT_TEMPLATES
786
+ parts = []
787
+ func_type = self._func_type()
788
+ state_type = self._state_type()
789
+ event_type = self._event_type()
790
+ dict_type = f"TransitionDictEntry_{self.layer_name}_t" if self.layer_name else "TransitionDictEntry_t"
791
+ dict_name = f"transition_dict_{self.layer_name}" if self.layer_name else "transition_dict"
792
+ size_macro = f"TRANSITION_DICT_{self.layer_name.upper()}_SIZE" if self.layer_name else "TRANSITION_DICT_SIZE"
793
+
794
+ parts.append(T['struct_comment'])
795
+ parts.append(T['struct_open'])
796
+ parts.append(T['struct_field_from'].substitute(state_type=state_type))
797
+ parts.append(T['struct_field_event'].substitute(event_type=event_type))
798
+ parts.append(T['struct_field_target'].substitute(state_type=state_type))
799
+ parts.append(T['struct_field_condition'])
800
+ parts.append(T['struct_field_func'].substitute(func_type=func_type))
801
+ parts.append(T['struct_close'].substitute(dict_type=dict_type))
802
+
803
+ parts.append(T['dict_comment'])
804
+ parts.append(T['dict_open'].substitute(
805
+ dict_type=dict_type, dict_name=dict_name))
806
+
807
+ for state in state_machine.states.values():
808
+ for event in state_machine.events.values():
809
+ transitions = state_machine.get_transitions_for_cell(
810
+ state.name, event.name)
811
+ if not transitions:
812
+ continue
813
+ for idx, trans in enumerate(transitions):
814
+ base_name = (
815
+ f"{self.layer_name}_{state.name}_{event.name or 'NONE'}"
816
+ if self.layer_name
817
+ else f"{state.name}_{event.name or 'NONE'}"
818
+ )
819
+ trans_name = (f"{base_name}_{idx}"
820
+ if len(transitions) > 1 else base_name)
821
+ target = trans.target or state.name
822
+ condition = trans.condition.replace('"', '\\"') if trans.condition else ""
823
+ parts.append(T['dict_entry'].substitute(
824
+ trans_name=trans_name,
825
+ state_enum=self._state_enum(state.name),
826
+ event_enum=self._event_enum(event.name),
827
+ target_enum=self._state_enum(target),
828
+ condition=condition,
829
+ func_name=self._cell_func_name(state.name, event.name),
830
+ ))
831
+ parts.append(T['dict_close'])
832
+ parts.append(T['dict_size'].substitute(
833
+ size_macro=size_macro, dict_name=dict_name))
834
+ return ''.join(parts)
835
+
836
+ def generate_process_function(self, state_machine: StateMachine,
837
+ generation_style: str = None) -> str:
838
+ if generation_style is None:
839
+ generation_style = self.DEFAULT_GENERATION_STYLE
840
+ generator = self.process_generators.get(generation_style)
841
+ if generator is None or generation_style != 'table_driven':
842
+ generator = self.process_generators['table_driven']
843
+ return generator(state_machine)
844
+
845
+ def _generate_process_table_driven(self, state_machine: StateMachine) -> str:
846
+ T = self.PROCESS_TEMPLATES
847
+ func_name = (f"StateMachine_Process_{self.layer_name}"
848
+ if self.layer_name else "StateMachine_Process")
849
+ parts = []
850
+ parts.append(T['comment'].substitute(layer=self.layer_name or "system"))
851
+ parts.append(T['signature'].substitute(
852
+ state_type=self._state_type(),
853
+ func_name=func_name,
854
+ event_type=self._event_type(),
855
+ ))
856
+ parts.append(T['body'].substitute(
857
+ context_type=self._context_type(),
858
+ func_type=self._func_type(),
859
+ table_name=self._table_name(),
860
+ state_type=self._state_type(),
861
+ layer=self.layer_name or "System",
862
+ ))
863
+ return ''.join(parts)
864
+
865
+ def _generate_process_switch_case(self, sm):
866
+ return self._generate_process_table_driven(sm)
867
+
868
+ def generate_get_next_event_function(self, state_machine: StateMachine) -> str:
869
+ T = self.GET_NEXT_TEMPLATES
870
+ func_name = (f"StateMachine_GetNextEvent_{self.layer_name}"
871
+ if self.layer_name else "StateMachine_GetNextEvent")
872
+ event_none = self._event_enum("")
873
+ parts = []
874
+ parts.append(T['comment'].substitute(
875
+ layer=self.layer_name or "system",
876
+ event_none=event_none))
877
+ parts.append(T['signature'].substitute(
878
+ event_type=self._event_type(),
879
+ func_name=func_name))
880
+ # [C-52] Pass layer name; falls back to layer_name attr
881
+ layer = self.layer_name or ""
882
+ parts.append(T['body'].substitute(
883
+ event_type=self._event_type(),
884
+ event_none=event_none,
885
+ layer=layer))
886
+ return ''.join(parts)
887
+
888
+ def generate_all(self, state_machine: StateMachine) -> Dict[str, str]:
889
+ return {
890
+ 'cell_prototypes': self.generate_transition_cell_prototypes(state_machine),
891
+ 'cell_functions': self.generate_transition_cell_functions(state_machine),
892
+ 'transition_table': self.generate_transition_table(state_machine),
893
+ 'transition_table_header': self.generate_transition_table_header(state_machine),
894
+ 'function_dict': self.generate_function_dictionary(state_machine),
895
+ 'process_func': self.generate_process_function(state_machine),
896
+ 'get_next_event': self.generate_get_next_event_function(state_machine),
897
+ }
898
+
899
+ def generate_all_transitions(self, state_machine, table_type='array',
900
+ process_type='table_driven'):
901
+ result = self.generate_all(state_machine)
902
+ return '\n'.join([
903
+ result['cell_prototypes'],
904
+ result['transition_table'],
905
+ result['cell_functions'],
906
+ result['process_func'],
907
+ ])