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,345 @@
1
+ # codegen/interrupt_generator.py
2
+ """\nInterrupt handler ISR generation module (H3: ISR context support)\n\nFeatures:\n 1. Automatic ctx pointer insertion (SystemContext_t *ctx = &g_ctx;)\n 2. Action Namespace.Name -> RoleFunc_<NS>_<Name>(NULL, ctx) conversion\n 3. Legacy identifier(args) -> RoleFunc_<Layer>_<Pascal>(NULL, ctx) conversion\n 4. User marker [[STABLE_USER_CODE_START:<Marker>]]\n 5. Automatic extraction of used_role_functions / used_variables\n 6. Entry / exit logging\n\nMarker naming convention:\n handler.name = \"TIMER0\" -> ISR_TIMER0 / Marker \"TIMER0\"\n handler.name = \"UART_RX\" -> ISR_UARTRX / Marker \"UARTRX\"\n"""
3
+
4
+ import sys
5
+ import os
6
+ import re
7
+ import logging
8
+ from typing import Dict, Callable, List, Any, Optional, Tuple
9
+
10
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
11
+
12
+ from statable.global_defs import (
13
+ InterruptHandlerDef, InterruptAction, GlobalDefinitions,
14
+ )
15
+
16
+ try:
17
+ from .naming_convention import CNamingConvention
18
+ from .code_templates import CodeTemplates
19
+ except ImportError:
20
+ from naming_convention import CNamingConvention
21
+ from code_templates import CodeTemplates
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ # C reserved words (excluded to avoid mis-converting function-call forms)
27
+ _C_KEYWORDS = {
28
+ 'if', 'else', 'for', 'while', 'do', 'switch', 'case',
29
+ 'default', 'break', 'continue', 'return', 'goto',
30
+ 'sizeof', 'typedef', 'struct', 'union', 'enum',
31
+ 'static', 'const', 'volatile', 'extern', 'inline',
32
+ 'void', 'int', 'char', 'short', 'long', 'float', 'double',
33
+ 'signed', 'unsigned', 'bool', 'true', 'false', 'NULL',
34
+ }
35
+
36
+
37
+ class InterruptGenerator:
38
+ """Interrupt handler ISR generation class (H3: context support)"""
39
+
40
+ # ================================================================
41
+ # Class constants
42
+ # ================================================================
43
+ AUTO_INSERT_CTX = True
44
+ CTX_VAR_NAME = 'ctx'
45
+ G_CTX_NAME = 'g_ctx'
46
+
47
+ def __init__(self):
48
+ self.naming = CNamingConvention()
49
+ self.templates = CodeTemplates()
50
+ self.strings = self.templates.STRINGS
51
+ self.formats = self.templates.FORMATS
52
+ self.layer_name: str = ''
53
+
54
+ # ============================================================
55
+ # Generation step
56
+ # ============================================================
57
+ self.isr_steps = [
58
+ {'action': 'comment'},
59
+ {'action': 'signature'},
60
+ {'action': 'open'},
61
+ {'action': 'context'},
62
+ {'action': 'enter_log'},
63
+ {'action': 'actions'},
64
+ {'action': 'user_section'},
65
+ {'action': 'exit_log'},
66
+ {'action': 'close'},
67
+ ]
68
+
69
+ self.step_executors: Dict[str, Callable] = {
70
+ 'comment': self._execute_comment_step,
71
+ 'signature': self._execute_signature_step,
72
+ 'open': self._execute_open_step,
73
+ 'context': self._execute_context_step,
74
+ 'enter_log': self._execute_enter_log_step,
75
+ 'actions': self._execute_actions_step,
76
+ 'user_section': self._execute_user_section_step,
77
+ 'exit_log': self._execute_exit_log_step,
78
+ 'close': self._execute_close_step,
79
+ }
80
+
81
+ def set_layer(self, layer_name: str):
82
+ """Set layer name (used for RoleFunc name generation in legacy identifier(args) form)"""
83
+ self.layer_name = layer_name or ''
84
+
85
+ def _log_debug(self, message, level='debug'):
86
+ log_func = getattr(logger, level, logger.debug)
87
+ log_func(message)
88
+
89
+ # ================================================================
90
+ # Name generation
91
+ # ================================================================
92
+ def _get_isr_function_name(self, handler) -> str:
93
+ """ISR_<PascalCase(handler.name)>"""
94
+ name = getattr(handler, 'name', '') or 'unnamed'
95
+ return f"ISR_{self.naming.to_pascal_case(name)}"
96
+
97
+ def _get_marker_name(self, handler) -> str:
98
+ """PascalCase(handler.name)"""
99
+ name = getattr(handler, 'name', '') or 'unnamed'
100
+ return self.naming.to_pascal_case(name)
101
+
102
+ def _get_handler_display_name(self, handler) -> str:
103
+ """Log display name (original name as-is)"""
104
+ return getattr(handler, 'name', '') or 'unnamed'
105
+
106
+ # ================================================================
107
+ # Action parsing
108
+ # ================================================================
109
+ def _parse_action(self, action_text: str) -> Tuple[str, List[str]]:
110
+ """\n Convert action string to C code\n\n Args:\n action_text: Input action string\n\n Returns:\n (c_code, used_role_functions)\n\n Conversion rules:\n 1. Namespace.Name [(args)] -> RoleFunc_Namespace_Name(NULL, ctx)\n 2. identifier(args) -> RoleFunc_<Layer>_PascalId(NULL, ctx)\n 3. otherwise -> as-is\n """
111
+ if not action_text:
112
+ return "", []
113
+ text = action_text.strip()
114
+ if not text:
115
+ return "", []
116
+
117
+ used: List[str] = []
118
+
119
+ # --- 1. Namespace.Name form ---
120
+ m = re.fullmatch(
121
+ r'([A-Z]\w*)\.([A-Za-z_]\w*)\s*(\(.*\))?',
122
+ text, re.DOTALL,
123
+ )
124
+ if m:
125
+ ns = m.group(1)
126
+ name = m.group(2)
127
+ pascal = self.naming.to_pascal_case(name)
128
+ qualified = f"{ns}.{name}"
129
+ used.append(qualified)
130
+ return f"RoleFunc_{ns}_{pascal}(NULL, ctx)", used
131
+
132
+ # --- 2. Function call form ---
133
+ m = re.fullmatch(
134
+ r'([A-Za-z_]\w*)\s*(\(.*\))',
135
+ text, re.DOTALL,
136
+ )
137
+ if m:
138
+ fname = m.group(1)
139
+ # Names already starting with RoleFunc_ / ISR_ are kept as-is
140
+ if fname.startswith('RoleFunc_') or fname.startswith('ISR_'):
141
+ return text, used
142
+ # C keywords kept as-is
143
+ if fname in _C_KEYWORDS:
144
+ return text, used
145
+ # Convert to RoleFunc name (arguments discarded)
146
+ pascal = self.naming.to_pascal_case(fname)
147
+ if self.layer_name:
148
+ call = f"RoleFunc_{self.layer_name}_{pascal}(NULL, ctx)"
149
+ qualified = f"{self.layer_name}.{pascal}"
150
+ else:
151
+ call = f"RoleFunc_{pascal}(NULL, ctx)"
152
+ qualified = pascal
153
+ used.append(qualified)
154
+ return call, used
155
+
156
+ # --- 3. Otherwise ---
157
+ return text, used
158
+
159
+ def _parse_action_with_semicolon(
160
+ self, action: InterruptAction,
161
+ ) -> Tuple[str, List[str]]:
162
+ """Convert condition-attached action to a single line of C code"""
163
+ body, used = self._parse_action(getattr(action, 'action', ''))
164
+ if not body:
165
+ return "", used
166
+ # Normalize trailing semicolon
167
+ body = body.rstrip(';').rstrip()
168
+ condition = (getattr(action, 'condition', '') or '').strip()
169
+ if condition:
170
+ return f"if ({condition}) {{ {body}; }}", used
171
+ return f"{body};", used
172
+
173
+ # ================================================================
174
+ # Used symbol extraction
175
+ # ================================================================
176
+ def extract_used_symbols(
177
+ self, handler: InterruptHandlerDef,
178
+ ) -> Tuple[List[str], List[str]]:
179
+ """\n Extract used role functions and used variables from handler\n\n Returns:\n (used_role_functions, used_variables)\n used_role_functions: [\"Driver.Init\", \"Application.HandleTick\"]\n used_variables: [\"counter\", \"EVT_START\"]\n """
180
+ used_rfs: List[str] = []
181
+ used_vars: List[str] = []
182
+ seen_rf = set()
183
+ seen_var = set()
184
+
185
+ for action in getattr(handler, 'actions', []):
186
+ text = getattr(action, 'action', '') or ''
187
+ _, rfs = self._parse_action(text)
188
+ for r in rfs:
189
+ if r and r not in seen_rf:
190
+ seen_rf.add(r)
191
+ used_rfs.append(r)
192
+
193
+ # ctx->data.X / ctx->flags.X
194
+ for m in re.finditer(
195
+ r'ctx->(?:data|flags)\.([A-Za-z_]\w*)', text,
196
+ ):
197
+ v = m.group(1)
198
+ if v and v not in seen_var:
199
+ seen_var.add(v)
200
+ used_vars.append(v)
201
+
202
+ return used_rfs, used_vars
203
+
204
+ def update_handler_symbols(self, handler: InterruptHandlerDef):
205
+ """Recompute handler's used_role_functions / used_variables and write back"""
206
+ used_rfs, used_vars = self.extract_used_symbols(handler)
207
+ handler.used_role_functions = used_rfs
208
+ handler.used_variables = used_vars
209
+
210
+ # ================================================================
211
+ # Step execution
212
+ # ================================================================
213
+ def _execute_comment_step(self, step, context):
214
+ handler = context['handler']
215
+ display = self._get_handler_display_name(handler)
216
+ used_rfs = context.get('used_role_functions', [])
217
+ description = getattr(handler, 'description', '') or ''
218
+
219
+ lines = [
220
+ "/**",
221
+ f" * @brief {display} interrupt handler",
222
+ ]
223
+ if description:
224
+ lines.append(f" * @note {description}")
225
+ if used_rfs:
226
+ lines.append(" * @note Used role functions:")
227
+ for r in used_rfs:
228
+ lines.append(f" * - {r}")
229
+ lines.append(" */")
230
+ return lines
231
+
232
+ def _execute_signature_step(self, step, context):
233
+ handler = context['handler']
234
+ func_name = self._get_isr_function_name(handler)
235
+ return [f"void {func_name}(void)"]
236
+
237
+ def _execute_open_step(self, step, context):
238
+ return ["{"]
239
+
240
+ def _execute_context_step(self, step, context):
241
+ if not self.AUTO_INSERT_CTX:
242
+ return []
243
+ return [
244
+ "",
245
+ " /* ===== Context reference (auto-generated) ===== */",
246
+ f" SystemContext_t *{self.CTX_VAR_NAME} = &{self.G_CTX_NAME};",
247
+ f" (void){self.CTX_VAR_NAME};",
248
+ ]
249
+
250
+ def _execute_enter_log_step(self, step, context):
251
+ handler = context['handler']
252
+ display = self._get_handler_display_name(handler)
253
+ return [
254
+ "",
255
+ " /* ===== Entry log ===== */",
256
+ f' LOG_DEBUG("Enter ISR: {display}");',
257
+ ]
258
+
259
+ def _execute_actions_step(self, step, context):
260
+ handler = context['handler']
261
+ actions = getattr(handler, 'actions', []) or []
262
+ if not actions:
263
+ return []
264
+
265
+ lines = [
266
+ "",
267
+ " /* ===== Actions (auto-generated) ===== */",
268
+ ]
269
+ has_action = False
270
+ for action in actions:
271
+ body, _ = self._parse_action_with_semicolon(action)
272
+ if body:
273
+ lines.append(f" {body}")
274
+ has_action = True
275
+ if not has_action:
276
+ lines.append(" /* (action undefined) */")
277
+ return lines
278
+
279
+ def _execute_user_section_step(self, step, context):
280
+ handler = context['handler']
281
+ marker = self._get_marker_name(handler)
282
+ return [
283
+ "",
284
+ " /* ===== User extension area ===== */",
285
+ f" /* [[STABLE_USER_CODE_START:{marker}]] */",
286
+ " /* Add user code here */",
287
+ f" /* [[STABLE_USER_CODE_END:{marker}]] */",
288
+ ]
289
+
290
+ def _execute_exit_log_step(self, step, context):
291
+ handler = context['handler']
292
+ display = self._get_handler_display_name(handler)
293
+ return [
294
+ "",
295
+ " /* ===== Exit log ===== */",
296
+ f' LOG_DEBUG("Exit ISR: {display}");',
297
+ ]
298
+
299
+ def _execute_close_step(self, step, context):
300
+ return ["}"]
301
+
302
+ # ================================================================
303
+ # Public API
304
+ # ================================================================
305
+ def generate_isr(
306
+ self, handler: InterruptHandlerDef,
307
+ update_handler: bool = False,
308
+ ) -> str:
309
+ """\n Generate ISR\n\n Args:\n handler: Interrupt handler definition\n update_handler: If True, write used_* back to handler\n """
310
+ self._log_debug(f"Generating ISR for: {handler.name}")
311
+
312
+ used_rfs, used_vars = self.extract_used_symbols(handler)
313
+
314
+ if update_handler:
315
+ handler.used_role_functions = used_rfs
316
+ handler.used_variables = used_vars
317
+
318
+ context = {
319
+ 'handler': handler,
320
+ 'used_role_functions': used_rfs,
321
+ 'used_variables': used_vars,
322
+ }
323
+ lines: List[str] = []
324
+ for step in self.isr_steps:
325
+ executor = self.step_executors.get(step['action'])
326
+ if executor is None:
327
+ continue
328
+ result = executor(step, context)
329
+ if result:
330
+ lines.extend(result)
331
+ return '\n'.join(lines)
332
+
333
+ def generate_all_isrs(
334
+ self, global_defs: GlobalDefinitions,
335
+ update_handlers: bool = False,
336
+ ) -> str:
337
+ """Generate all ISRs"""
338
+ lines: List[str] = []
339
+ for handler in getattr(global_defs, 'interrupts', []):
340
+ lines.append(self.generate_isr(
341
+ handler, update_handler=update_handlers,
342
+ ))
343
+ lines.append("")
344
+ lines.append("")
345
+ return '\n'.join(lines).rstrip() + '\n'
@@ -0,0 +1,171 @@
1
+ # codegen/naming_convention.py
2
+ """
3
+ C language naming convention module.
4
+
5
+ Version: 2.2.5 (2026-09-19)
6
+ - Fix: create_type_name() is now idempotent.
7
+ Names already ending in '_t' are preserved (avoids '_t_t' doubling
8
+ such as 'SystemStatus_t' -> 'SystemStatusT_t').
9
+ """
10
+
11
+ import re
12
+ import sys
13
+ import os
14
+
15
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
16
+
17
+ try:
18
+ from .code_templates import CodeTemplates
19
+ except ImportError:
20
+ from code_templates import CodeTemplates
21
+
22
+
23
+ class CNamingConvention:
24
+ """Class managing C naming conventions"""
25
+
26
+ CONVERSION_PATTERNS = {
27
+ 'upper_snake': {
28
+ 'patterns': [
29
+ (r'(.)([A-Z][a-z]+)', r'\1_\2'),
30
+ (r'([a-z0-9])([A-Z])', r'\1_\2'),
31
+ ],
32
+ 'transform': str.upper,
33
+ },
34
+ 'lower_snake': {
35
+ 'patterns': [
36
+ (r'(.)([A-Z][a-z]+)', r'\1_\2'),
37
+ (r'([a-z0-9])([A-Z])', r'\1_\2'),
38
+ ],
39
+ 'transform': str.lower,
40
+ },
41
+ 'camel': {
42
+ 'split': r'[_-]',
43
+ 'first': str.lower,
44
+ 'rest': str.capitalize,
45
+ },
46
+ 'pascal': {
47
+ 'split': r'[_-]',
48
+ 'first': str.capitalize,
49
+ 'rest': str.capitalize,
50
+ },
51
+ }
52
+
53
+ C_KEYWORDS = {
54
+ 'auto', 'break', 'case', 'char', 'const', 'continue',
55
+ 'default', 'do', 'double', 'else', 'enum', 'extern',
56
+ 'float', 'for', 'goto', 'if', 'inline', 'int', 'long',
57
+ 'register', 'restrict', 'return', 'short', 'signed',
58
+ 'sizeof', 'static', 'struct', 'switch', 'typedef',
59
+ 'union', 'unsigned', 'void', 'volatile', 'while',
60
+ '_Bool', '_Complex', '_Imaginary'
61
+ }
62
+
63
+ IDENTIFIER_RULES = {
64
+ 'variable': 'to_lower_snake',
65
+ 'function': 'to_pascal_case',
66
+ 'type': 'to_pascal_case',
67
+ 'enum': 'to_upper_snake',
68
+ 'macro': 'to_upper_snake',
69
+ }
70
+
71
+ def __init__(self):
72
+ self.templates = CodeTemplates()
73
+ self.strings = self.templates.STRINGS
74
+ self.formats = self.templates.FORMATS
75
+
76
+ @classmethod
77
+ def to_snake_case(cls, name, upper=False):
78
+ config = cls.CONVERSION_PATTERNS['upper_snake' if upper else 'lower_snake']
79
+ result = name
80
+ for pattern, replacement in config['patterns']:
81
+ result = re.sub(pattern, replacement, result)
82
+ return config['transform'](result)
83
+
84
+ @classmethod
85
+ def to_upper_snake(cls, name):
86
+ return cls.to_snake_case(name, upper=True)
87
+
88
+ @classmethod
89
+ def to_lower_snake(cls, name):
90
+ return cls.to_snake_case(name, upper=False)
91
+
92
+ @classmethod
93
+ def to_camel_case(cls, name):
94
+ config = cls.CONVERSION_PATTERNS['camel']
95
+ parts = re.split(config['split'], name)
96
+ if not parts:
97
+ return ""
98
+ result = config['first'](parts[0])
99
+ for part in parts[1:]:
100
+ if part:
101
+ result += config['rest'](part)
102
+ return result
103
+
104
+ @classmethod
105
+ def to_pascal_case(cls, name):
106
+ """Convert to PascalCase"""
107
+ config = cls.CONVERSION_PATTERNS['pascal']
108
+ parts = re.split(config['split'], name)
109
+ if not parts:
110
+ return ""
111
+
112
+ result = ""
113
+ for part in parts:
114
+ if part:
115
+ # If camelCase, capitalize first letter
116
+ result += part[0].upper() + part[1:]
117
+ return result
118
+
119
+ @classmethod
120
+ def sanitize_identifier(cls, name):
121
+ if not name:
122
+ return "_unnamed"
123
+ if name[0].isdigit():
124
+ name = '_' + name
125
+ name = re.sub(r'[^a-zA-Z0-9_]', '_', name)
126
+ if name in cls.C_KEYWORDS:
127
+ name = name + '_'
128
+ return name
129
+
130
+ @classmethod
131
+ def create_identifier(cls, name, kind='variable'):
132
+ method_name = cls.IDENTIFIER_RULES.get(kind, 'to_lower_snake')
133
+ method = getattr(cls, method_name)
134
+ return cls.sanitize_identifier(method(name))
135
+
136
+ @classmethod
137
+ def create_type_name(cls, name):
138
+ """Create a C type name with '_t' suffix (idempotent).
139
+
140
+ [v2.2.5 fix]
141
+ If the name already ends with '_t', it is preserved as-is
142
+ so that references match. Otherwise PascalCase + '_t' is applied.
143
+
144
+ Examples:
145
+ 'SystemStatus' -> 'SystemStatus_t'
146
+ 'SystemStatus_t' -> 'SystemStatus_t' (not 'SystemStatusT_t')
147
+ 'sensor_data' -> 'SensorData_t'
148
+ 'sensor_data_t' -> 'sensor_data_t' (preserved)
149
+ '' -> 'Unknown_t'
150
+ """
151
+ if not name:
152
+ return "Unknown_t"
153
+ if name.endswith('_t'):
154
+ return name
155
+ return cls.to_pascal_case(name) + "_t"
156
+
157
+ @classmethod
158
+ def create_enum_value(cls, prefix, name):
159
+ return cls.sanitize_identifier(prefix) + "_" + cls.to_upper_snake(name)
160
+
161
+ @classmethod
162
+ def create_function_name(cls, module, action):
163
+ return cls.to_pascal_case(module) + "_" + cls.to_pascal_case(action)
164
+
165
+ @classmethod
166
+ def create_variable_name(cls, name):
167
+ return cls.sanitize_identifier(cls.to_lower_snake(name))
168
+
169
+ @classmethod
170
+ def create_macro_name(cls, name):
171
+ return cls.to_upper_snake(name)