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
codegen/sample_data.py ADDED
@@ -0,0 +1,269 @@
1
+ # codegen/sample_data.py
2
+ """
3
+ Sample data for code generation tests.
4
+ Provides test data independent of the GUI.
5
+
6
+ [v1.5 fix]
7
+ - Transition action= migrated to pre_actions=[] (bug #87)
8
+ The action field is defined as "compat / unused" in v1.4 sec 2.2,
9
+ and role_function_generator only references pre_actions,
10
+ so action= does not reflect calls in code generation.
11
+ """
12
+
13
+ import sys
14
+ import os
15
+
16
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
17
+
18
+ from statable.model import State, Event, Transition, StateType, EventKind, RoleFunction
19
+ from statable.state_machine import StateMachine
20
+ from statable.global_defs import (
21
+ GlobalDefinitions, SystemVariable, EventFlag,
22
+ CustomTypeDef, StructMemberDef,
23
+ EventQueueDef, InterruptHandlerDef, InterruptAction,
24
+ DevicePlaceholderDef, TimerBaseDef, TimerDerivedDef
25
+ )
26
+
27
+
28
+ class SampleDataGenerator:
29
+ """Sample data generator class"""
30
+
31
+ def __init__(self):
32
+ self.state_machine = None
33
+ self.global_defs = None
34
+
35
+ def create_sample_state_machine(self):
36
+ sm = StateMachine()
37
+
38
+ sm.add_state(State(name="INIT", type=StateType.INITIAL, description="Initial state"))
39
+ sm.add_state(State(name="IDLE", type=StateType.NORMAL, description="Idle state"))
40
+ sm.add_state(State(name="RUNNING", type=StateType.NORMAL, description="Running state"))
41
+ sm.add_state(State(name="ERROR", type=StateType.NORMAL, description="Error state"))
42
+ sm.set_initial("INIT")
43
+
44
+ sm.add_event(Event(name="POWER_ON", kind=EventKind.SIGNAL, description="Power-on event"))
45
+ sm.add_event(Event(name="START", kind=EventKind.SIGNAL, description="Start event"))
46
+ sm.add_event(Event(name="STOP", kind=EventKind.SIGNAL, description="Stop event"))
47
+ sm.add_event(Event(name="ERROR_DETECTED", kind=EventKind.SIGNAL, description="Error detected event"))
48
+
49
+ # ==============================================================
50
+ # [v1.5 fix] action= -> pre_actions=[]
51
+ # ==============================================================
52
+ sm.add_transition(Transition(
53
+ source="INIT",
54
+ event="POWER_ON",
55
+ target="IDLE",
56
+ pre_actions=["PowerOn"],
57
+ title="Power on",
58
+ ))
59
+ sm.add_transition(Transition(
60
+ source="IDLE",
61
+ event="START",
62
+ target="RUNNING",
63
+ condition="StartOk",
64
+ pre_actions=["Start"],
65
+ title="Start",
66
+ ))
67
+ sm.add_transition(Transition(
68
+ source="RUNNING",
69
+ event="STOP",
70
+ target="IDLE",
71
+ pre_actions=["Stop"],
72
+ title="Stop",
73
+ ))
74
+ sm.add_transition(Transition(
75
+ source="RUNNING",
76
+ event="ERROR_DETECTED",
77
+ target="ERROR",
78
+ pre_actions=["HandleError"],
79
+ title="Error handling",
80
+ ))
81
+
82
+ sm.add_role_function(RoleFunction(
83
+ name="PowerOn", return_type="void",
84
+ description="Power-on processing",
85
+ ))
86
+ sm.add_role_function(RoleFunction(
87
+ name="StartOk", return_type="bool",
88
+ description="Start condition check",
89
+ ))
90
+ sm.add_role_function(RoleFunction(
91
+ name="Start", return_type="void",
92
+ description="Start processing",
93
+ ))
94
+ sm.add_role_function(RoleFunction(
95
+ name="Stop", return_type="void",
96
+ description="Stop processing",
97
+ ))
98
+ sm.add_role_function(RoleFunction(
99
+ name="HandleError", return_type="void",
100
+ description="Error handling",
101
+ ))
102
+ sm.add_role_function(RoleFunction(
103
+ name="ProcessData", return_type="int",
104
+ arg1_type="uint8_t*", arg1_name="data",
105
+ arg2_type="uint16_t", arg2_name="len",
106
+ description="Data processing",
107
+ ))
108
+
109
+ self.state_machine = sm
110
+ return sm
111
+
112
+ def create_sample_global_defs(self):
113
+ gd = GlobalDefinitions()
114
+
115
+ gd.variables = [
116
+ SystemVariable(name="battery_voltage", type="uint16", unit="mV",
117
+ group="Power", description="Battery voltage"),
118
+ SystemVariable(name="system_tick", type="uint32", unit="ms",
119
+ group="Timer", description="System timer"),
120
+ SystemVariable(name="temperature", type="int16", unit="0.1C",
121
+ group="Sensor", description="Temperature sensor value"),
122
+ SystemVariable(name="data_buffer", type="uint8",
123
+ group="Data", description="Data buffer",
124
+ array_size=64),
125
+ ]
126
+
127
+ gd.flags = [
128
+ EventFlag(name="EVT_POWER_ON_REQ", min_value=0, max_value=1,
129
+ group="System", description="Power-on request"),
130
+ EventFlag(name="EVT_START_REQ", min_value=0, max_value=1,
131
+ group="System", description="Start request"),
132
+ EventFlag(name="EVT_STOP_REQ", min_value=0, max_value=1,
133
+ group="System", description="Stop request"),
134
+ EventFlag(name="EVT_ERROR_FLAG", min_value=0, max_value=1,
135
+ group="Error", description="Error flag"),
136
+ ]
137
+
138
+ gd.custom_types = [
139
+ CustomTypeDef(name="SystemStatus", description="System status management struct", members=[
140
+ StructMemberDef(name="power_on", data_type="bool", bit_width=1,
141
+ description="Power-on state"),
142
+ StructMemberDef(name="initialized", data_type="bool", bit_width=1,
143
+ description="Initialization completed flag"),
144
+ StructMemberDef(name="error_code", data_type="uint8",
145
+ description="Error code"),
146
+ StructMemberDef(name="mode", data_type="uint8",
147
+ description="Operation mode"),
148
+ ]),
149
+ CustomTypeDef(name="SensorData", description="Sensor data struct", members=[
150
+ StructMemberDef(name="temperature", data_type="int16",
151
+ description="Temperature value"),
152
+ StructMemberDef(name="humidity", data_type="uint8",
153
+ description="Humidity value"),
154
+ StructMemberDef(name="pressure", data_type="uint16",
155
+ description="Pressure value"),
156
+ ]),
157
+ ]
158
+
159
+ gd.event_queues = [
160
+ EventQueueDef(
161
+ name="MainEventQueue",
162
+ size=16,
163
+ element_type="EVENT_t",
164
+ event_ids=["POWER_ON", "START", "STOP", "ERROR_DETECTED"],
165
+ priority_enabled=False,
166
+ interrupt_safe=True,
167
+ rtos_enabled=False,
168
+ description="Main event queue",
169
+ ),
170
+ EventQueueDef(
171
+ name="HighPriorityQueue",
172
+ size=8,
173
+ element_type="EVENT_t",
174
+ event_ids=["ERROR_DETECTED"],
175
+ priority_enabled=True,
176
+ interrupt_safe=True,
177
+ rtos_enabled=False,
178
+ description="High-priority event queue",
179
+ ),
180
+ ]
181
+
182
+ gd.interrupts = [
183
+ InterruptHandlerDef(
184
+ name="UART_RX",
185
+ description="UART receive interrupt",
186
+ event_names=["START", "STOP"],
187
+ is_timer=False,
188
+ actions=[
189
+ InterruptAction(condition="", action="EVT_START_REQ = 1"),
190
+ InterruptAction(
191
+ condition="ctx->data.battery_voltage > 3000",
192
+ action="EVT_STOP_REQ = 1",
193
+ ),
194
+ ],
195
+ ),
196
+ InterruptHandlerDef(
197
+ name="TimerTick",
198
+ description="Timer interrupt",
199
+ event_names=[],
200
+ is_timer=True,
201
+ actions=[
202
+ InterruptAction(condition="", action="ctx->data.system_tick++"),
203
+ ],
204
+ ),
205
+ ]
206
+
207
+ gd.placeholders = [
208
+ DevicePlaceholderDef(name="UART0", description="UART communication port"),
209
+ DevicePlaceholderDef(name="ADC0", description="AD converter"),
210
+ ]
211
+
212
+ gd.timer_base = TimerBaseDef(
213
+ variable_name="g_system_tick",
214
+ unit="1ms",
215
+ data_type="volatile uint32_t",
216
+ interrupt_name="TimerTick",
217
+ derived=[
218
+ TimerDerivedDef(period_name="10ms", multiplier=10,
219
+ variable_name="g_tick_10ms", data_type="uint8_t"),
220
+ TimerDerivedDef(period_name="100ms", multiplier=100,
221
+ variable_name="g_tick_100ms", data_type="uint8_t"),
222
+ ],
223
+ )
224
+
225
+ gd.extra_timers = [
226
+ TimerBaseDef(
227
+ variable_name="g_high_speed_tick",
228
+ unit="100us",
229
+ data_type="volatile uint32_t",
230
+ interrupt_name="HighSpeedTimerTick",
231
+ derived=[
232
+ TimerDerivedDef(period_name="1ms", multiplier=10,
233
+ variable_name="g_hs_tick_1ms",
234
+ data_type="uint16_t"),
235
+ ],
236
+ ),
237
+ ]
238
+
239
+ self.global_defs = gd
240
+ return gd
241
+
242
+ def get_sample_data(self):
243
+ if self.state_machine is None:
244
+ self.create_sample_state_machine()
245
+ if self.global_defs is None:
246
+ self.create_sample_global_defs()
247
+ return self.state_machine, self.global_defs
248
+
249
+
250
+ _sample_data_instance = None
251
+
252
+
253
+ def get_sample_data_instance():
254
+ global _sample_data_instance
255
+ if _sample_data_instance is None:
256
+ _sample_data_instance = SampleDataGenerator()
257
+ return _sample_data_instance
258
+
259
+
260
+ def get_sample_state_machine():
261
+ return get_sample_data_instance().create_sample_state_machine()
262
+
263
+
264
+ def get_sample_global_defs():
265
+ return get_sample_data_instance().create_sample_global_defs()
266
+
267
+
268
+ def get_sample_data():
269
+ return get_sample_data_instance().get_sample_data()
@@ -0,0 +1,395 @@
1
+ # codegen/state_actions_generator.py
2
+ """
3
+ State actions generation module (v2.7.0)
4
+
5
+ Generates per-layer Entry / Exit / Do dispatch code:
6
+
7
+ - statable_state_actions_<Layer>.h
8
+ void <Layer>_Entry(STATE_<Layer>_t state, SystemContext_t *ctx);
9
+ void <Layer>_Exit (STATE_<Layer>_t state, SystemContext_t *ctx);
10
+ void <Layer>_Do (STATE_<Layer>_t state, SystemContext_t *ctx);
11
+
12
+ - statable_state_actions_<Layer>.c
13
+ - Entry / Exit / Do dispatch tables (ordered init, C89-compatible)
14
+ - Dispatchers
15
+ - Per-state functions with [[STABLE_USER_CODE_..._custom]] markers
16
+
17
+ Design notes
18
+ ------------
19
+ - Table order MUST match STATE_<Layer>_t enum order.
20
+ - Each per-state function contains:
21
+ * GUI-edited actions (regenerated)
22
+ * User custom code (preserved)
23
+ - Role function calls receive NULL for the transition pointer
24
+ (state actions are not tied to a specific transition).
25
+
26
+ Version: 1.0 (2026-09-26 / initial)
27
+ """
28
+
29
+ import sys
30
+ import os
31
+ import logging
32
+ from string import Template
33
+ from typing import Dict, List, Optional
34
+
35
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
36
+
37
+ from statable.model import State, ActionStep
38
+ from statable.state_machine import StateMachine
39
+
40
+ try:
41
+ from .type_mapper import CTypeMapper
42
+ from .naming_convention import CNamingConvention
43
+ from .code_templates import CodeTemplates
44
+ except ImportError:
45
+ from type_mapper import CTypeMapper
46
+ from naming_convention import CNamingConvention
47
+ from code_templates import CodeTemplates
48
+
49
+ logger = logging.getLogger(__name__)
50
+
51
+
52
+ def _extract_rf_name(action) -> str:
53
+ """[v2.7.0] Extract role_function name from ActionStep or str."""
54
+ if action is None:
55
+ return ""
56
+ if isinstance(action, str):
57
+ return action
58
+ return getattr(action, "role_function", "") or ""
59
+
60
+
61
+ class StateActionsGenerator:
62
+ """State actions (Entry / Exit / Do) code generator."""
63
+
64
+ HEADER_TEMPLATES = {
65
+ 'file_comment': '''/**
66
+ * @file statable_state_actions_{layer}.h
67
+ * @brief State actions (Entry / Exit / Do) for {layer} layer
68
+ *
69
+ * @note Code auto-generated by StaTable
70
+ * - Manual editing is not recommended
71
+ * - To modify, use StaTable
72
+ */''',
73
+ 'guard_start': '#ifndef STATABLE_STATE_ACTIONS_{LAYER}_H\n'
74
+ '#define STATABLE_STATE_ACTIONS_{LAYER}_H\n',
75
+ 'guard_end': '#endif /* STATABLE_STATE_ACTIONS_{LAYER}_H */',
76
+ 'include_common': '#include "statable_types_common.h"',
77
+ 'include_layer': '#include "statable_types_{layer}.h"',
78
+ 'decl_entry': 'void {layer}_Entry(STATE_{layer}_t state, SystemContext_t *ctx);',
79
+ 'decl_exit': 'void {layer}_Exit (STATE_{layer}_t state, SystemContext_t *ctx);',
80
+ 'decl_do': 'void {layer}_Do (STATE_{layer}_t state, SystemContext_t *ctx);',
81
+ }
82
+
83
+ SOURCE_TEMPLATES = {
84
+ 'file_comment': '''/**
85
+ * @file statable_state_actions_{layer}.c
86
+ * @brief State actions (Entry / Exit / Do) for {layer} layer
87
+ *
88
+ * @note Code auto-generated by StaTable
89
+ * - Manual editing is not recommended
90
+ * - To modify, use StaTable
91
+ * - C89-compatible: no designated initializers used.
92
+ * Table order MUST match STATE_{layer}_t enum order.
93
+ */''',
94
+ 'include_own': '#include "statable_state_actions_{layer}.h"',
95
+ 'type_typedef': 'typedef void (*{layer}_StateFunc_t)(SystemContext_t *ctx);',
96
+ }
97
+
98
+ def __init__(self):
99
+ self.mapper = CTypeMapper()
100
+ self.naming = CNamingConvention()
101
+ self.templates = CodeTemplates()
102
+ self.layer_name: str = ""
103
+
104
+ def set_layer(self, layer_name: str):
105
+ self.layer_name = layer_name
106
+
107
+ def _log_debug(self, message, level='debug'):
108
+ log_func = getattr(logger, level, logger.debug)
109
+ log_func(message)
110
+
111
+ # ------------------------------------------------------------------
112
+ # Name helpers
113
+ # ------------------------------------------------------------------
114
+ def _state_type(self) -> str:
115
+ return (f"STATE_{self.layer_name}_t"
116
+ if self.layer_name else "STATE_t")
117
+
118
+ def _state_max(self) -> str:
119
+ return (f"STATE_{self.layer_name}_MAX"
120
+ if self.layer_name else "STATE_MAX")
121
+
122
+ def _state_enum(self, state_name: str) -> str:
123
+ s = self.naming.to_pascal_case(state_name) if state_name else "Unknown"
124
+ return (f"STATE_{self.layer_name}_{s}"
125
+ if self.layer_name else f"STATE_{s}")
126
+
127
+ def _func_type(self) -> str:
128
+ return (f"{self.layer_name}_StateFunc_t"
129
+ if self.layer_name else "StateFunc_t")
130
+
131
+ def _state_suffix(self, state_name: str) -> str:
132
+ """C-safe suffix for per-state function names."""
133
+ return self.naming.to_pascal_case(state_name) if state_name else "Unknown"
134
+
135
+ def _rf_call(self, rf_name: str) -> str:
136
+ """Generate RoleFunc_<NS>_<Name>(NULL, ctx) or fallback."""
137
+ if not rf_name:
138
+ return ""
139
+ if rf_name.startswith("RoleFunc_"):
140
+ full = rf_name
141
+ elif "." in rf_name:
142
+ ns, name = rf_name.split(".", 1)
143
+ full = (f"RoleFunc_{self.naming.to_pascal_case(ns)}_"
144
+ f"{self.naming.to_pascal_case(name)}")
145
+ elif self.layer_name:
146
+ full = (f"RoleFunc_{self.layer_name}_"
147
+ f"{self.naming.to_pascal_case(rf_name)}")
148
+ else:
149
+ full = f"RoleFunc_{self.naming.to_pascal_case(rf_name)}"
150
+ return f"(void){full}(NULL, ctx)"
151
+
152
+ def _fire_event_call(self, event_name: str) -> str:
153
+ """Generate FIRE_EVENT_<Layer>(ctx, EVENT_<Layer>_<EVT>).
154
+
155
+ The FIRE_EVENT_<Layer> macro is defined as:
156
+ #define FIRE_EVENT_Driver(ctx, evt) ...
157
+ so `ctx` must be passed explicitly.
158
+
159
+ The event enum is named EVENT_<Layer>_<UPPER_SNAKE>.
160
+ """
161
+ if not event_name:
162
+ return ""
163
+ # event_name is expected to be "Layer.EVENT" or "EVENT"
164
+ if "." in event_name:
165
+ layer, evt = event_name.split(".", 1)
166
+ else:
167
+ layer, evt = self.layer_name, event_name
168
+ evt_upper = self.naming.to_upper_snake(evt)
169
+ return f"FIRE_EVENT_{layer}(ctx, EVENT_{layer}_{evt_upper})"
170
+
171
+ # ------------------------------------------------------------------
172
+ # Per-action code emission
173
+ # ------------------------------------------------------------------
174
+ def _action_line(self, action: ActionStep, indent: str = " ") -> str:
175
+ """Emit one ActionStep as a C statement (with optional condition)."""
176
+ atype = getattr(action, "action_type", "role") or "role"
177
+ cond = (getattr(action, "condition", "") or "").strip()
178
+
179
+ if atype == "fire_event":
180
+ call = self._fire_event_call(
181
+ getattr(action, "event_name", "") or "")
182
+ if not call:
183
+ return ""
184
+ elif atype == "custom":
185
+ # Custom is not emitted from GUI actions
186
+ return ""
187
+ else:
188
+ rf = _extract_rf_name(action)
189
+ call = self._rf_call(rf)
190
+ if not call:
191
+ return ""
192
+
193
+ if cond:
194
+ return (f"{indent}if ({cond}) {{\n"
195
+ f"{indent} {call};\n"
196
+ f"{indent}}}\n")
197
+ return f"{indent}{call};\n"
198
+
199
+ def _emit_actions(self, actions: List[ActionStep]) -> str:
200
+ """Emit all GUI-edited actions for one kind (Entry / Exit / Do)."""
201
+ if not actions:
202
+ return ""
203
+ parts = [" /* --- GUI-edited actions (regenerated) --- */\n"]
204
+ for a in actions:
205
+ line = self._action_line(a)
206
+ if line:
207
+ parts.append(line)
208
+ parts.append(" /* --- end GUI-edited actions --- */\n\n")
209
+ return "".join(parts)
210
+
211
+ # ------------------------------------------------------------------
212
+ # Per-state function
213
+ # ------------------------------------------------------------------
214
+ def _state_func_name(self, kind: str, state_name: str) -> str:
215
+ """<Layer>_<Kind>_<State>"""
216
+ return (f"{self.layer_name}_{kind}_"
217
+ f"{self._state_suffix(state_name)}")
218
+
219
+ def _custom_marker_name(self, kind: str, state_name: str) -> str:
220
+ return (f"{self.layer_name}_{kind}_"
221
+ f"{self._state_suffix(state_name)}_custom")
222
+
223
+ def _generate_state_func(self, kind: str, state: State,
224
+ actions: List[ActionStep]) -> str:
225
+ """Generate one per-state function (Entry / Exit / Do)."""
226
+ func_name = self._state_func_name(kind, state.name)
227
+ marker = self._custom_marker_name(kind, state.name)
228
+
229
+ parts = []
230
+ parts.append(f"static void {func_name}(SystemContext_t *ctx)\n")
231
+ parts.append("{\n")
232
+ parts.append(self._emit_actions(actions))
233
+ parts.append(
234
+ f" /* --- user custom code (preserved) --- */\n")
235
+ parts.append(
236
+ f" /* [[STABLE_USER_CODE_START:{marker}]] */\n")
237
+ parts.append(" (void)ctx;\n")
238
+ parts.append(
239
+ f" /* [[STABLE_USER_CODE_END:{marker}]] */\n")
240
+ parts.append("}\n")
241
+ return "".join(parts)
242
+
243
+ # ------------------------------------------------------------------
244
+ # Entry / Exit / Do groups
245
+ # ------------------------------------------------------------------
246
+ def _collect_actions_by_kind(self, sm: StateMachine,
247
+ kind: str) -> Dict[str, List[ActionStep]]:
248
+ """Return {state_name: [ActionStep, ...]} for the given kind."""
249
+ result: Dict[str, List[ActionStep]] = {}
250
+ for state in sm.states.values():
251
+ if kind == "Entry":
252
+ actions = list(getattr(state, "entry", []) or [])
253
+ elif kind == "Exit":
254
+ actions = list(getattr(state, "exit", []) or [])
255
+ elif kind == "Do":
256
+ actions = list(getattr(state, "do_actions", []) or [])
257
+ else:
258
+ actions = []
259
+ result[state.name] = actions
260
+ return result
261
+
262
+ # ------------------------------------------------------------------
263
+ # Forward declarations
264
+ # ------------------------------------------------------------------
265
+ def _generate_forward_decls(self, sm: StateMachine) -> str:
266
+ parts = []
267
+ for kind in ("Entry", "Exit", "Do"):
268
+ parts.append(
269
+ f"/* ---- {kind} forward declarations ---- */\n")
270
+ for state in sm.states.values():
271
+ func_name = self._state_func_name(kind, state.name)
272
+ parts.append(
273
+ f"static void {func_name}(SystemContext_t *ctx);\n")
274
+ parts.append("\n")
275
+ return "".join(parts)
276
+
277
+ # ------------------------------------------------------------------
278
+ # Dispatch table (ordered init, C89)
279
+ # ------------------------------------------------------------------
280
+ def _generate_table(self, kind: str, sm: StateMachine) -> str:
281
+ """Generate one ordered-init dispatch table."""
282
+ layer = self.layer_name or "System"
283
+ func_type = self._func_type()
284
+ state_max = self._state_max()
285
+ table_name = f"g_{layer}_{kind}Table"
286
+
287
+ parts = []
288
+ parts.append(
289
+ f"static const {func_type} {table_name}[{state_max}] = {{\n")
290
+
291
+ # Ordered by enum definition order.
292
+ # StateMachine.states is a dict (insertion order); we rely on
293
+ # the same iteration order used by enum generation.
294
+ for state in sm.states.values():
295
+ func_name = self._state_func_name(kind, state.name)
296
+ parts.append(
297
+ f" {func_name}, /* {self._state_enum(state.name)} */\n")
298
+ parts.append("};\n")
299
+ return "".join(parts)
300
+
301
+ # ------------------------------------------------------------------
302
+ # Dispatcher
303
+ # ------------------------------------------------------------------
304
+ def _generate_dispatcher(self, kind: str, sm: StateMachine) -> str:
305
+ layer = self.layer_name or "System"
306
+ func_type = self._func_type()
307
+ state_type = self._state_type()
308
+ state_max = self._state_max()
309
+ table_name = f"g_{layer}_{kind}Table"
310
+
311
+ parts = []
312
+ parts.append(f"void {layer}_{kind}({state_type} state, "
313
+ f"SystemContext_t *ctx)\n")
314
+ parts.append("{\n")
315
+ parts.append(f" if ((unsigned)state < (unsigned){state_max}) {{\n")
316
+ parts.append(f" {func_type} fn = {table_name}[state];\n")
317
+ parts.append(" if (fn != NULL) {\n")
318
+ parts.append(" fn(ctx);\n")
319
+ parts.append(" }\n")
320
+ parts.append(" }\n")
321
+ parts.append("}\n")
322
+ return "".join(parts)
323
+
324
+ # ------------------------------------------------------------------
325
+ # Public API
326
+ # ------------------------------------------------------------------
327
+ def generate_header(self, sm: StateMachine) -> str:
328
+ """Generate statable_state_actions_<Layer>.h"""
329
+ layer = self.layer_name or "System"
330
+ T = self.HEADER_TEMPLATES
331
+ guard = T['guard_start'].replace("{LAYER}", layer.upper())
332
+
333
+ parts = []
334
+ parts.append(T['file_comment'].format(layer=layer))
335
+ parts.append("\n\n")
336
+ parts.append(guard)
337
+ parts.append("\n")
338
+ parts.append(T['include_common'])
339
+ parts.append("\n")
340
+ parts.append(T['include_layer'].format(layer=layer))
341
+ parts.append("\n\n")
342
+ parts.append(
343
+ "/* Entry / Exit / Do dispatch (state actions) */\n")
344
+ parts.append(T['decl_entry'].format(layer=layer))
345
+ parts.append("\n")
346
+ parts.append(T['decl_exit'].format(layer=layer))
347
+ parts.append("\n")
348
+ parts.append(T['decl_do'].format(layer=layer))
349
+ parts.append("\n\n")
350
+ parts.append(T['guard_end'].replace("{LAYER}", layer.upper()))
351
+ parts.append("\n")
352
+ return "".join(parts)
353
+
354
+ def generate_source(self, sm: StateMachine) -> str:
355
+ """Generate statable_state_actions_<Layer>.c"""
356
+ layer = self.layer_name or "System"
357
+ T = self.SOURCE_TEMPLATES
358
+
359
+ parts = []
360
+ parts.append(T['file_comment'].format(layer=layer))
361
+ parts.append("\n\n")
362
+ parts.append(T['include_own'].format(layer=layer))
363
+ parts.append("\n\n")
364
+ parts.append(T['type_typedef'].format(layer=layer))
365
+ parts.append("\n\n")
366
+
367
+ # Forward declarations
368
+ parts.append(self._generate_forward_decls(sm))
369
+ parts.append("\n")
370
+
371
+ # Tables
372
+ for kind in ("Entry", "Exit", "Do"):
373
+ parts.append(
374
+ f"/* ---- {kind} dispatch table ---- */\n")
375
+ parts.append(self._generate_table(kind, sm))
376
+ parts.append("\n")
377
+
378
+ # Dispatchers
379
+ for kind in ("Entry", "Exit", "Do"):
380
+ parts.append(
381
+ f"/* ---- {kind} dispatcher ---- */\n")
382
+ parts.append(self._generate_dispatcher(kind, sm))
383
+ parts.append("\n")
384
+
385
+ # Per-state functions
386
+ for kind in ("Entry", "Exit", "Do"):
387
+ actions_by_state = self._collect_actions_by_kind(sm, kind)
388
+ parts.append(
389
+ f"/* ---- {kind} functions (user-editable) ---- */\n")
390
+ for state in sm.states.values():
391
+ parts.append(self._generate_state_func(
392
+ kind, state, actions_by_state.get(state.name, [])))
393
+ parts.append("\n")
394
+
395
+ return "".join(parts)