statable 3.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. codegen/__init__.py +38 -0
  2. codegen/c_code_generator.py +1752 -0
  3. codegen/code_merger.py +371 -0
  4. codegen/code_templates.py +509 -0
  5. codegen/config.py +167 -0
  6. codegen/enum_generator.py +377 -0
  7. codegen/event_queue_generator.py +303 -0
  8. codegen/interrupt_generator.py +345 -0
  9. codegen/naming_convention.py +171 -0
  10. codegen/osal_generator.py +282 -0
  11. codegen/role_function_generator.py +1244 -0
  12. codegen/sample_data.py +269 -0
  13. codegen/state_actions_generator.py +395 -0
  14. codegen/struct_generator.py +721 -0
  15. codegen/timer_generator.py +299 -0
  16. codegen/transition_generator.py +907 -0
  17. codegen/type_mapper.py +69 -0
  18. codegen/validate/__init__.py +38 -0
  19. codegen/validate/change_actions.py +90 -0
  20. codegen/validate/change_applier.py +416 -0
  21. codegen/validate/clipboard_manager.py +47 -0
  22. codegen/validate/data/__init__.py +30 -0
  23. codegen/validate/data/action_definitions.py +200 -0
  24. codegen/validate/data/keywords.py +46 -0
  25. codegen/validate/data/prompt_templates.py +176 -0
  26. codegen/validate/data/validation_rules.py +205 -0
  27. codegen/validate/items/__init__.py +30 -0
  28. codegen/validate/items/base_validator.py +25 -0
  29. codegen/validate/items/cell_validator.py +275 -0
  30. codegen/validate/items/custom_type_validator.py +75 -0
  31. codegen/validate/items/event_validator.py +78 -0
  32. codegen/validate/items/flag_validator.py +74 -0
  33. codegen/validate/items/interrupt_validator.py +75 -0
  34. codegen/validate/items/queue_validator.py +73 -0
  35. codegen/validate/items/role_function_validator.py +175 -0
  36. codegen/validate/items/state_validator.py +88 -0
  37. codegen/validate/items/timer_validator.py +120 -0
  38. codegen/validate/items/transition_validator.py +113 -0
  39. codegen/validate/items/variable_validator.py +132 -0
  40. codegen/validate/logger.py +52 -0
  41. codegen/validate/models.py +177 -0
  42. codegen/validate/prompt_generator.py +724 -0
  43. codegen/validate/response_parser.py +154 -0
  44. codegen/validate/response_validator.py +275 -0
  45. codegen/validate/validation_dialog.py +322 -0
  46. codegen/validate/validator.py +94 -0
  47. codegen/variable_generator.py +419 -0
  48. statable/__init__.py +71 -0
  49. statable/__main__.py +49 -0
  50. statable/cli.py +251 -0
  51. statable/global_defs.py +256 -0
  52. statable/mermaid_gen.py +144 -0
  53. statable/model.py +426 -0
  54. statable/parser.py +64 -0
  55. statable/sample_data.py +435 -0
  56. statable/shared/__init__.py +24 -0
  57. statable/shared/condition_library.py +63 -0
  58. statable/shared/literal_library.py +66 -0
  59. statable/shared/role_function_library.py +150 -0
  60. statable/state_machine.py +121 -0
  61. statable/xml_io.py +1164 -0
  62. statable-3.1.0.dist-info/METADATA +151 -0
  63. statable-3.1.0.dist-info/RECORD +117 -0
  64. statable-3.1.0.dist-info/WHEEL +5 -0
  65. statable-3.1.0.dist-info/entry_points.txt +2 -0
  66. statable-3.1.0.dist-info/top_level.txt +3 -0
  67. statable_gui/__init__.py +23 -0
  68. statable_gui/action_edit_dialog.py +214 -0
  69. statable_gui/code_generation_dialog.py +486 -0
  70. statable_gui/code_generation_settings_dialog.py +522 -0
  71. statable_gui/common_widgets.py +537 -0
  72. statable_gui/condition_builder_dialog.py +500 -0
  73. statable_gui/condition_edit_dialog.py +123 -0
  74. statable_gui/condition_template_dialog.py +58 -0
  75. statable_gui/config.py +21 -0
  76. statable_gui/dialogs.py +439 -0
  77. statable_gui/event_definition_dialog.py +605 -0
  78. statable_gui/event_delivery_settings_dialog.py +191 -0
  79. statable_gui/event_queue_dialog.py +245 -0
  80. statable_gui/global_defs.py +167 -0
  81. statable_gui/global_defs_dialog.py +796 -0
  82. statable_gui/i18n/__init__.py +118 -0
  83. statable_gui/interrupt_handler_edit_dialog.py +810 -0
  84. statable_gui/layer_settings_dialog.py +180 -0
  85. statable_gui/libcntrl/__init__.py +23 -0
  86. statable_gui/libcntrl/condition_library.py +13 -0
  87. statable_gui/libcntrl/literal_library.py +13 -0
  88. statable_gui/libcntrl/literal_management_dialog.py +185 -0
  89. statable_gui/libcntrl/role_function_edit_dialog.py +145 -0
  90. statable_gui/libcntrl/role_function_library.py +13 -0
  91. statable_gui/literal_definition_dialog.py +62 -0
  92. statable_gui/logger.py +83 -0
  93. statable_gui/main_window.py +1380 -0
  94. statable_gui/matrix_table.py +413 -0
  95. statable_gui/preference_keys.py +18 -0
  96. statable_gui/preferences.py +74 -0
  97. statable_gui/role_function_dialog.py +244 -0
  98. statable_gui/sample_data.py +11 -0
  99. statable_gui/state_actions_dialog.py +398 -0
  100. statable_gui/symbol_picker.py +170 -0
  101. statable_gui/traceball.py +37 -0
  102. statable_gui/transition_editor_direct/__init__.py +15 -0
  103. statable_gui/transition_editor_direct/actions_tab.py +856 -0
  104. statable_gui/transition_editor_direct/code_widget.py +344 -0
  105. statable_gui/transition_editor_direct/coverage_analyzer.py +163 -0
  106. statable_gui/transition_editor_direct/dialog.py +335 -0
  107. statable_gui/transition_editor_direct/draft.py +270 -0
  108. statable_gui/transition_editor_direct/edit_dialogs.py +160 -0
  109. statable_gui/transition_editor_direct/overview_tab.py +173 -0
  110. statable_gui/transition_editor_direct/palette_widget.py +233 -0
  111. statable_gui/transition_editor_direct/relations_edit_dialog.py +156 -0
  112. statable_gui/transition_editor_direct/relations_tab.py +230 -0
  113. statable_gui/transition_editor_direct/system_global_dialog.py +52 -0
  114. statable_gui/transition_editor_direct/transition_actions_dialog.py +109 -0
  115. statable_gui/transition_editor_direct/transitions_tab.py +620 -0
  116. statable_gui/validation_dialog.py +285 -0
  117. statable_gui/widgets.py +1315 -0
codegen/code_merger.py ADDED
@@ -0,0 +1,371 @@
1
+ # codegen/code_merger.py
2
+ """
3
+ Merging generated code with existing code.
4
+ Updates auto-generated code while preserving user-edited sections.
5
+
6
+ Supported markers:
7
+ - File-level user area: [[STABLE_USER_CODE_START/END]]
8
+ - Function-level user area: [[STABLE_USER_CODE_START:<name>/END:<name>]]
9
+ - RoleFunc_XXX uses <name> in "Driver_Init" form (includes layer)
10
+ - ISR_XXX uses <name> in "TIMER0" form (no layer)
11
+ - File-tail user area: [[STABLE_USER_CODE_TAIL_START/END]]
12
+
13
+ Version History
14
+ ---------------
15
+ v2.0 - inject_file_user_code: if an existing START/END block is present,
16
+ replace its contents. Old implementation always inserted a new
17
+ block after the includes, leaving the original block as dead code
18
+ and the placeholder behind.
19
+
20
+ v2.1 - Idempotency fix. The replacement pattern previously captured
21
+ the trailing newline into group(2), so each merge added one
22
+ extra newline per marker (4 markers -> +4 chars per merge).
23
+ The new pattern replaces the ENTIRE block (START..END) with a
24
+ freshly constructed block (START\\n user_code \\n END), and
25
+ uses a callable replacement to avoid re.sub escape handling.
26
+ Verified by tests/test_v2_4_p1_merge.py §3.
27
+ """
28
+
29
+ import re
30
+ import os
31
+ import logging
32
+ from typing import Dict, List, Optional, Tuple, Callable
33
+
34
+ logger = logging.getLogger(__name__)
35
+
36
+
37
+ class CodeMerger:
38
+ """Merge class for generated code and existing code"""
39
+
40
+ # Marker definitions
41
+ MARKERS = {
42
+ 'file_user_start': '/* [[STABLE_USER_CODE_START]] */',
43
+ 'file_user_end': '/* [[STABLE_USER_CODE_END]] */',
44
+ 'func_user_start': '/* [[STABLE_USER_CODE_START:{func_name}]] */',
45
+ 'func_user_end': '/* [[STABLE_USER_CODE_END:{func_name}]] */',
46
+ 'auto_start': '/* [[STABLE_AUTO_GENERATED_START]] */',
47
+ 'auto_end': '/* [[STABLE_AUTO_GENERATED_END]] */',
48
+ # File-tail user area
49
+ 'file_tail_user_start': '/* [[STABLE_USER_CODE_TAIL_START]] */',
50
+ 'file_tail_user_end': '/* [[STABLE_USER_CODE_TAIL_END]] */',
51
+ }
52
+
53
+ # Function name extraction patterns (RoleFunc_ / ISR_)
54
+ FUNC_NAME_PATTERNS = [
55
+ r'RoleFunc_(\w+)\s*\(', # RoleFunc_Driver_Init(
56
+ r'ISR_(\w+)\s*\(', # ISR_TIMER0(
57
+ ]
58
+
59
+ def __init__(self):
60
+ self.markers = self.MARKERS
61
+
62
+ def _log_debug(self, message, level='debug'):
63
+ log_func = getattr(logger, level, logger.debug)
64
+ log_func(message)
65
+
66
+ # ===== Extraction =====
67
+ def extract_file_user_code(self, existing_content: str) -> str:
68
+ """Extract file-level user code (contents between START and END)."""
69
+ start = self.markers['file_user_start']
70
+ end = self.markers['file_user_end']
71
+
72
+ pattern = rf'{re.escape(start)}\s*\n(.*?)\n\s*{re.escape(end)}'
73
+ match = re.search(pattern, existing_content, re.DOTALL)
74
+
75
+ if match:
76
+ self._log_debug("Extracted file user code")
77
+ return match.group(1)
78
+ return ""
79
+
80
+ def extract_func_user_code(self, existing_content: str,
81
+ func_name: str) -> str:
82
+ """Extract per-function user code."""
83
+ start = self.markers['func_user_start'].format(func_name=func_name)
84
+ end = self.markers['func_user_end'].format(func_name=func_name)
85
+
86
+ pattern = rf'{re.escape(start)}\s*\n(.*?)\n\s*{re.escape(end)}'
87
+ match = re.search(pattern, existing_content, re.DOTALL)
88
+
89
+ if match:
90
+ self._log_debug(f"Extracted user code for function {func_name}")
91
+ return match.group(1)
92
+ return ""
93
+
94
+ def extract_all_func_user_codes(self, existing_content: str
95
+ ) -> Dict[str, str]:
96
+ """
97
+ Extract all per-function user codes.
98
+
99
+ Supports both RoleFunc_XXX and ISR_XXX.
100
+ - RoleFunc_Driver_Init -> key "Driver_Init"
101
+ - ISR_TIMER0 -> key "TIMER0"
102
+ """
103
+ func_user_codes = {}
104
+ seen = set()
105
+
106
+ for pattern in self.FUNC_NAME_PATTERNS:
107
+ for match in re.finditer(pattern, existing_content):
108
+ func_name = match.group(1)
109
+ if func_name in seen:
110
+ continue
111
+ seen.add(func_name)
112
+ user_code = self.extract_func_user_code(
113
+ existing_content, func_name
114
+ )
115
+ if user_code:
116
+ func_user_codes[func_name] = user_code
117
+
118
+ self._log_debug(
119
+ f"extract_all_func_user_codes: {len(func_user_codes)} funcs"
120
+ )
121
+ return func_user_codes
122
+
123
+ def extract_file_tail_user_code(self, existing_content: str) -> str:
124
+ """Extract file-tail user code."""
125
+ start = self.markers['file_tail_user_start']
126
+ end = self.markers['file_tail_user_end']
127
+
128
+ pattern = rf'{re.escape(start)}\s*\n(.*?)\n\s*{re.escape(end)}'
129
+ match = re.search(pattern, existing_content, re.DOTALL)
130
+
131
+ if match:
132
+ self._log_debug("Extracted tail user code")
133
+ return match.group(1)
134
+ return ""
135
+
136
+ # ===== Helper: block replacement (idempotent) =====
137
+ @staticmethod
138
+ def _replace_block(text: str, start_marker: str, end_marker: str,
139
+ user_code: str) -> Tuple[str, int]:
140
+ """
141
+ Replace the contents of an existing START..END block with a
142
+ canonical block:
143
+
144
+ START
145
+ <user_code>
146
+ END
147
+
148
+ Returns (new_text, replaced_count).
149
+ The replacement uses a callable to bypass re.sub escape handling.
150
+ """
151
+ pattern = re.compile(
152
+ rf'{re.escape(start_marker)}.*?{re.escape(end_marker)}',
153
+ re.DOTALL,
154
+ )
155
+ canonical = f"{start_marker}\n{user_code}\n{end_marker}"
156
+ new_text, count = pattern.subn(
157
+ lambda m: canonical, text, count=1,
158
+ )
159
+ return new_text, count
160
+
161
+ # ===== Injection =====
162
+ def inject_file_user_code(self, generated_content: str,
163
+ user_code: str) -> str:
164
+ """
165
+ Inject file-level user code into the generated code.
166
+
167
+ [v2.0]
168
+ If an existing START/END block is present, replace its contents.
169
+ Old: always inserted a new block after includes -> the original
170
+ block became dead code.
171
+ New: replace existing block -> always exactly one block.
172
+
173
+ [v2.1]
174
+ Idempotency fix. The whole block (START..END) is now replaced
175
+ with a freshly constructed one, so repeated merges do not
176
+ accumulate extra newlines.
177
+ """
178
+ if not user_code:
179
+ return generated_content
180
+
181
+ start = self.markers['file_user_start']
182
+ end = self.markers['file_user_end']
183
+
184
+ # ---- 1. Replace if existing block found ----
185
+ result, count = self._replace_block(
186
+ generated_content, start, end, user_code,
187
+ )
188
+ if count:
189
+ self._log_debug("Replaced file user code")
190
+ return result
191
+
192
+ # ---- 2. No block: insert new one after includes ----
193
+ canonical = f"{start}\n{user_code}\n{end}"
194
+ lines = generated_content.split('\n')
195
+ result_lines = []
196
+ injected = False
197
+ last_include_idx = -1
198
+
199
+ for i, line in enumerate(lines):
200
+ if line.startswith('#include'):
201
+ last_include_idx = i
202
+
203
+ if last_include_idx >= 0:
204
+ for i, line in enumerate(lines):
205
+ result_lines.append(line)
206
+ if i == last_include_idx:
207
+ result_lines.append("")
208
+ result_lines.append(canonical)
209
+ injected = True
210
+
211
+ if not injected:
212
+ result_lines.insert(0, canonical)
213
+
214
+ self._log_debug("Inserted new file user code block")
215
+ return '\n'.join(result_lines)
216
+
217
+ def inject_func_user_code(self, generated_content: str,
218
+ func_name: str, user_code: str) -> str:
219
+ """
220
+ Replace existing per-function marker block with user_code.
221
+
222
+ - Assumes the generated code already has the marker; replaces
223
+ the content between them.
224
+ - If the marker is missing, does nothing (no new insertion).
225
+ """
226
+ if not user_code:
227
+ return generated_content
228
+
229
+ start = self.markers['func_user_start'].format(func_name=func_name)
230
+ end = self.markers['func_user_end'].format(func_name=func_name)
231
+
232
+ result, count = self._replace_block(
233
+ generated_content, start, end, user_code,
234
+ )
235
+ if count:
236
+ self._log_debug(f"Injected user code for function {func_name}")
237
+ else:
238
+ self._log_debug(
239
+ f"Marker not found for function {func_name}", 'warning'
240
+ )
241
+ return result
242
+
243
+ def inject_file_tail_user_code(self, generated_content: str,
244
+ user_code: str) -> str:
245
+ """Inject user code into the file-tail marker (replace)."""
246
+ if not user_code:
247
+ return generated_content
248
+
249
+ start = self.markers['file_tail_user_start']
250
+ end = self.markers['file_tail_user_end']
251
+
252
+ result, count = self._replace_block(
253
+ generated_content, start, end, user_code,
254
+ )
255
+ if count:
256
+ self._log_debug("Injected tail user code")
257
+ else:
258
+ self._log_debug("Tail marker not found", 'warning')
259
+ return result
260
+
261
+ # ===== Merge =====
262
+ def merge_file(self, generated_content: str,
263
+ existing_content: Optional[str]) -> str:
264
+ """Merge generated code with existing code"""
265
+ if existing_content is None or not existing_content.strip():
266
+ self._log_debug("No existing code, using generated code as-is")
267
+ return generated_content
268
+
269
+ self._log_debug("Starting merge")
270
+
271
+ # Extract user codes
272
+ file_user_code = self.extract_file_user_code(existing_content)
273
+ func_user_codes = self.extract_all_func_user_codes(existing_content)
274
+ tail_user_code = self.extract_file_tail_user_code(existing_content)
275
+
276
+ self._log_debug(f"File user code: {len(file_user_code)} chars")
277
+ self._log_debug(f"Function user codes: {len(func_user_codes)} funcs")
278
+ self._log_debug(f"Tail user code: {len(tail_user_code)} chars")
279
+
280
+ # Inject user codes into generated code
281
+ result = generated_content
282
+
283
+ result = self.inject_file_user_code(result, file_user_code)
284
+
285
+ for func_name, user_code in func_user_codes.items():
286
+ result = self.inject_func_user_code(
287
+ result, func_name, user_code
288
+ )
289
+
290
+ result = self.inject_file_tail_user_code(result, tail_user_code)
291
+
292
+ return result
293
+
294
+ # Folder structure aware version
295
+ def merge_all_files(self, generated_files: Dict[str, str],
296
+ existing_dir: str,
297
+ path_resolver: Optional[Callable] = None,
298
+ layer_name: str = '') -> Dict[str, str]:
299
+ """
300
+ Merge all files (folder structure aware).
301
+
302
+ Args:
303
+ generated_files: generated file dictionary
304
+ existing_dir: base directory for existing file lookup
305
+ path_resolver: function returning relative path
306
+ (filename, layer_name) -> rel_path
307
+ If None, use filename as-is
308
+ layer_name: layer name (for by_layer)
309
+ """
310
+ self._log_debug(f"Starting merge all files: {existing_dir}")
311
+ merged_files = {}
312
+
313
+ for filename, generated_content in generated_files.items():
314
+ # Path resolution
315
+ if path_resolver is not None:
316
+ try:
317
+ rel_path = path_resolver(filename, layer_name)
318
+ except Exception as e:
319
+ self._log_debug(
320
+ f"path_resolver failed for {filename}: {e}",
321
+ 'warning'
322
+ )
323
+ rel_path = filename
324
+ else:
325
+ rel_path = filename
326
+
327
+ existing_path = os.path.join(existing_dir, rel_path)
328
+
329
+ if os.path.exists(existing_path):
330
+ self._log_debug(f"Existing file found: {existing_path}")
331
+ with open(existing_path, 'r', encoding='utf-8') as f:
332
+ existing_content = f.read()
333
+ else:
334
+ self._log_debug(f"No existing file: {existing_path}")
335
+ existing_content = None
336
+
337
+ merged_files[filename] = self.merge_file(
338
+ generated_content, existing_content,
339
+ )
340
+
341
+ return merged_files
342
+
343
+ # ===== Marker existence =====
344
+ def has_user_code(self, content: str) -> bool:
345
+ """Whether the content contains user code"""
346
+ return self.markers['file_user_start'] in content
347
+
348
+ def has_func_user_code(self, content: str, func_name: str) -> bool:
349
+ """Whether a specific function contains user code"""
350
+ start = self.markers['func_user_start'].format(func_name=func_name)
351
+ return start in content
352
+
353
+ def get_user_code_summary(self, content: str) -> Dict[str, int]:
354
+ """Get user code summary"""
355
+ summary = {
356
+ 'file_user_code': 0,
357
+ 'func_user_codes': 0,
358
+ 'file_tail_user_code': 0,
359
+ }
360
+
361
+ if self.has_user_code(content):
362
+ file_code = self.extract_file_user_code(content)
363
+ summary['file_user_code'] = len(file_code)
364
+
365
+ func_codes = self.extract_all_func_user_codes(content)
366
+ summary['func_user_codes'] = len(func_codes)
367
+
368
+ tail_code = self.extract_file_tail_user_code(content)
369
+ summary['file_tail_user_code'] = len(tail_code)
370
+
371
+ return summary