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,1244 @@
1
+ # codegen/role_function_generator.py
2
+ """
3
+ Role function generation module (multi-layer state machine / ISR support)
4
+
5
+ Version: 3.4 (2026-09-23 / R-10: all (void) suppressions in user marker)
6
+ - Extend the v2.5.2 user-editable suppression block (which covered
7
+ only ctx->data.*) to also cover from_state, event, transition_id,
8
+ and ctx. All (void) lines now live INSIDE the STABLE_USER_CODE
9
+ marker so users can delete individual lines once they start using
10
+ the corresponding variable. The now-unused `null_guard_suppress`
11
+ and `unused_ctx` templates were removed.
12
+
13
+ Version: 3.3 (2026-09-20 / MISRA 12.1 fix)
14
+ Version: 3.2 (2026-09-20 / MISRA fixes)
15
+ Version: 3.1 (2026-09-19 / v2.2.5 fixes)
16
+ Version: 3.0 (2026-09-13 / Stage 4)
17
+ [v1.5] [v1.6]
18
+ """
19
+
20
+ import sys
21
+ import os
22
+ import re
23
+ import logging
24
+ from string import Template
25
+ from typing import Dict, List, Iterable, Optional
26
+
27
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
28
+
29
+ from statable.model import RoleFunction
30
+
31
+ try:
32
+ from .type_mapper import CTypeMapper
33
+ from .naming_convention import CNamingConvention
34
+ from .code_templates import CodeTemplates
35
+ from .code_merger import CodeMerger
36
+ except ImportError:
37
+ from type_mapper import CTypeMapper
38
+ from naming_convention import CNamingConvention
39
+ from code_templates import CodeTemplates
40
+ from code_merger import CodeMerger
41
+
42
+ logger = logging.getLogger(__name__)
43
+
44
+
45
+ # ======================================================================
46
+ # v1.5 addition: C identifier validation regex
47
+ # ======================================================================
48
+ _VALID_C_IDENTIFIER = re.compile(r'^[A-Za-z_][A-Za-z0-9_]*$')
49
+ _VALID_QUALIFIED = re.compile(
50
+ r'^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)?$'
51
+ )
52
+
53
+
54
+ # ======================================================================
55
+ # Helpers
56
+ # ======================================================================
57
+ def ensure_list(value) -> List[str]:
58
+ if value is None:
59
+ return []
60
+ if isinstance(value, str):
61
+ s = value.strip()
62
+ return [s] if s else []
63
+ if isinstance(value, list):
64
+ return [str(x) for x in value if str(x).strip()]
65
+ return [str(value)]
66
+
67
+
68
+ def _extract_rf_name(action) -> str:
69
+ """[v2.7.0] Extract role function name from ActionStep or str.
70
+
71
+ State.entry / state.exit are List[ActionStep] since v2.7.0,
72
+ but may still hold plain strings from legacy data.
73
+ """
74
+ if action is None:
75
+ return ""
76
+ if isinstance(action, str):
77
+ return action
78
+ return getattr(action, "role_function", "") or ""
79
+
80
+
81
+ _C_KEYWORDS = {
82
+ 'auto', 'break', 'case', 'char', 'const', 'continue',
83
+ 'default', 'do', 'double', 'else', 'enum', 'extern',
84
+ 'float', 'for', 'goto', 'if', 'inline', 'int', 'long',
85
+ 'register', 'restrict', 'return', 'short', 'signed',
86
+ 'sizeof', 'static', 'struct', 'switch', 'typedef',
87
+ 'union', 'unsigned', 'void', 'volatile', 'while',
88
+ 'true', 'false', 'NULL',
89
+ }
90
+
91
+
92
+ class RoleFuncCallSite:
93
+ """One role function call site (from_state x event)"""
94
+ __slots__ = ('func_name', 'kind', 'from_state', 'event', 'target')
95
+
96
+ def __init__(self, func_name, kind, from_state, event, target):
97
+ self.func_name = func_name
98
+ self.kind = kind
99
+ self.from_state = from_state
100
+ self.event = event
101
+ self.target = target
102
+
103
+ def key(self):
104
+ return (self.from_state, self.event)
105
+
106
+ def __repr__(self):
107
+ return (f"CallSite({self.func_name!r}, {self.kind!r}, "
108
+ f"{self.from_state}->{self.target})")
109
+
110
+
111
+ class RoleFunctionGenerator:
112
+ """Role function generation class (multi-layer state machine / ISR support)"""
113
+
114
+ # ================================================================
115
+ # [Table 1] Declaration templates
116
+ # ================================================================
117
+ DECLARATION_TEMPLATES = {
118
+ 'comment_with_desc': Template(
119
+ '/**\n'
120
+ ' * @brief Role function: $title\n'
121
+ ' * @note $description\n'
122
+ ' * @param transition Transition context (may be NULL: called from ISR)\n'
123
+ ' * @param ctx System context pointer\n'
124
+ ' * @return 0: success, non-zero: error (can also be used for condition checks)\n'
125
+ ' */\n'
126
+ ),
127
+ 'comment_no_desc': Template(
128
+ '/**\n'
129
+ ' * @brief Role function: $title\n'
130
+ ' * @param transition Transition context (may be NULL: called from ISR)\n'
131
+ ' * @param ctx System context pointer\n'
132
+ ' * @return 0: success, non-zero: error (can also be used for condition checks)\n'
133
+ ' */\n'
134
+ ),
135
+ 'signature_open': Template('int $func_name(\n'),
136
+ 'signature_args': Template(
137
+ ' const $context_type *transition,\n'
138
+ ' SystemContext_t *ctx\n'
139
+ ),
140
+ 'declaration_close': ');\n',
141
+ }
142
+
143
+ # ================================================================
144
+ # [Table 2] Implementation templates (with NULL guard)
145
+ # ================================================================
146
+ IMPLEMENTATION_TEMPLATES = {
147
+ 'comment_with_desc': Template(
148
+ '/**\n'
149
+ ' * @brief Role function: $title\n'
150
+ ' * @note $description\n'
151
+ '$call_sites'
152
+ ' */\n'
153
+ ),
154
+ 'comment_no_desc': Template(
155
+ '/**\n'
156
+ ' * @brief Role function: $title\n'
157
+ '$call_sites'
158
+ ' */\n'
159
+ ),
160
+ 'signature_open': Template('int $func_name(\n'),
161
+ 'signature_args': Template(
162
+ ' const $context_type *transition,\n'
163
+ ' SystemContext_t *ctx\n'
164
+ ),
165
+ 'body_open': ')\n{\n',
166
+
167
+ 'null_guard_header': (
168
+ ' /* ===== transition NULL guard (supports ISR calls) ===== */\n'
169
+ ),
170
+ 'null_guard_decl': Template(
171
+ ' $c_type $member_name = $max_value;\n'
172
+ ),
173
+ 'null_guard_check': (
174
+ ' if (transition != NULL) {\n'
175
+ ),
176
+ 'null_guard_assign': Template(
177
+ ' $member_name = transition->$member_name;\n'
178
+ ),
179
+ 'null_guard_close': (
180
+ ' }\n'
181
+ ),
182
+
183
+ 'blank': '\n',
184
+
185
+ 'local_transition_id_header': (
186
+ ' /* ===== transition ID (index within call_sites) ===== */\n'
187
+ ),
188
+ # [v3.2 / MISRA 17.7] transition_id is declared here, but its
189
+ # (void) suppression was moved into the user-code marker by
190
+ # v3.4 / R-10.
191
+ 'local_transition_id_decl': Template(
192
+ ' const uint16_t transition_id = Transition_GetId(\n'
193
+ ' transition, $table_arg, $count_arg);\n'
194
+ ),
195
+
196
+ 'local_data_header': ' /* ===== local pointer to ctx->data ===== */\n',
197
+ 'local_data_pointer': Template(
198
+ ' $c_type *const $var_name = &ctx->data.$var_name;'
199
+ ' /* $comment */\n'
200
+ ),
201
+ 'local_data_array': Template(
202
+ ' $c_type *const $var_name = ctx->data.$var_name;'
203
+ ' /* $comment */\n'
204
+ ),
205
+
206
+ 'local_ret_header': ' /* ===== return value ===== */\n',
207
+ 'local_ret_decl': ' int ret = 0; /* can be modified in user code */\n',
208
+
209
+ 'todo_comment': Template(' /* $todo */\n'),
210
+ 'user_marker_start': Template(
211
+ ' /* [[STABLE_USER_CODE_START:$marker_name]] */\n'
212
+ ),
213
+ 'user_marker_hint': ' /* Write user implementation code here */\n',
214
+ 'user_marker_end': Template(
215
+ ' /* [[STABLE_USER_CODE_END:$marker_name]] */\n'
216
+ ),
217
+
218
+ 'return_default': ' return ret;\n',
219
+ 'body_close': '}\n',
220
+ }
221
+
222
+ # ================================================================
223
+ # [Table 3] TRANSITION_ID_NONE constant
224
+ # ================================================================
225
+ NONE_DEFINE_TEMPLATES = {
226
+ 'section_comment': (
227
+ '\n'
228
+ '/* ============================================================== */\n'
229
+ '/* Transition ID constant */\n'
230
+ '/* ============================================================== */\n'
231
+ ),
232
+ 'define': '#define TRANSITION_ID_NONE ((uint16_t)0xFFFF)\n',
233
+ }
234
+
235
+ # ================================================================
236
+ # [Table 4] Common struct for call site tables
237
+ # ================================================================
238
+ ENTRY_STRUCT_TEMPLATES = {
239
+ 'section_comment': (
240
+ '\n'
241
+ '/* ============================================================== */\n'
242
+ '/* Role function call site table (common struct) */\n'
243
+ '/* Identify cells by {from_state, event} pair */\n'
244
+ '/* ============================================================== */\n'
245
+ ),
246
+ 'struct_typedef': Template(
247
+ 'typedef struct {\n'
248
+ ' $state_type from_state; /* source state */\n'
249
+ ' $event_type event; /* event */\n'
250
+ '} $entry_type;\n'
251
+ ),
252
+ }
253
+
254
+ # ================================================================
255
+ # [Table 5] Transition_GetId
256
+ # ================================================================
257
+ TRANSITION_ID_FUNC_TEMPLATES = {
258
+ 'prototype_comment': (
259
+ '\n'
260
+ '/* Transition_GetId forward declaration (body at end of file) */\n'
261
+ ),
262
+ 'prototype': Template(
263
+ 'static uint16_t Transition_GetId(\n'
264
+ ' const $context_type *transition,\n'
265
+ ' const $entry_type *table,\n'
266
+ ' uint16_t table_size);\n'
267
+ ),
268
+ 'definition_comment': (
269
+ '\n\n'
270
+ '/* ============================================================== */\n'
271
+ '/* Transition ID conversion (end of file) */\n'
272
+ '/* Linearly scans the per-function call_sites table and */\n'
273
+ '/* returns the index of the matching entry. */\n'
274
+ '/* Returns TRANSITION_ID_NONE if no match / transition==NULL. */\n'
275
+ '/* ============================================================== */\n'
276
+ '/**\n'
277
+ ' * @brief Convert transition info to a unique ID\n'
278
+ ' * @param transition Transition context (may be NULL)\n'
279
+ ' * @param table Call site table (may be NULL)\n'
280
+ ' * @param table_size Number of entries in the table\n'
281
+ ' * @return Index in the table (TRANSITION_ID_NONE if no match)\n'
282
+ ' */\n'
283
+ ),
284
+ # [v3.3 / MISRA 12.1] Explicit parentheses around sub-expressions
285
+ # of `||` and `&&`.
286
+ 'definition': Template(
287
+ 'static uint16_t Transition_GetId(\n'
288
+ ' const $context_type *transition,\n'
289
+ ' const $entry_type *table,\n'
290
+ ' uint16_t table_size)\n'
291
+ '{\n'
292
+ ' uint16_t i;\n'
293
+ '\n'
294
+ ' if ((transition == NULL) || (table == NULL)) {\n'
295
+ ' return TRANSITION_ID_NONE;\n'
296
+ ' }\n'
297
+ '\n'
298
+ ' for (i = 0; i < table_size; i++) {\n'
299
+ ' if ((table[i].from_state == transition->from_state) &&\n'
300
+ ' (table[i].event == transition->event)) {\n'
301
+ ' return i;\n'
302
+ ' }\n'
303
+ ' }\n'
304
+ ' return TRANSITION_ID_NONE;\n'
305
+ '}\n'
306
+ ),
307
+ }
308
+
309
+ # ================================================================
310
+ # [Table 6] Per-function call_sites table
311
+ # ================================================================
312
+ CALL_SITE_TABLE_TEMPLATES = {
313
+ 'table_header': Template(
314
+ '\n/* --- call site table for $func_name --- */\n'
315
+ ),
316
+ 'table_open': Template(
317
+ 'static const $struct_type $table_name[] = {\n'
318
+ ),
319
+ 'entry': Template(
320
+ ' { $from_state,$event },\n'
321
+ ),
322
+ 'table_close': '};\n',
323
+ 'count_macro': Template(
324
+ '#define $count_macro \\\n'
325
+ ' (sizeof($table_name) / sizeof($table_name[0]))\n'
326
+ ),
327
+ }
328
+
329
+ # ================================================================
330
+ # [Table 7] Call site comments
331
+ # ================================================================
332
+ CALL_SITES_COMMENT_TEMPLATES = {
333
+ 'header': ' *\n * @note Call sites:\n',
334
+ 'line': Template(
335
+ ' * - [$kind]$kind_pad $from_state$from_pad'
336
+ ' -[$event]-> $target\n'
337
+ ),
338
+ 'none': ' *\n * @note Call sites: (none)\n',
339
+ }
340
+
341
+ # ================================================================
342
+ # [Table 8] User-added area at end of file
343
+ # ================================================================
344
+ TAIL_USER_SECTION_TEMPLATES = {
345
+ 'section_comment': (
346
+ '\n\n'
347
+ '/* ============================================================== */\n'
348
+ '/* User-added area */\n'
349
+ '/* Code added here is preserved across regenerations */\n'
350
+ '/* ============================================================== */\n'
351
+ ),
352
+ 'marker_start': '/* [[STABLE_USER_CODE_TAIL_START]] */\n',
353
+ 'default_hint': '/* Write user-added code here (helper functions, etc.) */\n',
354
+ 'marker_end': '/* [[STABLE_USER_CODE_TAIL_END]] */\n',
355
+ }
356
+
357
+ # ================================================================
358
+ # Constructor
359
+ # ================================================================
360
+ def __init__(self):
361
+ self.mapper = CTypeMapper()
362
+ self.naming = CNamingConvention()
363
+ self.templates = CodeTemplates()
364
+ self.strings = self.templates.STRINGS
365
+ self.formats = self.templates.FORMATS
366
+ self.merger = CodeMerger()
367
+ self.layer_name: str = ""
368
+
369
+ def set_layer(self, layer_name: str):
370
+ self.layer_name = layer_name
371
+
372
+ def _log_debug(self, message, level='debug'):
373
+ log_func = getattr(logger, level, logger.debug)
374
+ log_func(message)
375
+
376
+ # ================================================================
377
+ # Name generation (namespace support)
378
+ # ================================================================
379
+ def _resolve_name_and_namespace(self, func) -> tuple:
380
+ name = getattr(func, 'name', 'unnamed')
381
+ namespace = getattr(func, 'namespace', '') or ''
382
+ if not namespace and self.layer_name:
383
+ namespace = self.layer_name
384
+ return name, namespace
385
+
386
+ def _generate_function_name(self, func) -> str:
387
+ """RoleFunc_<Namespace>_<PascalName>"""
388
+ name, namespace = self._resolve_name_and_namespace(func)
389
+ pascal = self.naming.to_pascal_case(name)
390
+ if namespace:
391
+ return f"RoleFunc_{namespace}_{pascal}"
392
+ return f"RoleFunc_{pascal}"
393
+
394
+ def _get_marker_name(self, func) -> str:
395
+ name, namespace = self._resolve_name_and_namespace(func)
396
+ pascal = self.naming.to_pascal_case(name)
397
+ if namespace:
398
+ return f"{namespace}_{pascal}"
399
+ return pascal
400
+
401
+ def _get_short_name(self, func) -> str:
402
+ return self.naming.to_pascal_case(getattr(func, 'name', 'unnamed'))
403
+
404
+ def _context_type(self) -> str:
405
+ if self.layer_name:
406
+ return f"TransitionContext_{self.layer_name}_t"
407
+ return "TransitionContext_t"
408
+
409
+ def _state_type(self) -> str:
410
+ return f"STATE_{self.layer_name}_t" if self.layer_name else "STATE_t"
411
+
412
+ def _event_type(self) -> str:
413
+ return f"EVENT_{self.layer_name}_t" if self.layer_name else "EVENT_t"
414
+
415
+ def _state_enum(self, state_name: str) -> str:
416
+ s = self.naming.to_pascal_case(state_name) if state_name else "Unknown"
417
+ return f"STATE_{self.layer_name}_{s}" if self.layer_name else f"STATE_{s}"
418
+
419
+ def _event_enum(self, event_name: str) -> str:
420
+ e = self.naming.to_upper_snake(event_name) if event_name else "NONE"
421
+ return f"EVENT_{self.layer_name}_{e}" if self.layer_name else f"EVENT_{e}"
422
+
423
+ def _state_max(self) -> str:
424
+ return f"STATE_{self.layer_name}_MAX" if self.layer_name else "STATE_MAX"
425
+
426
+ def _event_none(self) -> str:
427
+ return f"EVENT_{self.layer_name}_NONE" if self.layer_name else "EVENT_NONE"
428
+
429
+ def _entry_struct_type(self) -> str:
430
+ return (f"RoleFuncCallSiteEntry_{self.layer_name}_t"
431
+ if self.layer_name else "RoleFuncCallSiteEntry_t")
432
+
433
+ # ================================================================
434
+ # Deduplication
435
+ # ================================================================
436
+ def _dedupe_by_name(self, funcs: Iterable) -> List:
437
+ seen = set()
438
+ result = []
439
+ for func in funcs:
440
+ name = getattr(func, 'name', None)
441
+ namespace = getattr(func, 'namespace', '') or ''
442
+ if not name:
443
+ self._log_debug(
444
+ f"_dedupe_by_name: skip (no name): {func!r}",
445
+ 'warning'
446
+ )
447
+ continue
448
+ key = f"{namespace}.{name}" if namespace else name
449
+ if key in seen:
450
+ self._log_debug(
451
+ f"_dedupe_by_name: duplicate skipped: {key}"
452
+ )
453
+ continue
454
+ seen.add(key)
455
+ result.append(func)
456
+ return result
457
+
458
+ def _resolve_comment_title(self, func) -> str:
459
+ title = getattr(func, 'title', '')
460
+ name = getattr(func, 'name', 'unnamed')
461
+ qualified = getattr(func, 'qualified_name', name)
462
+ if title and title != f"Role function: {name}" \
463
+ and title != f"Role function: {qualified}":
464
+ return title
465
+ return qualified
466
+
467
+ # ================================================================
468
+ # v1.5: reference normalization (with identifier validation)
469
+ # ================================================================
470
+ def _normalize_func_ref(self, ref: str) -> str:
471
+ if not ref:
472
+ return ""
473
+ name = ref.strip()
474
+ if not name:
475
+ return ""
476
+
477
+ # 1. Strip argument part
478
+ if '(' in name:
479
+ name = name.split('(', 1)[0].strip()
480
+
481
+ # 2. Handle RoleFunc_ prefix
482
+ if name.startswith("RoleFunc_"):
483
+ rest = name[len("RoleFunc_"):]
484
+ if self.layer_name:
485
+ prefix = f"{self.layer_name}_"
486
+ if rest.startswith(prefix):
487
+ candidate = f"{self.layer_name}.{rest[len(prefix):]}"
488
+ if _VALID_QUALIFIED.match(candidate):
489
+ return candidate
490
+ self._log_debug(
491
+ f"_normalize_func_ref: reject invalid RoleFunc_ ref: "
492
+ f"{ref!r} (candidate={candidate!r})",
493
+ 'warning',
494
+ )
495
+ return ""
496
+ if _VALID_C_IDENTIFIER.match(rest):
497
+ return rest
498
+ self._log_debug(
499
+ f"_normalize_func_ref: reject invalid RoleFunc_ ref: "
500
+ f"{ref!r} (rest={rest!r})",
501
+ 'warning',
502
+ )
503
+ return ""
504
+
505
+ # 3. v1.5 addition: identifier validation
506
+ if not _VALID_QUALIFIED.match(name):
507
+ self._log_debug(
508
+ f"_normalize_func_ref: reject invalid identifier: "
509
+ f"{ref!r} (name={name!r})",
510
+ 'warning',
511
+ )
512
+ return ""
513
+
514
+ return name
515
+
516
+ # ================================================================
517
+ # Extract function names from condition expression
518
+ # ================================================================
519
+ def _extract_func_names_from_condition(self, condition: str) -> List[str]:
520
+ if not condition:
521
+ return []
522
+ names = set()
523
+
524
+ # 1. RoleFunc_XXX form
525
+ for m in re.finditer(r'\bRoleFunc_\w+', condition):
526
+ names.add(m.group(0))
527
+
528
+ # 2. Namespace.Name form
529
+ for m in re.finditer(r'\b([A-Z]\w*)\.([A-Za-z_]\w*)\b', condition):
530
+ names.add(f"{m.group(1)}.{m.group(2)}")
531
+
532
+ # 3. function_name(...) form
533
+ for m in re.finditer(r'\b([A-Za-z_][A-Za-z0-9_]*)\s*\(', condition):
534
+ n = m.group(1)
535
+ if n not in _C_KEYWORDS:
536
+ names.add(n)
537
+
538
+ # 4. Whole string is a bare identifier
539
+ stripped = condition.strip()
540
+ if re.fullmatch(r'[A-Za-z_][A-Za-z0-9_]*', stripped):
541
+ if stripped not in _C_KEYWORDS:
542
+ names.add(stripped)
543
+
544
+ # 5. Bare identifiers within the expression (PascalCase only)
545
+ for m in re.finditer(r'(?<![.\w])([A-Z]\w*)(?![.\w])', condition):
546
+ n = m.group(1)
547
+ if n not in _C_KEYWORDS:
548
+ names.add(n)
549
+
550
+ return [self._normalize_func_ref(n) for n in names if n]
551
+
552
+ # ================================================================
553
+ # Call site collection
554
+ # ================================================================
555
+ def _collect_call_sites(self, state_machine
556
+ ) -> Dict[str, List[RoleFuncCallSite]]:
557
+ """Collect all call sites for role functions.
558
+
559
+ [v2.2.5 fix]
560
+ Extended to also collect:
561
+ - State entry / exit references
562
+ - Cell-level actions (before_transitions / after_transitions)
563
+ """
564
+ if state_machine is None:
565
+ return {}
566
+ call_map: Dict[str, List[RoleFuncCallSite]] = {}
567
+ seen = set()
568
+
569
+ # ---- 1. Transition-level refs ----
570
+ for state in state_machine.states.values():
571
+ for event in state_machine.events.values():
572
+ transitions = state_machine.get_transitions_for_cell(
573
+ state.name, event.name
574
+ )
575
+ for trans in transitions:
576
+ target = getattr(trans, 'target', '') or ''
577
+ cond = getattr(trans, 'condition', '') or ''
578
+ for fname in self._extract_func_names_from_condition(cond):
579
+ key = (fname, 'condition', state.name, event.name)
580
+ if key in seen:
581
+ continue
582
+ seen.add(key)
583
+ call_map.setdefault(fname, []).append(RoleFuncCallSite(
584
+ fname, 'condition',
585
+ state.name, event.name, target,
586
+ ))
587
+ for action in ensure_list(
588
+ getattr(trans, 'pre_actions', [])):
589
+ norm = self._normalize_func_ref(action)
590
+ if not norm:
591
+ continue
592
+ key = (norm, 'pre_action', state.name, event.name)
593
+ if key in seen:
594
+ continue
595
+ seen.add(key)
596
+ call_map.setdefault(norm, []).append(RoleFuncCallSite(
597
+ norm, 'pre_action',
598
+ state.name, event.name, target,
599
+ ))
600
+ else_target = getattr(trans, 'else_target', '') or ''
601
+ for action in ensure_list(
602
+ getattr(trans, 'else_actions', [])):
603
+ norm = self._normalize_func_ref(action)
604
+ if not norm:
605
+ continue
606
+ key = (norm, 'else_action', state.name, event.name)
607
+ if key in seen:
608
+ continue
609
+ seen.add(key)
610
+ call_map.setdefault(norm, []).append(RoleFuncCallSite(
611
+ norm, 'else_action',
612
+ state.name, event.name,
613
+ else_target or target,
614
+ ))
615
+
616
+ # ---- 2. v2.7.0 fix: State entry / exit refs ----
617
+ # state.entry / state.exit are List[ActionStep]; extract role_function
618
+ for state in state_machine.states.values():
619
+ for action in (getattr(state, 'entry', []) or []):
620
+ fname = _extract_rf_name(action)
621
+ if not fname:
622
+ continue
623
+ norm = self._normalize_func_ref(fname)
624
+ if not norm:
625
+ continue
626
+ key = (norm, 'state_entry', state.name, '')
627
+ if key in seen:
628
+ continue
629
+ seen.add(key)
630
+ call_map.setdefault(norm, []).append(RoleFuncCallSite(
631
+ norm, 'state_entry', state.name, '', state.name,
632
+ ))
633
+ for action in (getattr(state, 'exit', []) or []):
634
+ fname = _extract_rf_name(action)
635
+ if not fname:
636
+ continue
637
+ norm = self._normalize_func_ref(fname)
638
+ if not norm:
639
+ continue
640
+ key = (norm, 'state_exit', state.name, '')
641
+ if key in seen:
642
+ continue
643
+ seen.add(key)
644
+ call_map.setdefault(norm, []).append(RoleFuncCallSite(
645
+ norm, 'state_exit', state.name, '', state.name,
646
+ ))
647
+
648
+ # ---- 3. v2.2.5: Cell-level actions ----
649
+ if hasattr(state_machine, 'get_cell_keys'):
650
+ try:
651
+ cell_keys = state_machine.get_cell_keys()
652
+ except Exception:
653
+ cell_keys = []
654
+ for cell_key in cell_keys:
655
+ try:
656
+ source, event = cell_key
657
+ except Exception:
658
+ continue
659
+ for action in state_machine.get_actions_for_cell(
660
+ source, event):
661
+ rf = getattr(action, 'role_function', '') or ''
662
+ norm = self._normalize_func_ref(rf)
663
+ if not norm:
664
+ continue
665
+ trigger = getattr(action, 'trigger',
666
+ 'before_transitions')
667
+ kind = f'cell_action_{trigger}'
668
+ key = (norm, kind, source, event)
669
+ if key in seen:
670
+ continue
671
+ seen.add(key)
672
+ call_map.setdefault(norm, []).append(RoleFuncCallSite(
673
+ norm, kind, source, event, '',
674
+ ))
675
+
676
+ self._log_debug(
677
+ f"_collect_call_sites: {len(call_map)} funcs, "
678
+ f"{sum(len(v) for v in call_map.values())} call sites"
679
+ )
680
+ return call_map
681
+
682
+ def _get_call_sites_for_func(
683
+ self, func, call_map: Dict[str, List[RoleFuncCallSite]],
684
+ ) -> List[RoleFuncCallSite]:
685
+ qualified = getattr(func, 'qualified_name', None) or ''
686
+ if qualified:
687
+ cs = call_map.get(qualified, [])
688
+ if cs:
689
+ return cs
690
+ bare = getattr(func, 'name', '')
691
+ if bare:
692
+ return call_map.get(bare, [])
693
+ return []
694
+
695
+ def _format_call_sites_comment(self, call_sites) -> str:
696
+ T = self.CALL_SITES_COMMENT_TEMPLATES
697
+ if not call_sites:
698
+ return T['none']
699
+ kind_width = max(len(cs.kind) for cs in call_sites)
700
+ from_width = max(
701
+ len(self._state_enum(cs.from_state)) for cs in call_sites
702
+ )
703
+ parts = [T['header']]
704
+ for cs in call_sites:
705
+ from_str = self._state_enum(cs.from_state)
706
+ event_str = self._event_enum(cs.event)
707
+ target_str = (self._state_enum(cs.target)
708
+ if cs.target else '(not set)')
709
+ parts.append(T['line'].substitute(
710
+ kind=cs.kind,
711
+ kind_pad=' ' * (kind_width - len(cs.kind)),
712
+ from_state=from_str,
713
+ from_pad=' ' * (from_width - len(from_str)),
714
+ event=event_str,
715
+ target=target_str,
716
+ ))
717
+ return ''.join(parts)
718
+
719
+ # ================================================================
720
+ # v1.6 sec 9.6 #90 Option A: layer filter decision
721
+ # ================================================================
722
+ def _should_emit_implementation(self, func, call_map) -> bool:
723
+ namespace = getattr(func, 'namespace', '') or ''
724
+ layer = self.layer_name or ''
725
+
726
+ # 1a. Exact match
727
+ if namespace == layer:
728
+ return True
729
+
730
+ # 1b. v2.2.5: prefix match (App <-> Application, Drv <-> Driver)
731
+ if namespace and layer:
732
+ ns_l, ly_l = namespace.lower(), layer.lower()
733
+ if ly_l.startswith(ns_l) or ns_l.startswith(ly_l):
734
+ if min(len(ns_l), len(ly_l)) >= 3:
735
+ return True
736
+
737
+ # 2. Caller in this layer
738
+ qn = getattr(func, 'qualified_name', None) or ''
739
+ if qn and call_map.get(qn):
740
+ return True
741
+ bare = getattr(func, 'name', '') or ''
742
+ if bare and call_map.get(bare):
743
+ return True
744
+ return False
745
+
746
+ def _should_declare_here(self, func, call_map=None) -> bool:
747
+ """Decide whether to emit this function's declaration here.
748
+
749
+ [C-50 fix]
750
+ A function is declared in this layer's header if either:
751
+ 1. its namespace matches the layer (exact or 3+ char
752
+ prefix match), OR
753
+ 2. it is called from somewhere in this layer (call_map hit).
754
+ """
755
+ if not self.layer_name:
756
+ return True
757
+ name, namespace = self._resolve_name_and_namespace(func)
758
+ layer = self.layer_name
759
+
760
+ # 1a. Exact match
761
+ if namespace == layer:
762
+ return True
763
+
764
+ # 1b. Prefix match
765
+ ns_l, ly_l = namespace.lower(), layer.lower()
766
+ if min(len(ns_l), len(ly_l)) >= 3:
767
+ if ly_l.startswith(ns_l) or ns_l.startswith(ly_l):
768
+ return True
769
+
770
+ # 1c. [C-50 fix] Called from this layer
771
+ if call_map:
772
+ qn = getattr(func, 'qualified_name', None) or ''
773
+ if qn and call_map.get(qn):
774
+ return True
775
+ bare = getattr(func, 'name', '') or ''
776
+ if bare and call_map.get(bare):
777
+ return True
778
+
779
+ return False
780
+
781
+ # ================================================================
782
+ # Generation methods
783
+ # ================================================================
784
+ def generate_none_define(self) -> str:
785
+ T = self.NONE_DEFINE_TEMPLATES
786
+ return T['section_comment'] + T['define']
787
+
788
+ def generate_entry_struct(self) -> str:
789
+ T = self.ENTRY_STRUCT_TEMPLATES
790
+ return (
791
+ T['section_comment'] +
792
+ T['struct_typedef'].substitute(
793
+ state_type=self._state_type(),
794
+ event_type=self._event_type(),
795
+ entry_type=self._entry_struct_type(),
796
+ )
797
+ )
798
+
799
+ def generate_transition_id_prototype(self) -> str:
800
+ T = self.TRANSITION_ID_FUNC_TEMPLATES
801
+ return (
802
+ T['prototype_comment'] +
803
+ T['prototype'].substitute(
804
+ context_type=self._context_type(),
805
+ entry_type=self._entry_struct_type(),
806
+ )
807
+ )
808
+
809
+ def generate_transition_id_function(self) -> str:
810
+ T = self.TRANSITION_ID_FUNC_TEMPLATES
811
+ return (
812
+ T['definition_comment'] +
813
+ T['definition'].substitute(
814
+ context_type=self._context_type(),
815
+ entry_type=self._entry_struct_type(),
816
+ )
817
+ )
818
+
819
+ def generate_call_sites_table(self, func, call_sites) -> str:
820
+ if not call_sites:
821
+ return ""
822
+ short = self._get_short_name(func)
823
+ table_name = f"call_sites_{short}"
824
+ count_macro = f"CALL_SITES_{short}_COUNT"
825
+ unique_entries = []
826
+ seen = set()
827
+ for cs in call_sites:
828
+ k = cs.key()
829
+ if k in seen:
830
+ continue
831
+ seen.add(k)
832
+ unique_entries.append(cs)
833
+ if not unique_entries:
834
+ return ""
835
+ from_strs = [self._state_enum(cs.from_state)
836
+ for cs in unique_entries]
837
+ from_width = max(len(s) for s in from_strs)
838
+ T = self.CALL_SITE_TABLE_TEMPLATES
839
+ parts = [
840
+ T['table_header'].substitute(func_name=short),
841
+ T['table_open'].substitute(
842
+ struct_type=self._entry_struct_type(),
843
+ table_name=table_name,
844
+ ),
845
+ ]
846
+ for cs in unique_entries:
847
+ from_str = self._state_enum(cs.from_state)
848
+ event_str = self._event_enum(cs.event)
849
+ pad = ' ' * (from_width - len(from_str) + 1)
850
+ parts.append(T['entry'].substitute(
851
+ from_state=from_str,
852
+ event=pad + event_str,
853
+ ))
854
+ parts.append(T['table_close'])
855
+ parts.append(T['count_macro'].substitute(
856
+ count_macro=count_macro,
857
+ table_name=table_name,
858
+ ))
859
+ return ''.join(parts)
860
+
861
+ def generate_tail_user_section(self) -> str:
862
+ T = self.TAIL_USER_SECTION_TEMPLATES
863
+ return ''.join([
864
+ T['section_comment'],
865
+ T['marker_start'],
866
+ T['default_hint'],
867
+ T['marker_end'],
868
+ ])
869
+
870
+ def _format_var_comment(self, var) -> str:
871
+ description = getattr(var, 'description', '') or ''
872
+ unit = getattr(var, 'unit', '') or ''
873
+ if description and unit:
874
+ return f"{description} [{unit}]"
875
+ return description or unit or ""
876
+
877
+ def _generate_local_transition_members(self) -> str:
878
+ """Declarations for from_state / event only.
879
+
880
+ [v3.4 / R-10]
881
+ The (void) suppression lines were removed from this method
882
+ and moved into _generate_local_data_suppress, which is
883
+ emitted inside the user-code marker.
884
+ """
885
+ T = self.IMPLEMENTATION_TEMPLATES
886
+ parts = [
887
+ T['null_guard_header'],
888
+ T['null_guard_decl'].substitute(
889
+ c_type=self._state_type(),
890
+ member_name='from_state',
891
+ max_value=self._state_max(),
892
+ ),
893
+ T['null_guard_decl'].substitute(
894
+ c_type=self._event_type(),
895
+ member_name='event',
896
+ max_value=self._event_none(),
897
+ ),
898
+ T['null_guard_check'],
899
+ T['null_guard_assign'].substitute(member_name='from_state'),
900
+ T['null_guard_assign'].substitute(member_name='event'),
901
+ T['null_guard_close'],
902
+ T['blank'],
903
+ ]
904
+ return ''.join(parts)
905
+
906
+ def _generate_local_transition_id(self, func,
907
+ has_call_sites: bool) -> str:
908
+ """Declaration of transition_id only.
909
+
910
+ [v3.4 / R-10]
911
+ The (void)transition_id; line was removed from the template
912
+ and moved into _generate_local_data_suppress.
913
+ """
914
+ T = self.IMPLEMENTATION_TEMPLATES
915
+ if has_call_sites:
916
+ short = self._get_short_name(func)
917
+ table_arg = f"call_sites_{short}"
918
+ count_arg = f"(uint16_t)CALL_SITES_{short}_COUNT"
919
+ else:
920
+ table_arg = "NULL"
921
+ count_arg = "0"
922
+ return ''.join([
923
+ T['local_transition_id_header'],
924
+ T['local_transition_id_decl'].substitute(
925
+ table_arg=table_arg,
926
+ count_arg=count_arg,
927
+ ),
928
+ T['blank'],
929
+ ])
930
+
931
+ def _generate_local_data_decls(self, global_defs) -> str:
932
+ """Generate local pointer declarations to ctx->data.
933
+
934
+ [v2.5.2 / user-editable suppression]
935
+ Declarations only; the (void) suppression lines are emitted
936
+ by _generate_local_data_suppress inside the user marker.
937
+ """
938
+ if global_defs is None:
939
+ return ""
940
+ variables = getattr(global_defs, 'variables', []) or []
941
+ if not variables:
942
+ return ""
943
+ T = self.IMPLEMENTATION_TEMPLATES
944
+ parts = [T['local_data_header']]
945
+ for var in variables:
946
+ var_name = self.naming.sanitize_identifier(
947
+ getattr(var, 'name', 'unnamed')
948
+ )
949
+ c_type = self.mapper.map_type(
950
+ getattr(var, 'type', 'void')
951
+ )
952
+ comment = self._format_var_comment(var)
953
+ array_size = getattr(var, 'array_size', 0)
954
+ if array_size > 0:
955
+ parts.append(T['local_data_array'].substitute(
956
+ c_type=c_type, var_name=var_name,
957
+ comment=comment,
958
+ ))
959
+ else:
960
+ parts.append(T['local_data_pointer'].substitute(
961
+ c_type=c_type, var_name=var_name,
962
+ comment=comment,
963
+ ))
964
+ parts.append(T['blank'])
965
+ return ''.join(parts)
966
+
967
+ def _generate_local_data_suppress(self, global_defs,
968
+ include_transition_id: bool = True) -> str:
969
+ """Generate the (void) suppression block for all auto-declared
970
+ variables: from_state, event, transition_id, and either ctx or
971
+ ctx->data.* (depending on whether local data pointers exist).
972
+
973
+ [v3.4 / R-10]
974
+ Extended from v2.5.2 (which covered only ctx->data.*).
975
+ All suppressions now live inside the user-code marker so
976
+ users can delete individual lines once they start using the
977
+ corresponding variable. Regeneration preserves the user's
978
+ version via code_merger.
979
+ """
980
+ parts = []
981
+ # [v2.5.2] Explanatory header, printed once per role function.
982
+ parts.append(
983
+ ' /* --- auto-generated: unused-variable suppression ---\n'
984
+ ' * Delete each (void) line once you start using the\n'
985
+ ' * corresponding variable. Keeping them all is\n'
986
+ ' * harmless (no-op). */\n'
987
+ )
988
+ # transition-guard locals (always declared by the generator)
989
+ parts.append(
990
+ ' (void)from_state; /* suppress unused warning */\n')
991
+ parts.append(
992
+ ' (void)event; /* suppress unused warning */\n')
993
+ # transition_id (declared only when state_machine is available)
994
+ if include_transition_id:
995
+ parts.append(
996
+ ' (void)transition_id; /* suppress unused warning */\n')
997
+ # ctx / ctx->data.*
998
+ if self._has_local_data_pointers(global_defs):
999
+ for var in (getattr(global_defs, 'variables', []) or []):
1000
+ var_name = self.naming.sanitize_identifier(
1001
+ getattr(var, 'name', 'unnamed'))
1002
+ parts.append(
1003
+ f' (void){var_name};'
1004
+ f' /* suppress unused warning */\n')
1005
+ else:
1006
+ parts.append(
1007
+ ' (void)ctx; /* suppress unused argument warning */\n')
1008
+ parts.append('\n')
1009
+ return ''.join(parts)
1010
+
1011
+ def _generate_local_data_pointers(self, global_defs) -> str:
1012
+ """Legacy wrapper: delegate to _generate_local_data_decls."""
1013
+ return self._generate_local_data_decls(global_defs)
1014
+
1015
+ def _has_local_data_pointers(self, global_defs) -> bool:
1016
+ if global_defs is None:
1017
+ return False
1018
+ return bool(getattr(global_defs, 'variables', []) or [])
1019
+
1020
+ def _generate_local_retvar(self) -> str:
1021
+ T = self.IMPLEMENTATION_TEMPLATES
1022
+ return ''.join([
1023
+ T['local_ret_header'],
1024
+ T['local_ret_decl'],
1025
+ T['blank'],
1026
+ ])
1027
+
1028
+ def generate_declaration(self, func) -> str:
1029
+ T = self.DECLARATION_TEMPLATES
1030
+ parts = []
1031
+ title = self._resolve_comment_title(func)
1032
+ description = getattr(func, 'description', '')
1033
+ if description:
1034
+ parts.append(T['comment_with_desc'].substitute(
1035
+ title=title, description=description,
1036
+ ))
1037
+ else:
1038
+ parts.append(T['comment_no_desc'].substitute(title=title))
1039
+ parts.append(T['signature_open'].substitute(
1040
+ func_name=self._generate_function_name(func),
1041
+ ))
1042
+ parts.append(T['signature_args'].substitute(
1043
+ context_type=self._context_type(),
1044
+ ))
1045
+ parts.append(T['declaration_close'])
1046
+ return ''.join(parts)
1047
+
1048
+ def generate_implementation(self, func, global_defs=None,
1049
+ call_sites=None,
1050
+ include_transition_id: bool = True) -> str:
1051
+ T = self.IMPLEMENTATION_TEMPLATES
1052
+ parts = []
1053
+
1054
+ title = self._resolve_comment_title(func)
1055
+ description = getattr(func, 'description', '')
1056
+ call_sites_comment = self._format_call_sites_comment(
1057
+ call_sites or []
1058
+ )
1059
+
1060
+ if description:
1061
+ parts.append(T['comment_with_desc'].substitute(
1062
+ title=title, description=description,
1063
+ call_sites=call_sites_comment,
1064
+ ))
1065
+ else:
1066
+ parts.append(T['comment_no_desc'].substitute(
1067
+ title=title, call_sites=call_sites_comment,
1068
+ ))
1069
+
1070
+ parts.append(T['signature_open'].substitute(
1071
+ func_name=self._generate_function_name(func),
1072
+ ))
1073
+ parts.append(T['signature_args'].substitute(
1074
+ context_type=self._context_type(),
1075
+ ))
1076
+ parts.append(T['body_open'])
1077
+
1078
+ parts.append(self._generate_local_transition_members())
1079
+
1080
+ if include_transition_id:
1081
+ has_cs = bool(call_sites)
1082
+ parts.append(self._generate_local_transition_id(func, has_cs))
1083
+
1084
+ if self._has_local_data_pointers(global_defs):
1085
+ parts.append(self._generate_local_data_decls(global_defs))
1086
+
1087
+ parts.append(self._generate_local_retvar())
1088
+
1089
+ parts.append(T['todo_comment'].substitute(
1090
+ todo=self.strings['todo']
1091
+ ))
1092
+ parts.append(T['blank'])
1093
+
1094
+ marker_name = self._get_marker_name(func)
1095
+ parts.append(T['user_marker_start'].substitute(
1096
+ marker_name=marker_name
1097
+ ))
1098
+
1099
+ # [v3.4 / R-10] ALL (void) suppression lines live INSIDE the
1100
+ # user-code marker so the user can delete individual lines
1101
+ # once they start using the corresponding variable.
1102
+ parts.append(self._generate_local_data_suppress(
1103
+ global_defs,
1104
+ include_transition_id=include_transition_id,
1105
+ ))
1106
+
1107
+ parts.append(T['user_marker_hint'])
1108
+ parts.append(T['user_marker_end'].substitute(
1109
+ marker_name=marker_name
1110
+ ))
1111
+ parts.append(T['blank'])
1112
+
1113
+ parts.append(T['return_default'])
1114
+ parts.append(T['body_close'])
1115
+ return ''.join(parts)
1116
+
1117
+ def generate_call(self, func_name: str) -> str:
1118
+ if func_name.startswith("RoleFunc_"):
1119
+ full_name = func_name
1120
+ elif '.' in func_name:
1121
+ ns, name = func_name.split('.', 1)
1122
+ full_name = (f"RoleFunc_{ns}_"
1123
+ f"{self.naming.to_pascal_case(name)}")
1124
+ elif self.layer_name:
1125
+ full_name = (f"RoleFunc_{self.layer_name}_"
1126
+ f"{self.naming.to_pascal_case(func_name)}")
1127
+ else:
1128
+ full_name = (f"RoleFunc_"
1129
+ f"{self.naming.to_pascal_case(func_name)}")
1130
+ return f"{full_name}(transition, ctx)"
1131
+
1132
+ def generate_all_declarations(self, role_functions: List,
1133
+ state_machine=None) -> str:
1134
+ """Generate declarations for self-layer role functions only.
1135
+
1136
+ [v3.2 / MISRA 8.5 fix]
1137
+ Previously every layer emitted declarations for ALL role functions
1138
+ from the shared library, so `Driver.Init` was declared in
1139
+ Driver/statable_role_functions_Driver.h AND in
1140
+ Application/statable_role_functions_Application.h AND in
1141
+ Middleware/... -> MISRA 8.5 (declared more than once).
1142
+
1143
+ Now only functions whose namespace belongs to this layer are
1144
+ declared here.
1145
+ """
1146
+ unique_funcs = self._dedupe_by_name(role_functions)
1147
+ call_map = self._collect_call_sites(state_machine) \
1148
+ if state_machine is not None else {}
1149
+ parts = []
1150
+ for func in unique_funcs:
1151
+ if not self._should_declare_here(func, call_map):
1152
+ continue
1153
+ parts.append(self.generate_declaration(func))
1154
+ parts.append('\n')
1155
+ return ''.join(parts)
1156
+
1157
+ def generate_all_implementations(self, role_functions: List,
1158
+ state_machine=None,
1159
+ global_defs=None) -> str:
1160
+ """
1161
+ Generate the implementation set for this layer's file.
1162
+
1163
+ [v2.2.5]
1164
+ Filtering now includes:
1165
+ - Self-layer functions (namespace == layer_name or prefix match)
1166
+ - Functions called from this layer's transitions, state
1167
+ entry/exit, or cell actions
1168
+ """
1169
+ unique_funcs = self._dedupe_by_name(role_functions)
1170
+ include_transition_id = state_machine is not None
1171
+ call_map = self._collect_call_sites(state_machine) \
1172
+ if include_transition_id else {}
1173
+
1174
+ # v1.5 addition: detect undefined references (before filtering)
1175
+ if call_map:
1176
+ defined_keys = set()
1177
+ for f in unique_funcs:
1178
+ qn = getattr(f, 'qualified_name', None) \
1179
+ or getattr(f, 'name', '')
1180
+ if qn:
1181
+ defined_keys.add(qn)
1182
+ bare = getattr(f, 'name', '')
1183
+ if bare:
1184
+ defined_keys.add(bare)
1185
+ for ref in call_map.keys():
1186
+ if ref and ref not in defined_keys:
1187
+ self._log_debug(
1188
+ f"Undefined role function reference in "
1189
+ f"transitions: '{ref}' (not in role_functions)",
1190
+ 'warning',
1191
+ )
1192
+
1193
+ # v1.6 sec 9.6 #90 Option A: layer filter
1194
+ filtered_funcs = [
1195
+ f for f in unique_funcs
1196
+ if self._should_emit_implementation(f, call_map)
1197
+ ]
1198
+ skipped = len(unique_funcs) - len(filtered_funcs)
1199
+ if skipped:
1200
+ self._log_debug(
1201
+ f"generate_all_implementations: "
1202
+ f"layer='{self.layer_name}', "
1203
+ f"emitted={len(filtered_funcs)}, "
1204
+ f"skipped={skipped} "
1205
+ f"(not self-layer and no caller in this layer)"
1206
+ )
1207
+ parts = []
1208
+ if include_transition_id:
1209
+ parts.append(self.generate_none_define())
1210
+ parts.append('\n')
1211
+ parts.append(self.generate_entry_struct())
1212
+ parts.append('\n')
1213
+ parts.append(self.generate_transition_id_prototype())
1214
+ parts.append('\n')
1215
+ for func in filtered_funcs:
1216
+ call_sites = self._get_call_sites_for_func(func, call_map)
1217
+ table_code = self.generate_call_sites_table(
1218
+ func, call_sites
1219
+ )
1220
+ if table_code:
1221
+ parts.append(table_code)
1222
+ parts.append('\n')
1223
+ parts.append('\n')
1224
+ for func in filtered_funcs:
1225
+ call_sites = self._get_call_sites_for_func(func, call_map)
1226
+ parts.append(self.generate_implementation(
1227
+ func, global_defs, call_sites, include_transition_id,
1228
+ ))
1229
+ parts.append('\n')
1230
+ if include_transition_id:
1231
+ parts.append(self.generate_transition_id_function())
1232
+ parts.append(self.generate_tail_user_section())
1233
+ return ''.join(parts)
1234
+
1235
+ def _collect_args(self, func) -> List[tuple]:
1236
+ return [
1237
+ (
1238
+ 'transition',
1239
+ f'const TransitionContext_{self.layer_name}_t *'
1240
+ if self.layer_name else 'const TransitionContext_t *',
1241
+ 'transition context',
1242
+ ),
1243
+ ('ctx', 'SystemContext_t *', 'system context pointer'),
1244
+ ]