consync 0.1.1__tar.gz → 2.0.0__tar.gz
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.
- {consync-0.1.1 → consync-2.0.0}/PKG-INFO +1 -1
- {consync-0.1.1 → consync-2.0.0}/consync/__init__.py +1 -1
- {consync-0.1.1 → consync-2.0.0}/consync/config.py +2 -0
- {consync-0.1.1 → consync-2.0.0}/consync/models.py +3 -0
- {consync-0.1.1 → consync-2.0.0}/consync/parsers/__init__.py +1 -1
- consync-2.0.0/consync/parsers/c_struct_table.py +615 -0
- {consync-0.1.1 → consync-2.0.0}/consync/renderers/__init__.py +1 -0
- consync-2.0.0/consync/renderers/c_struct_table.py +207 -0
- {consync-0.1.1 → consync-2.0.0}/consync/sync.py +184 -13
- {consync-0.1.1 → consync-2.0.0}/pyproject.toml +1 -1
- consync-2.0.0/tests/test_c_struct_table.py +474 -0
- {consync-0.1.1 → consync-2.0.0}/.github/CODEOWNERS +0 -0
- {consync-0.1.1 → consync-2.0.0}/.github/copilot-instructions.md +0 -0
- {consync-0.1.1 → consync-2.0.0}/.github/dependabot.yml +0 -0
- {consync-0.1.1 → consync-2.0.0}/.github/workflows/ci.yml +0 -0
- {consync-0.1.1 → consync-2.0.0}/.github/workflows/codeql.yml +0 -0
- {consync-0.1.1 → consync-2.0.0}/.github/workflows/publish.yml +0 -0
- {consync-0.1.1 → consync-2.0.0}/.github/workflows/release.yml +0 -0
- {consync-0.1.1 → consync-2.0.0}/.gitignore +0 -0
- {consync-0.1.1 → consync-2.0.0}/CLAUDE.md +0 -0
- {consync-0.1.1 → consync-2.0.0}/CONTRIBUTING.md +0 -0
- {consync-0.1.1 → consync-2.0.0}/FAQ.md +0 -0
- {consync-0.1.1 → consync-2.0.0}/LICENSE +0 -0
- {consync-0.1.1 → consync-2.0.0}/README.md +0 -0
- {consync-0.1.1 → consync-2.0.0}/SECURITY.md +0 -0
- {consync-0.1.1 → consync-2.0.0}/TODO.md +0 -0
- {consync-0.1.1 → consync-2.0.0}/assets/demo.gif +0 -0
- {consync-0.1.1 → consync-2.0.0}/assets/demo.tape +0 -0
- {consync-0.1.1 → consync-2.0.0}/consync/backup.py +0 -0
- {consync-0.1.1 → consync-2.0.0}/consync/cli.py +0 -0
- {consync-0.1.1 → consync-2.0.0}/consync/hooks.py +0 -0
- {consync-0.1.1 → consync-2.0.0}/consync/lock.py +0 -0
- {consync-0.1.1 → consync-2.0.0}/consync/logging_config.py +0 -0
- {consync-0.1.1 → consync-2.0.0}/consync/parsers/c_header.py +0 -0
- {consync-0.1.1 → consync-2.0.0}/consync/parsers/csv_parser.py +0 -0
- {consync-0.1.1 → consync-2.0.0}/consync/parsers/json_parser.py +0 -0
- {consync-0.1.1 → consync-2.0.0}/consync/parsers/toml_parser.py +0 -0
- {consync-0.1.1 → consync-2.0.0}/consync/parsers/xlsx.py +0 -0
- {consync-0.1.1 → consync-2.0.0}/consync/precision.py +0 -0
- {consync-0.1.1 → consync-2.0.0}/consync/renderers/c_header.py +0 -0
- {consync-0.1.1 → consync-2.0.0}/consync/renderers/csharp.py +0 -0
- {consync-0.1.1 → consync-2.0.0}/consync/renderers/csv_renderer.py +0 -0
- {consync-0.1.1 → consync-2.0.0}/consync/renderers/json_renderer.py +0 -0
- {consync-0.1.1 → consync-2.0.0}/consync/renderers/python_const.py +0 -0
- {consync-0.1.1 → consync-2.0.0}/consync/renderers/rust_const.py +0 -0
- {consync-0.1.1 → consync-2.0.0}/consync/renderers/verilog.py +0 -0
- {consync-0.1.1 → consync-2.0.0}/consync/renderers/vhdl.py +0 -0
- {consync-0.1.1 → consync-2.0.0}/consync/state.py +0 -0
- {consync-0.1.1 → consync-2.0.0}/consync/validators.py +0 -0
- {consync-0.1.1 → consync-2.0.0}/consync/watcher.py +0 -0
- {consync-0.1.1 → consync-2.0.0}/examples/fpga/.consync.yaml +0 -0
- {consync-0.1.1 → consync-2.0.0}/examples/fpga/design_params.csv +0 -0
- {consync-0.1.1 → consync-2.0.0}/examples/hardware/.consync.yaml +0 -0
- {consync-0.1.1 → consync-2.0.0}/examples/hardware/constants.csv +0 -0
- {consync-0.1.1 → consync-2.0.0}/examples/multilang/.consync.yaml +0 -0
- {consync-0.1.1 → consync-2.0.0}/examples/multilang/constants.json +0 -0
- {consync-0.1.1 → consync-2.0.0}/npm/.npmrc +0 -0
- {consync-0.1.1 → consync-2.0.0}/npm/LICENSE +0 -0
- {consync-0.1.1 → consync-2.0.0}/npm/README.md +0 -0
- {consync-0.1.1 → consync-2.0.0}/npm/bin/consync.js +0 -0
- {consync-0.1.1 → consync-2.0.0}/npm/package.json +0 -0
- {consync-0.1.1 → consync-2.0.0}/npm/scripts/install.js +0 -0
- {consync-0.1.1 → consync-2.0.0}/tests/__init__.py +0 -0
- {consync-0.1.1 → consync-2.0.0}/tests/test_arrays.py +0 -0
- {consync-0.1.1 → consync-2.0.0}/tests/test_cli.py +0 -0
- {consync-0.1.1 → consync-2.0.0}/tests/test_embedded.py +0 -0
- {consync-0.1.1 → consync-2.0.0}/tests/test_parsers.py +0 -0
- {consync-0.1.1 → consync-2.0.0}/tests/test_precision.py +0 -0
- {consync-0.1.1 → consync-2.0.0}/tests/test_renderers.py +0 -0
- {consync-0.1.1 → consync-2.0.0}/tests/test_safety.py +0 -0
- {consync-0.1.1 → consync-2.0.0}/tests/test_sync.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: consync
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 2.0.0
|
|
4
4
|
Summary: Bidirectional sync between spreadsheets and source code constants — with full decimal precision.
|
|
5
5
|
Project-URL: Homepage, https://github.com/naveenkumarbaskaran/consync
|
|
6
6
|
Project-URL: Repository, https://github.com/naveenkumarbaskaran/consync
|
|
@@ -116,6 +116,8 @@ def _parse_mapping(raw: dict[str, Any], config_dir: Path) -> MappingConfig:
|
|
|
116
116
|
output_style=raw.get("output_style", "const"),
|
|
117
117
|
static_const=raw.get("static_const", False),
|
|
118
118
|
typed_ints=raw.get("typed_ints", True),
|
|
119
|
+
parser_options=raw.get("parser_options", {}),
|
|
120
|
+
renderer_options=raw.get("renderer_options", {}),
|
|
119
121
|
validators=raw.get("validators", {}),
|
|
120
122
|
)
|
|
121
123
|
|
|
@@ -90,6 +90,9 @@ class MappingConfig:
|
|
|
90
90
|
output_style: str = "const" # "const" | "define" — #define vs const declaration
|
|
91
91
|
static_const: bool = False # emit "static const" to avoid linker duplicates
|
|
92
92
|
typed_ints: bool = True # use uint32_t/int32_t instead of plain int/double for integers
|
|
93
|
+
# Parser/renderer options (format-specific kwargs)
|
|
94
|
+
parser_options: dict = field(default_factory=dict) # passed to parser as **kwargs
|
|
95
|
+
renderer_options: dict = field(default_factory=dict) # passed to renderer as **kwargs
|
|
93
96
|
# Validation hooks
|
|
94
97
|
validators: dict = field(default_factory=dict) # {name: {min:, max:, type:, pattern:, ...}}
|
|
95
98
|
|
|
@@ -36,4 +36,4 @@ def list_parsers() -> list[str]:
|
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
# Import all parser modules to trigger registration
|
|
39
|
-
from consync.parsers import xlsx, csv_parser, json_parser, toml_parser, c_header # noqa: E402, F401
|
|
39
|
+
from consync.parsers import xlsx, csv_parser, json_parser, toml_parser, c_header, c_struct_table # noqa: E402, F401
|
|
@@ -0,0 +1,615 @@
|
|
|
1
|
+
"""C struct table parser — reads struct array initializers from .c/.h files.
|
|
2
|
+
|
|
3
|
+
Handles complex embedded-style constant tables like:
|
|
4
|
+
|
|
5
|
+
static const MyStruct LUT[COUNT] = {
|
|
6
|
+
/* Label1 */ {{val1, val2}, val3, ...},
|
|
7
|
+
/* Label2 */ {{val1, val2}, val3, ...},
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
Features:
|
|
11
|
+
- Extracts row labels from leading comments (/* Label */)
|
|
12
|
+
- Parses nested brace initializers with field mapping
|
|
13
|
+
- Handles #if/#elif/#endif conditional blocks (extracts active variant)
|
|
14
|
+
- Supports C float suffixes (1.0F), hex literals (0xFF), unsigned (5u)
|
|
15
|
+
- Preserves computed expressions as string constants (e.g., "0.25F / NF_PSC_FREQUENCY")
|
|
16
|
+
- Separates literal numeric values from expression-based values
|
|
17
|
+
|
|
18
|
+
Configuration (in .consync.yaml):
|
|
19
|
+
source_format: c_struct_table
|
|
20
|
+
parser_options:
|
|
21
|
+
fields: [R_Phase, L_d, L_q, Psi, J, Imax, Tmax, NPpair]
|
|
22
|
+
variant: DPB # selects #elif block (matches RBFS_PscMotorParameter_DPB)
|
|
23
|
+
table_var: PSC_HWVarParLUT # variable name of the struct array
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import re
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
|
|
31
|
+
from consync.models import Constant
|
|
32
|
+
from consync.parsers import register
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# Regex to match row labels in comments like /* BWA NI S4 */ or /* EMB 12V 35KN */
|
|
36
|
+
_ROW_LABEL_RE = re.compile(r"/\*\s*(.+?)\s*\*/")
|
|
37
|
+
|
|
38
|
+
# Regex to match a C numeric literal (int or float, with optional suffix)
|
|
39
|
+
_NUMERIC_LITERAL_RE = re.compile(
|
|
40
|
+
r"^[+-]?"
|
|
41
|
+
r"(?:0[xX][0-9a-fA-F]+|" # hex
|
|
42
|
+
r"\d+\.?\d*(?:[eE][+-]?\d+)?)" # decimal / float / scientific
|
|
43
|
+
r"[fFlLuU]*$" # C type suffixes
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Regex to detect #if / #elif lines with variant names
|
|
47
|
+
_PREPROC_IF_RE = re.compile(
|
|
48
|
+
r"^\s*#\s*(?:el)?if\s*\(.*?==\s*(\w+)\s*\)"
|
|
49
|
+
)
|
|
50
|
+
_PREPROC_ENDIF_RE = re.compile(r"^\s*#\s*endif")
|
|
51
|
+
_PREPROC_ELIF_RE = re.compile(r"^\s*#\s*elif")
|
|
52
|
+
|
|
53
|
+
# Matches the start of a static const struct array declaration
|
|
54
|
+
_TABLE_DECL_RE = re.compile(
|
|
55
|
+
r"^\s*(?:static\s+)?(?:const\s+)?\w+\s+(\w+)\s*\["
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _strip_c_suffix(token: str) -> str:
|
|
60
|
+
"""Strip C numeric literal suffixes like F, f, L, u, U.
|
|
61
|
+
|
|
62
|
+
Does NOT strip from hex literals (0x...) since hex digits overlap with suffixes.
|
|
63
|
+
"""
|
|
64
|
+
t = token.strip()
|
|
65
|
+
# Don't strip from hex — 'F' is a valid hex digit
|
|
66
|
+
if t.startswith(("0x", "0X", "+0x", "+0X", "-0x", "-0X")):
|
|
67
|
+
return t
|
|
68
|
+
return t.rstrip("fFlLuU")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _is_numeric_literal(token: str) -> bool:
|
|
72
|
+
"""Check if a token is a plain C numeric literal (not an expression)."""
|
|
73
|
+
stripped = _strip_c_suffix(token.strip())
|
|
74
|
+
return bool(_NUMERIC_LITERAL_RE.match(stripped))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _parse_numeric(token: str) -> float | int:
|
|
78
|
+
"""Parse a C numeric literal to Python number."""
|
|
79
|
+
stripped = _strip_c_suffix(token.strip())
|
|
80
|
+
if stripped.startswith(("0x", "0X")):
|
|
81
|
+
return int(stripped, 16)
|
|
82
|
+
if "." in stripped or "e" in stripped.lower():
|
|
83
|
+
return float(stripped)
|
|
84
|
+
return int(stripped)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _tokenize_brace_values(text: str) -> list[str]:
|
|
88
|
+
"""Tokenize a brace-enclosed initializer into top-level values.
|
|
89
|
+
|
|
90
|
+
Handles nested braces: {1.0, {2.0, 3.0}, 4.0} → ['1.0', '{2.0, 3.0}', '4.0']
|
|
91
|
+
"""
|
|
92
|
+
tokens: list[str] = []
|
|
93
|
+
depth = 0
|
|
94
|
+
current = ""
|
|
95
|
+
|
|
96
|
+
for ch in text:
|
|
97
|
+
if ch == "{":
|
|
98
|
+
if depth > 0:
|
|
99
|
+
current += ch
|
|
100
|
+
depth += 1
|
|
101
|
+
elif ch == "}":
|
|
102
|
+
depth -= 1
|
|
103
|
+
if depth > 0:
|
|
104
|
+
current += ch
|
|
105
|
+
elif depth == 0 and current.strip():
|
|
106
|
+
tokens.append(current.strip())
|
|
107
|
+
current = ""
|
|
108
|
+
elif ch == "," and depth == 1:
|
|
109
|
+
if current.strip():
|
|
110
|
+
tokens.append(current.strip())
|
|
111
|
+
current = ""
|
|
112
|
+
else:
|
|
113
|
+
if depth >= 1:
|
|
114
|
+
current += ch
|
|
115
|
+
|
|
116
|
+
# Handle remaining (for cases without trailing comma)
|
|
117
|
+
if current.strip():
|
|
118
|
+
tokens.append(current.strip())
|
|
119
|
+
|
|
120
|
+
return tokens
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _flatten_values(text: str) -> list[str]:
|
|
124
|
+
"""Recursively flatten nested brace initializers into a flat list of scalar values.
|
|
125
|
+
|
|
126
|
+
Input is the raw content INSIDE the outermost braces.
|
|
127
|
+
{1.0, {2.0, 3.0}, 4.0} → ['1.0', '2.0', '3.0', '4.0']
|
|
128
|
+
Scalars at any depth are collected. Sub-braces are recursed into.
|
|
129
|
+
"""
|
|
130
|
+
# Tokenize at the current level (comma-separated, respecting brace depth)
|
|
131
|
+
tokens = _tokenize_at_level(text)
|
|
132
|
+
result: list[str] = []
|
|
133
|
+
for t in tokens:
|
|
134
|
+
t = t.strip()
|
|
135
|
+
if not t:
|
|
136
|
+
continue
|
|
137
|
+
if t.startswith("{"):
|
|
138
|
+
# Recurse into the brace content
|
|
139
|
+
inner = t[1:-1] if t.endswith("}") else t[1:]
|
|
140
|
+
result.extend(_flatten_values(inner))
|
|
141
|
+
else:
|
|
142
|
+
result.append(t)
|
|
143
|
+
return result
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _tokenize_at_level(text: str) -> list[str]:
|
|
147
|
+
"""Split text by commas, respecting nested braces.
|
|
148
|
+
|
|
149
|
+
Returns tokens at the current level — brace groups are kept as single tokens.
|
|
150
|
+
"""
|
|
151
|
+
tokens: list[str] = []
|
|
152
|
+
depth = 0
|
|
153
|
+
current = ""
|
|
154
|
+
|
|
155
|
+
for ch in text:
|
|
156
|
+
if ch == "{":
|
|
157
|
+
depth += 1
|
|
158
|
+
current += ch
|
|
159
|
+
elif ch == "}":
|
|
160
|
+
depth -= 1
|
|
161
|
+
current += ch
|
|
162
|
+
elif ch == "," and depth == 0:
|
|
163
|
+
if current.strip():
|
|
164
|
+
tokens.append(current.strip())
|
|
165
|
+
current = ""
|
|
166
|
+
else:
|
|
167
|
+
current += ch
|
|
168
|
+
|
|
169
|
+
if current.strip():
|
|
170
|
+
tokens.append(current.strip())
|
|
171
|
+
|
|
172
|
+
return tokens
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _extract_row_data(line: str) -> tuple[str, str] | None:
|
|
176
|
+
"""Extract (label, brace_content) from a struct initializer row.
|
|
177
|
+
|
|
178
|
+
Returns None if line doesn't look like a struct row.
|
|
179
|
+
The brace_content is the inner content of the outermost balanced braces.
|
|
180
|
+
"""
|
|
181
|
+
# Try to find a label comment
|
|
182
|
+
label_match = _ROW_LABEL_RE.search(line)
|
|
183
|
+
label = label_match.group(1).strip() if label_match else ""
|
|
184
|
+
|
|
185
|
+
# Find the first { that starts the struct data
|
|
186
|
+
brace_start = line.find("{")
|
|
187
|
+
if brace_start == -1:
|
|
188
|
+
return None
|
|
189
|
+
|
|
190
|
+
# Find the matching closing brace by counting depth
|
|
191
|
+
data_portion = line[brace_start:]
|
|
192
|
+
depth = 0
|
|
193
|
+
end_idx = -1
|
|
194
|
+
for i, ch in enumerate(data_portion):
|
|
195
|
+
if ch == "{":
|
|
196
|
+
depth += 1
|
|
197
|
+
elif ch == "}":
|
|
198
|
+
depth -= 1
|
|
199
|
+
if depth == 0:
|
|
200
|
+
end_idx = i
|
|
201
|
+
break
|
|
202
|
+
|
|
203
|
+
if end_idx == -1:
|
|
204
|
+
return None
|
|
205
|
+
|
|
206
|
+
# Extract the balanced content (strip outermost { })
|
|
207
|
+
inner_content = data_portion[1:end_idx]
|
|
208
|
+
return (label, inner_content)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _auto_detect_table_var(text: str) -> str | None:
|
|
212
|
+
"""Auto-detect the struct array variable name from the file.
|
|
213
|
+
|
|
214
|
+
Looks for patterns like:
|
|
215
|
+
static const MyType VarName[SIZE] = {
|
|
216
|
+
Returns the variable name or None.
|
|
217
|
+
"""
|
|
218
|
+
for line in text.splitlines():
|
|
219
|
+
m = _TABLE_DECL_RE.match(line)
|
|
220
|
+
if m and "=" in line:
|
|
221
|
+
return m.group(1)
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
# Regex for field header comment: /* field1 field2 field3 ... */
|
|
226
|
+
_FIELD_HEADER_RE = re.compile(
|
|
227
|
+
r"/\*[\s,]*((?:[A-Za-z_]\w*[\s,]+)+[A-Za-z_]\w*)\s*\*/"
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _auto_detect_fields(text: str) -> list[str] | None:
|
|
232
|
+
"""Auto-detect field names from the column header comment.
|
|
233
|
+
|
|
234
|
+
Looks for a comment line like:
|
|
235
|
+
/* R_Phase L_d L_q Psi J Imax ... */
|
|
236
|
+
that appears just before the first data row (line with {{ ).
|
|
237
|
+
|
|
238
|
+
Returns list of field names or None.
|
|
239
|
+
"""
|
|
240
|
+
lines = text.splitlines()
|
|
241
|
+
prev_comment = None
|
|
242
|
+
|
|
243
|
+
for line in lines:
|
|
244
|
+
stripped = line.strip()
|
|
245
|
+
# Look for comment lines that could be field headers
|
|
246
|
+
if stripped.startswith("/*") and stripped.endswith("*/") and "{{" not in stripped:
|
|
247
|
+
# Extract content between /* and */
|
|
248
|
+
inner = stripped[2:-2].strip()
|
|
249
|
+
# Must contain multiple words separated by whitespace/commas (field names)
|
|
250
|
+
# Filter: must have at least 3 words and no sentences (no spaces within words)
|
|
251
|
+
tokens = re.split(r"[\s,]+", inner)
|
|
252
|
+
tokens = [t for t in tokens if t and re.match(r"^[A-Za-z_]\w*$", t)]
|
|
253
|
+
if len(tokens) >= 3:
|
|
254
|
+
prev_comment = tokens
|
|
255
|
+
# When we hit a data row, the previous comment was the header
|
|
256
|
+
elif "{{" in stripped and prev_comment:
|
|
257
|
+
return prev_comment
|
|
258
|
+
|
|
259
|
+
return None
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _auto_detect_variants(text: str) -> list[str]:
|
|
263
|
+
"""Find all preprocessor variant names for the primary #if chain in the file.
|
|
264
|
+
|
|
265
|
+
Groups by the macro being tested (e.g. RBFS_PscMotorParameter) and returns
|
|
266
|
+
the short variant names from the chain with the most alternatives.
|
|
267
|
+
This avoids picking up unrelated #if blocks (feature flags, etc.)
|
|
268
|
+
|
|
269
|
+
Returns list like ['BWA', 'EMB', 'DPB', 'IPB2'].
|
|
270
|
+
"""
|
|
271
|
+
# Regex that captures both the macro name and the value it's compared against
|
|
272
|
+
_MACRO_VARIANT_RE = re.compile(
|
|
273
|
+
r"^\s*#\s*(?:el)?if\s*\(\s*(\w+)\s*==\s*(\w+)\s*\)"
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
# Group by macro name → list of (full_value, short_name)
|
|
277
|
+
macro_groups: dict[str, list[str]] = {}
|
|
278
|
+
for line in text.splitlines():
|
|
279
|
+
m = _MACRO_VARIANT_RE.match(line)
|
|
280
|
+
if m:
|
|
281
|
+
macro_name = m.group(1) # e.g. RBFS_PscMotorParameter
|
|
282
|
+
full_value = m.group(2) # e.g. RBFS_PscMotorParameter_DPB
|
|
283
|
+
short = full_value.rsplit("_", 1)[-1] if "_" in full_value else full_value
|
|
284
|
+
if macro_name not in macro_groups:
|
|
285
|
+
macro_groups[macro_name] = []
|
|
286
|
+
if short not in macro_groups[macro_name]:
|
|
287
|
+
macro_groups[macro_name].append(short)
|
|
288
|
+
|
|
289
|
+
if not macro_groups:
|
|
290
|
+
return []
|
|
291
|
+
|
|
292
|
+
# Return variants from the chain with the most alternatives (the product switch)
|
|
293
|
+
primary_chain = max(macro_groups.values(), key=len)
|
|
294
|
+
return primary_chain
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _find_variant_block(text: str, variant: str) -> str | None:
|
|
298
|
+
"""Find the code block for a specific preprocessor variant.
|
|
299
|
+
|
|
300
|
+
Given variant="DPB", finds the block after #elif (...== RBFS_PscMotorParameter_DPB)
|
|
301
|
+
until the next #elif or #endif.
|
|
302
|
+
"""
|
|
303
|
+
lines = text.splitlines()
|
|
304
|
+
in_target_block = False
|
|
305
|
+
block_lines: list[str] = []
|
|
306
|
+
|
|
307
|
+
for line in lines:
|
|
308
|
+
if in_target_block:
|
|
309
|
+
# Check if we've hit the next #elif or #endif
|
|
310
|
+
if _PREPROC_ELIF_RE.match(line) or _PREPROC_ENDIF_RE.match(line):
|
|
311
|
+
break
|
|
312
|
+
block_lines.append(line)
|
|
313
|
+
else:
|
|
314
|
+
# Check if this is our target #if/#elif
|
|
315
|
+
m = _PREPROC_IF_RE.match(line)
|
|
316
|
+
if m:
|
|
317
|
+
block_name = m.group(1)
|
|
318
|
+
# Match against the variant name (e.g., "DPB" matches "RBFS_PscMotorParameter_DPB")
|
|
319
|
+
if variant.upper() in block_name.upper():
|
|
320
|
+
in_target_block = True
|
|
321
|
+
|
|
322
|
+
return "\n".join(block_lines) if block_lines else None
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _find_table_block(text: str, table_var: str | None = None) -> str | None:
|
|
326
|
+
"""Find the struct array declaration block.
|
|
327
|
+
|
|
328
|
+
If table_var is given, look for that specific variable name.
|
|
329
|
+
Otherwise find the first static const array declaration.
|
|
330
|
+
"""
|
|
331
|
+
lines = text.splitlines()
|
|
332
|
+
in_table = False
|
|
333
|
+
brace_depth = 0
|
|
334
|
+
block_lines: list[str] = []
|
|
335
|
+
|
|
336
|
+
for line in lines:
|
|
337
|
+
if not in_table:
|
|
338
|
+
if table_var:
|
|
339
|
+
if table_var in line and "[" in line:
|
|
340
|
+
in_table = True
|
|
341
|
+
else:
|
|
342
|
+
m = _TABLE_DECL_RE.match(line)
|
|
343
|
+
if m:
|
|
344
|
+
in_table = True
|
|
345
|
+
|
|
346
|
+
if in_table:
|
|
347
|
+
# Find the opening brace
|
|
348
|
+
if "{" in line:
|
|
349
|
+
brace_depth += line.count("{") - line.count("}")
|
|
350
|
+
block_lines.append(line)
|
|
351
|
+
continue
|
|
352
|
+
|
|
353
|
+
if in_table:
|
|
354
|
+
block_lines.append(line)
|
|
355
|
+
brace_depth += line.count("{") - line.count("}")
|
|
356
|
+
if brace_depth <= 0:
|
|
357
|
+
break
|
|
358
|
+
|
|
359
|
+
return "\n".join(block_lines) if block_lines else None
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
@register("c_struct_table")
|
|
363
|
+
def parse_c_struct_table(
|
|
364
|
+
filepath: str | Path,
|
|
365
|
+
fields: list[str] | None = None,
|
|
366
|
+
variant: str | None = None,
|
|
367
|
+
table_var: str | None = None,
|
|
368
|
+
**kwargs,
|
|
369
|
+
) -> list[Constant]:
|
|
370
|
+
"""Parse constants from a C struct array initializer table.
|
|
371
|
+
|
|
372
|
+
Args:
|
|
373
|
+
filepath: Path to the .c/.h file.
|
|
374
|
+
fields: Ordered list of field names matching the struct layout.
|
|
375
|
+
If not provided, auto-detected from column header comments,
|
|
376
|
+
or falls back to field_0, field_1, etc.
|
|
377
|
+
variant: Preprocessor variant to extract (e.g., "DPB", "EMB", "BWA").
|
|
378
|
+
Use "all" to extract ALL variants (for multi-sheet output).
|
|
379
|
+
Matches against #if/#elif conditions.
|
|
380
|
+
If the file has #if blocks and no variant is given, uses the
|
|
381
|
+
first variant found.
|
|
382
|
+
table_var: Name of the struct array variable to find.
|
|
383
|
+
If not provided, auto-detected from the first
|
|
384
|
+
`static const ... NAME[...] = {` declaration.
|
|
385
|
+
|
|
386
|
+
Returns:
|
|
387
|
+
List of Constant objects with names like "RowLabel__FieldName".
|
|
388
|
+
When variant="all", each Constant has metadata["variant"] set.
|
|
389
|
+
"""
|
|
390
|
+
filepath = Path(filepath)
|
|
391
|
+
if not filepath.exists():
|
|
392
|
+
raise FileNotFoundError(f"C struct table file not found: {filepath}")
|
|
393
|
+
|
|
394
|
+
text = filepath.read_text(encoding="utf-8")
|
|
395
|
+
|
|
396
|
+
# --- Auto-detection ---
|
|
397
|
+
# Auto-detect table_var if not provided
|
|
398
|
+
if not table_var:
|
|
399
|
+
table_var = _auto_detect_table_var(text)
|
|
400
|
+
|
|
401
|
+
# Handle "all" variant mode — parse every variant
|
|
402
|
+
if variant and variant.lower() == "all":
|
|
403
|
+
return _parse_all_variants(text, table_var, fields)
|
|
404
|
+
|
|
405
|
+
# Auto-detect variant if file has #if blocks and none specified
|
|
406
|
+
if not variant:
|
|
407
|
+
available_variants = _auto_detect_variants(text)
|
|
408
|
+
if available_variants:
|
|
409
|
+
variant = available_variants[0] # Default to first variant
|
|
410
|
+
|
|
411
|
+
# Strategy: First find the table block, THEN find the variant within it.
|
|
412
|
+
# This handles files where #if/#elif blocks are INSIDE the array declaration.
|
|
413
|
+
working_text = text
|
|
414
|
+
|
|
415
|
+
# Step 1: Find the table block (struct array declaration)
|
|
416
|
+
if table_var:
|
|
417
|
+
table_block = _find_table_block(working_text, table_var)
|
|
418
|
+
if table_block:
|
|
419
|
+
working_text = table_block
|
|
420
|
+
|
|
421
|
+
# Step 2: Within the table (or full text), find the variant block
|
|
422
|
+
if variant:
|
|
423
|
+
block = _find_variant_block(working_text, variant)
|
|
424
|
+
if block is None:
|
|
425
|
+
# Fallback: try searching in full text (variant block wraps the table)
|
|
426
|
+
block = _find_variant_block(text, variant)
|
|
427
|
+
if block is None:
|
|
428
|
+
raise ValueError(
|
|
429
|
+
f"Variant '{variant}' not found in {filepath}. "
|
|
430
|
+
f"Available variants: {_auto_detect_variants(text)}"
|
|
431
|
+
)
|
|
432
|
+
working_text = block
|
|
433
|
+
|
|
434
|
+
# If we still haven't narrowed it down, try finding table in the variant block
|
|
435
|
+
if table_var and variant:
|
|
436
|
+
inner_table = _find_table_block(working_text, table_var)
|
|
437
|
+
if inner_table:
|
|
438
|
+
working_text = inner_table
|
|
439
|
+
|
|
440
|
+
# Auto-detect fields from header comment (if not provided)
|
|
441
|
+
if not fields:
|
|
442
|
+
fields = _auto_detect_fields(working_text)
|
|
443
|
+
# If not found in variant block, try full table block or text
|
|
444
|
+
if not fields and table_var:
|
|
445
|
+
table_block = _find_table_block(text, table_var)
|
|
446
|
+
if table_block:
|
|
447
|
+
fields = _auto_detect_fields(table_block)
|
|
448
|
+
if not fields:
|
|
449
|
+
fields = _auto_detect_fields(text)
|
|
450
|
+
|
|
451
|
+
constants: list[Constant] = _parse_rows(working_text, fields)
|
|
452
|
+
|
|
453
|
+
# Tag each constant with the variant name
|
|
454
|
+
if variant:
|
|
455
|
+
for c in constants:
|
|
456
|
+
c.metadata["variant"] = variant
|
|
457
|
+
|
|
458
|
+
return constants
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def _parse_all_variants(
|
|
462
|
+
text: str, table_var: str | None, fields: list[str] | None
|
|
463
|
+
) -> list[Constant]:
|
|
464
|
+
"""Parse ALL variants from the file. Each constant gets metadata['variant']."""
|
|
465
|
+
available_variants = _auto_detect_variants(text)
|
|
466
|
+
if not available_variants:
|
|
467
|
+
# No variants — just parse the whole file
|
|
468
|
+
working_text = text
|
|
469
|
+
if table_var:
|
|
470
|
+
table_block = _find_table_block(working_text, table_var)
|
|
471
|
+
if table_block:
|
|
472
|
+
working_text = table_block
|
|
473
|
+
if not fields:
|
|
474
|
+
fields = _auto_detect_fields(working_text) or _auto_detect_fields(text)
|
|
475
|
+
return _parse_rows(working_text, fields)
|
|
476
|
+
|
|
477
|
+
# Auto-detect fields once (from first variant or table header)
|
|
478
|
+
if not fields:
|
|
479
|
+
# Try finding in table block
|
|
480
|
+
if table_var:
|
|
481
|
+
table_block = _find_table_block(text, table_var)
|
|
482
|
+
if table_block:
|
|
483
|
+
fields = _auto_detect_fields(table_block)
|
|
484
|
+
if not fields:
|
|
485
|
+
fields = _auto_detect_fields(text)
|
|
486
|
+
|
|
487
|
+
all_constants: list[Constant] = []
|
|
488
|
+
for v in available_variants:
|
|
489
|
+
# Find variant block within the table
|
|
490
|
+
working_text = text
|
|
491
|
+
if table_var:
|
|
492
|
+
table_block = _find_table_block(working_text, table_var)
|
|
493
|
+
if table_block:
|
|
494
|
+
working_text = table_block
|
|
495
|
+
|
|
496
|
+
block = _find_variant_block(working_text, v)
|
|
497
|
+
if block is None:
|
|
498
|
+
block = _find_variant_block(text, v)
|
|
499
|
+
if block is None:
|
|
500
|
+
continue
|
|
501
|
+
|
|
502
|
+
constants = _parse_rows(block, fields)
|
|
503
|
+
# Tag each with variant
|
|
504
|
+
for c in constants:
|
|
505
|
+
c.metadata["variant"] = v
|
|
506
|
+
all_constants.extend(constants)
|
|
507
|
+
|
|
508
|
+
return all_constants
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def _parse_rows(text: str, fields: list[str] | None) -> list[Constant]:
|
|
512
|
+
"""Parse struct initializer rows from a block of text."""
|
|
513
|
+
constants: list[Constant] = []
|
|
514
|
+
row_counter = 0
|
|
515
|
+
|
|
516
|
+
for line in text.splitlines():
|
|
517
|
+
# Skip preprocessor directives, blank lines, and pure comments
|
|
518
|
+
stripped = line.strip()
|
|
519
|
+
if not stripped or stripped.startswith("#") or stripped.startswith("//"):
|
|
520
|
+
continue
|
|
521
|
+
if stripped.startswith("/*") and stripped.endswith("*/") and "{{" not in stripped:
|
|
522
|
+
continue
|
|
523
|
+
# Skip lines that are just opening/closing braces for the array
|
|
524
|
+
if stripped in ("{", "}", "};", "{}", "},"):
|
|
525
|
+
continue
|
|
526
|
+
# Skip table declaration line
|
|
527
|
+
if _TABLE_DECL_RE.match(stripped):
|
|
528
|
+
continue
|
|
529
|
+
# Skip #define lines (SPEEDPARAM macros etc.)
|
|
530
|
+
if stripped.startswith("#define"):
|
|
531
|
+
continue
|
|
532
|
+
|
|
533
|
+
row_data = _extract_row_data(line)
|
|
534
|
+
if row_data is None:
|
|
535
|
+
continue
|
|
536
|
+
|
|
537
|
+
label, brace_content = row_data
|
|
538
|
+
|
|
539
|
+
# Flatten all values from nested braces
|
|
540
|
+
flat_values = _flatten_values(brace_content)
|
|
541
|
+
|
|
542
|
+
if not flat_values:
|
|
543
|
+
continue
|
|
544
|
+
|
|
545
|
+
# Sanitize label for use as name prefix
|
|
546
|
+
name_prefix = _sanitize_label(label) if label else f"row_{row_counter}"
|
|
547
|
+
row_counter += 1
|
|
548
|
+
|
|
549
|
+
# Map values to field names
|
|
550
|
+
for i, raw_val in enumerate(flat_values):
|
|
551
|
+
field_name = fields[i] if fields and i < len(fields) else f"field_{i}"
|
|
552
|
+
const_name = f"{name_prefix}__{field_name}"
|
|
553
|
+
|
|
554
|
+
# Determine if value is a literal number or an expression
|
|
555
|
+
raw_val_trimmed = raw_val.strip()
|
|
556
|
+
|
|
557
|
+
# Handle boolean-like values (must check before macro detection)
|
|
558
|
+
if raw_val_trimmed.upper() in ("TRUE", "FALSE"):
|
|
559
|
+
constants.append(Constant(
|
|
560
|
+
name=const_name,
|
|
561
|
+
value=raw_val_trimmed,
|
|
562
|
+
unit="",
|
|
563
|
+
description=f"Row: {label}, Field: {field_name}",
|
|
564
|
+
metadata={"row_label": label, "field": field_name, "field_index": i,
|
|
565
|
+
"is_expression": False},
|
|
566
|
+
))
|
|
567
|
+
continue
|
|
568
|
+
|
|
569
|
+
# Skip macro references like SPEEDPARAM, SPEEDPARAM_SIZEM1
|
|
570
|
+
if re.match(r"^[A-Z_][A-Z0-9_]*$", raw_val_trimmed) and not raw_val_trimmed.isdigit():
|
|
571
|
+
constants.append(Constant(
|
|
572
|
+
name=const_name,
|
|
573
|
+
value=raw_val_trimmed,
|
|
574
|
+
unit="",
|
|
575
|
+
description=f"Row: {label}, Field: {field_name} [macro]",
|
|
576
|
+
metadata={"row_label": label, "field": field_name, "field_index": i,
|
|
577
|
+
"is_expression": True, "raw": raw_val_trimmed},
|
|
578
|
+
))
|
|
579
|
+
continue
|
|
580
|
+
|
|
581
|
+
if _is_numeric_literal(raw_val_trimmed):
|
|
582
|
+
value = _parse_numeric(raw_val_trimmed)
|
|
583
|
+
constants.append(Constant(
|
|
584
|
+
name=const_name,
|
|
585
|
+
value=value,
|
|
586
|
+
unit="",
|
|
587
|
+
description=f"Row: {label}, Field: {field_name}",
|
|
588
|
+
metadata={"row_label": label, "field": field_name, "field_index": i,
|
|
589
|
+
"is_expression": False, "raw": raw_val_trimmed},
|
|
590
|
+
))
|
|
591
|
+
else:
|
|
592
|
+
# Expression-based value — store as string
|
|
593
|
+
constants.append(Constant(
|
|
594
|
+
name=const_name,
|
|
595
|
+
value=raw_val_trimmed,
|
|
596
|
+
unit="",
|
|
597
|
+
description=f"Row: {label}, Field: {field_name} [expression]",
|
|
598
|
+
metadata={"row_label": label, "field": field_name, "field_index": i,
|
|
599
|
+
"is_expression": True, "raw": raw_val_trimmed},
|
|
600
|
+
))
|
|
601
|
+
|
|
602
|
+
return constants
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def _sanitize_label(label: str) -> str:
|
|
606
|
+
"""Convert a row label to a valid C-style identifier prefix.
|
|
607
|
+
|
|
608
|
+
"BWA NI S4" → "BWA_NI_S4"
|
|
609
|
+
"EMB 12V 35KN" → "EMB_12V_35KN"
|
|
610
|
+
"""
|
|
611
|
+
# Replace non-alphanumeric chars with underscore
|
|
612
|
+
sanitized = re.sub(r"[^a-zA-Z0-9]+", "_", label.strip())
|
|
613
|
+
# Remove leading/trailing underscores
|
|
614
|
+
sanitized = sanitized.strip("_")
|
|
615
|
+
return sanitized
|