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.
- codegen/__init__.py +38 -0
- codegen/c_code_generator.py +1752 -0
- codegen/code_merger.py +371 -0
- codegen/code_templates.py +509 -0
- codegen/config.py +167 -0
- codegen/enum_generator.py +377 -0
- codegen/event_queue_generator.py +303 -0
- codegen/interrupt_generator.py +345 -0
- codegen/naming_convention.py +171 -0
- codegen/osal_generator.py +282 -0
- codegen/role_function_generator.py +1244 -0
- codegen/sample_data.py +269 -0
- codegen/state_actions_generator.py +395 -0
- codegen/struct_generator.py +721 -0
- codegen/timer_generator.py +299 -0
- codegen/transition_generator.py +907 -0
- codegen/type_mapper.py +69 -0
- codegen/validate/__init__.py +38 -0
- codegen/validate/change_actions.py +90 -0
- codegen/validate/change_applier.py +416 -0
- codegen/validate/clipboard_manager.py +47 -0
- codegen/validate/data/__init__.py +30 -0
- codegen/validate/data/action_definitions.py +200 -0
- codegen/validate/data/keywords.py +46 -0
- codegen/validate/data/prompt_templates.py +176 -0
- codegen/validate/data/validation_rules.py +205 -0
- codegen/validate/items/__init__.py +30 -0
- codegen/validate/items/base_validator.py +25 -0
- codegen/validate/items/cell_validator.py +275 -0
- codegen/validate/items/custom_type_validator.py +75 -0
- codegen/validate/items/event_validator.py +78 -0
- codegen/validate/items/flag_validator.py +74 -0
- codegen/validate/items/interrupt_validator.py +75 -0
- codegen/validate/items/queue_validator.py +73 -0
- codegen/validate/items/role_function_validator.py +175 -0
- codegen/validate/items/state_validator.py +88 -0
- codegen/validate/items/timer_validator.py +120 -0
- codegen/validate/items/transition_validator.py +113 -0
- codegen/validate/items/variable_validator.py +132 -0
- codegen/validate/logger.py +52 -0
- codegen/validate/models.py +177 -0
- codegen/validate/prompt_generator.py +724 -0
- codegen/validate/response_parser.py +154 -0
- codegen/validate/response_validator.py +275 -0
- codegen/validate/validation_dialog.py +322 -0
- codegen/validate/validator.py +94 -0
- codegen/variable_generator.py +419 -0
- statable/__init__.py +71 -0
- statable/__main__.py +49 -0
- statable/cli.py +251 -0
- statable/global_defs.py +256 -0
- statable/mermaid_gen.py +144 -0
- statable/model.py +426 -0
- statable/parser.py +64 -0
- statable/sample_data.py +435 -0
- statable/shared/__init__.py +24 -0
- statable/shared/condition_library.py +63 -0
- statable/shared/literal_library.py +66 -0
- statable/shared/role_function_library.py +150 -0
- statable/state_machine.py +121 -0
- statable/xml_io.py +1164 -0
- statable-3.1.0.dist-info/METADATA +151 -0
- statable-3.1.0.dist-info/RECORD +117 -0
- statable-3.1.0.dist-info/WHEEL +5 -0
- statable-3.1.0.dist-info/entry_points.txt +2 -0
- statable-3.1.0.dist-info/top_level.txt +3 -0
- statable_gui/__init__.py +23 -0
- statable_gui/action_edit_dialog.py +214 -0
- statable_gui/code_generation_dialog.py +486 -0
- statable_gui/code_generation_settings_dialog.py +522 -0
- statable_gui/common_widgets.py +537 -0
- statable_gui/condition_builder_dialog.py +500 -0
- statable_gui/condition_edit_dialog.py +123 -0
- statable_gui/condition_template_dialog.py +58 -0
- statable_gui/config.py +21 -0
- statable_gui/dialogs.py +439 -0
- statable_gui/event_definition_dialog.py +605 -0
- statable_gui/event_delivery_settings_dialog.py +191 -0
- statable_gui/event_queue_dialog.py +245 -0
- statable_gui/global_defs.py +167 -0
- statable_gui/global_defs_dialog.py +796 -0
- statable_gui/i18n/__init__.py +118 -0
- statable_gui/interrupt_handler_edit_dialog.py +810 -0
- statable_gui/layer_settings_dialog.py +180 -0
- statable_gui/libcntrl/__init__.py +23 -0
- statable_gui/libcntrl/condition_library.py +13 -0
- statable_gui/libcntrl/literal_library.py +13 -0
- statable_gui/libcntrl/literal_management_dialog.py +185 -0
- statable_gui/libcntrl/role_function_edit_dialog.py +145 -0
- statable_gui/libcntrl/role_function_library.py +13 -0
- statable_gui/literal_definition_dialog.py +62 -0
- statable_gui/logger.py +83 -0
- statable_gui/main_window.py +1380 -0
- statable_gui/matrix_table.py +413 -0
- statable_gui/preference_keys.py +18 -0
- statable_gui/preferences.py +74 -0
- statable_gui/role_function_dialog.py +244 -0
- statable_gui/sample_data.py +11 -0
- statable_gui/state_actions_dialog.py +398 -0
- statable_gui/symbol_picker.py +170 -0
- statable_gui/traceball.py +37 -0
- statable_gui/transition_editor_direct/__init__.py +15 -0
- statable_gui/transition_editor_direct/actions_tab.py +856 -0
- statable_gui/transition_editor_direct/code_widget.py +344 -0
- statable_gui/transition_editor_direct/coverage_analyzer.py +163 -0
- statable_gui/transition_editor_direct/dialog.py +335 -0
- statable_gui/transition_editor_direct/draft.py +270 -0
- statable_gui/transition_editor_direct/edit_dialogs.py +160 -0
- statable_gui/transition_editor_direct/overview_tab.py +173 -0
- statable_gui/transition_editor_direct/palette_widget.py +233 -0
- statable_gui/transition_editor_direct/relations_edit_dialog.py +156 -0
- statable_gui/transition_editor_direct/relations_tab.py +230 -0
- statable_gui/transition_editor_direct/system_global_dialog.py +52 -0
- statable_gui/transition_editor_direct/transition_actions_dialog.py +109 -0
- statable_gui/transition_editor_direct/transitions_tab.py +620 -0
- statable_gui/validation_dialog.py +285 -0
- 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
|
+
]
|