@h2loop/autosar 1.0.1 → 1.1.0

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 (47) hide show
  1. package/README.md +4 -0
  2. package/agents/autosar-integrator.md +34 -2
  3. package/package.json +4 -1
  4. package/plugins/index.js +53 -23
  5. package/python/ecuc_engine/__init__.py +1 -1
  6. package/python/ecuc_engine/add_signal.py +351 -0
  7. package/python/ecuc_engine/arxml_edit.py +387 -0
  8. package/python/ecuc_engine/audit.py +328 -48
  9. package/python/ecuc_engine/cli.py +114 -5
  10. package/python/ecuc_engine/codegen/__init__.py +185 -0
  11. package/python/ecuc_engine/codegen/binding.py +210 -0
  12. package/python/ecuc_engine/codegen/bindings/sip_standin.json +111 -0
  13. package/python/ecuc_engine/codegen/cdiff.py +395 -0
  14. package/python/ecuc_engine/codegen/cwriter.py +150 -0
  15. package/python/ecuc_engine/codegen/emit.py +395 -0
  16. package/python/ecuc_engine/codegen/handles.py +89 -0
  17. package/python/ecuc_engine/codegen/mcal/tc4d.json +60 -0
  18. package/python/ecuc_engine/codegen/model.py +259 -0
  19. package/python/ecuc_engine/codegen/modules/__init__.py +7 -0
  20. package/python/ecuc_engine/codegen/modules/comstack.py +14 -0
  21. package/python/ecuc_engine/codegen/modules/dem.py +22 -0
  22. package/python/ecuc_engine/codegen/modules/mcal.py +170 -0
  23. package/python/ecuc_engine/codegen/modules/memstack.py +24 -0
  24. package/python/ecuc_engine/codegen/registry.py +133 -0
  25. package/python/ecuc_engine/codegen/schm_memmap.py +159 -0
  26. package/python/ecuc_engine/context.py +481 -0
  27. package/python/ecuc_engine/csrc.py +189 -0
  28. package/python/ecuc_engine/defs.py +30 -0
  29. package/python/ecuc_engine/find.py +162 -0
  30. package/python/ecuc_engine/gen.py +105 -0
  31. package/python/ecuc_engine/history.py +146 -0
  32. package/python/ecuc_engine/model.py +4 -1
  33. package/python/ecuc_engine/probe.py +229 -0
  34. package/python/ecuc_engine/rte_api.py +935 -0
  35. package/python/ecuc_engine/rules/__init__.py +5 -3
  36. package/python/ecuc_engine/rules/mem.py +84 -0
  37. package/python/ecuc_engine/rules/nvm.py +131 -0
  38. package/python/ecuc_engine/rules/pdur.py +87 -0
  39. package/python/ecuc_engine/rules/system.py +162 -0
  40. package/python/ecuc_engine/sysmodel.py +152 -0
  41. package/python/ecuc_engine/validate.py +15 -2
  42. package/python/ecuc_engine/verify_rte.py +445 -0
  43. package/python/pyproject.toml +1 -1
  44. package/skills/autosar/SKILL.md +47 -1
  45. package/skills/autosar/references/codegen.md +30 -0
  46. package/skills/autosar/references/project-layout.md +7 -1
  47. package/skills/autosar/references/runnable-code.md +50 -0
@@ -0,0 +1,395 @@
1
+ """Semantic diff of generated C configuration code (reference dir vs generated dir).
2
+
3
+ Compared: `#define` names and values, and file-scope initialised objects (struct/array tables)
4
+ element by element, including table ordering. Ignored: formatting, comments, banners, include
5
+ order, MemMap/section boilerplate, storage qualifiers (static/CONST()/const).
6
+
7
+ Values are normalised before comparison: integer literals by value (0x2A0u == 672), casts and
8
+ outer parentheses dropped, macros resolved through the defines of the same side (falling back to
9
+ the other side's defines for symbols a side does not define, e.g. ComConf_* from a Com_Cfg.h that
10
+ was not regenerated), constant integer expressions folded. Designated initialisers
11
+ (`[NvMConf_X] = {...}`) are placed at their resolved index.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import ast
16
+ import os
17
+ import re
18
+ from typing import Optional
19
+
20
+ BUILTINS = {"TRUE": "1", "FALSE": "0", "STD_ON": "1", "STD_OFF": "0", "STD_HIGH": "1", "STD_LOW": "0",
21
+ "E_OK": "0", "E_NOT_OK": "1"}
22
+
23
+ TOKEN = re.compile(r"""
24
+ (?P<ws>\s+)
25
+ | (?P<str>"(?:\\.|[^"\\])*")
26
+ | (?P<chr>'(?:\\.|[^'\\])*')
27
+ | (?P<num>(?:0[xX][0-9A-Fa-f]+|\d+\.\d*(?:[eE][+-]?\d+)?|\.\d+(?:[eE][+-]?\d+)?|\d+(?:[eE][+-]?\d+)?)[uUlLfF]*)
28
+ | (?P<id>[A-Za-z_]\w*)
29
+ | (?P<op>->|<<|>>|<=|>=|==|!=|&&|\|\||[-+*/%&|^~!<>=?:;,.\[\](){}#])
30
+ """, re.X)
31
+
32
+
33
+ def strip_comments(src: str) -> str:
34
+ out, i, n = [], 0, len(src)
35
+ while i < n:
36
+ c = src[i]
37
+ if c in "\"'":
38
+ j = i + 1
39
+ while j < n and src[j] != c:
40
+ j += 2 if src[j] == "\\" else 1
41
+ out.append(src[i:j + 1])
42
+ i = j + 1
43
+ elif src.startswith("/*", i):
44
+ j = src.find("*/", i + 2)
45
+ j = n if j < 0 else j + 2
46
+ out.append(" " + "\n" * src.count("\n", i, j))
47
+ i = j
48
+ elif src.startswith("//", i):
49
+ j = src.find("\n", i)
50
+ i = n if j < 0 else j
51
+ else:
52
+ out.append(c)
53
+ i += 1
54
+ return "".join(out)
55
+
56
+
57
+ def tokenize(src: str) -> list:
58
+ toks, pos = [], 0
59
+ while pos < len(src):
60
+ m = TOKEN.match(src, pos)
61
+ if not m:
62
+ toks.append(src[pos]) # stray character (e.g. '@'): keep, never crash
63
+ pos += 1
64
+ continue
65
+ pos = m.end()
66
+ if m.lastgroup != "ws":
67
+ toks.append(m.group(m.lastgroup))
68
+ return toks
69
+
70
+
71
+ def split_source(src: str) -> tuple:
72
+ """-> (defines {name: value-text}, code text without preprocessor lines)."""
73
+ src = strip_comments(src).replace("\\\r\n", " ").replace("\\\n", " ")
74
+ defines: dict = {}
75
+ code = []
76
+ for line in src.splitlines():
77
+ s = line.strip()
78
+ if s.startswith("#"):
79
+ m = re.match(r"#\s*define\s+([A-Za-z_]\w*)(\([^)]*\))?\s*(.*)$", s)
80
+ if m:
81
+ name = m.group(1) + (m.group(2) or "")
82
+ if m.group(1) not in defines and name not in defines:
83
+ defines[name if m.group(2) else m.group(1)] = m.group(3).strip()
84
+ code.append("")
85
+ continue
86
+ code.append(line)
87
+ return defines, "\n".join(code)
88
+
89
+
90
+ class Parser:
91
+ def __init__(self, toks: list):
92
+ self.t = toks
93
+ self.i = 0
94
+
95
+ def objects(self) -> dict:
96
+ """File-scope `<decl> name[...] = <init>;` -> {name: init-tree}."""
97
+ out: dict = {}
98
+ stmt_start = 0
99
+ depth = 0
100
+ t = self.t
101
+ i = 0
102
+ while i < len(t):
103
+ tok = t[i]
104
+ if tok in "({[":
105
+ depth += 1
106
+ elif tok in ")}]":
107
+ depth -= 1
108
+ elif tok == ";" and depth == 0:
109
+ stmt_start = i + 1
110
+ elif tok == "=" and depth == 0:
111
+ name = self._declarator(stmt_start, i)
112
+ self.i = i + 1
113
+ init = self._init()
114
+ if name:
115
+ out[name] = init
116
+ while self.i < len(t) and t[self.i] != ";":
117
+ self.i += 1
118
+ i = self.i
119
+ stmt_start = i + 1
120
+ depth = 0
121
+ i += 1
122
+ return out
123
+
124
+ def _declarator(self, start: int, eq: int) -> Optional[str]:
125
+ j = eq - 1
126
+ while j >= start and self.t[j] == "]": # skip [size] groups
127
+ d = 0
128
+ while j >= start:
129
+ if self.t[j] == "]":
130
+ d += 1
131
+ elif self.t[j] == "[":
132
+ d -= 1
133
+ if d == 0:
134
+ break
135
+ j -= 1
136
+ j -= 1
137
+ if j >= start and re.match(r"^[A-Za-z_]\w*$", self.t[j]):
138
+ return self.t[j]
139
+ return None
140
+
141
+ def _init(self):
142
+ t = self.t
143
+ if self.i < len(t) and t[self.i] == "{":
144
+ self.i += 1
145
+ items = []
146
+ while self.i < len(t) and t[self.i] != "}":
147
+ desig = None
148
+ if t[self.i] == "[":
149
+ j = self.i + 1
150
+ d = 1
151
+ while j < len(t) and d:
152
+ d += {"[": 1, "]": -1}.get(t[j], 0)
153
+ j += 1
154
+ if j < len(t) and t[j] == "=":
155
+ desig = ("index", t[self.i + 1:j - 1])
156
+ self.i = j + 1
157
+ elif t[self.i] == "." and self.i + 2 < len(t) and t[self.i + 2] == "=":
158
+ desig = ("field", t[self.i + 1])
159
+ self.i += 3
160
+ val = self._init()
161
+ items.append((desig, val))
162
+ if self.i < len(t) and t[self.i] == ",":
163
+ self.i += 1
164
+ self.i += 1 # '}'
165
+ return ("list", items)
166
+ start = self.i
167
+ d = 0
168
+ while self.i < len(t):
169
+ tok = t[self.i]
170
+ if tok in "([{":
171
+ d += 1
172
+ elif tok in ")]}":
173
+ if d == 0:
174
+ break
175
+ d -= 1
176
+ elif tok in ",;" and d == 0:
177
+ break
178
+ self.i += 1
179
+ return ("expr", t[start:self.i])
180
+
181
+
182
+ def parse_c(src: str) -> tuple:
183
+ defines, code = split_source(src)
184
+ return defines, Parser(tokenize(code)).objects()
185
+
186
+
187
+ # ---------- normalisation ----------
188
+
189
+ INT = re.compile(r"^(0[xX][0-9A-Fa-f]+|\d+)[uUlL]*$")
190
+
191
+
192
+ class Resolver:
193
+ def __init__(self, own: dict, fallback: dict):
194
+ self.own = own
195
+ self.fallback = fallback
196
+
197
+ def lookup(self, name: str) -> Optional[str]:
198
+ if name in self.own:
199
+ return self.own[name]
200
+ if name in self.fallback:
201
+ return self.fallback[name]
202
+ return BUILTINS.get(name)
203
+
204
+ def norm_tokens(self, toks: list, depth: int = 0) -> str:
205
+ toks = list(toks)
206
+ out = []
207
+ for tk in toks:
208
+ if re.match(r"^[A-Za-z_]\w*$", tk) and depth < 24:
209
+ v = self.lookup(tk)
210
+ if v is not None and v != tk:
211
+ out.append("(" + self.norm_tokens(tokenize(v), depth + 1) + ")")
212
+ continue
213
+ m = INT.match(tk)
214
+ out.append(str(int(m.group(1), 0) if not m.group(1).startswith("0") or m.group(1) == "0"
215
+ or m.group(1)[:2] in ("0x", "0X") else int(m.group(1), 8)) if m else tk)
216
+ return fold(" ".join(out))
217
+
218
+ def norm(self, toks: list) -> str:
219
+ return self.norm_tokens(toks)
220
+
221
+
222
+ CAST = re.compile(r"\(\s*(?:const\s+)?[A-Za-z_]\w*(?:\s*\*)*\s*\)\s*(?=[\w(&-])")
223
+
224
+
225
+ def fold(expr: str) -> str:
226
+ e = CAST.sub("", expr)
227
+ e = re.sub(r"\s+", " ", e).strip()
228
+ while e.startswith("(") and e.endswith(")") and _balanced(e[1:-1]):
229
+ e = e[1:-1].strip()
230
+ if re.fullmatch(r"[\d\s()+\-*/%<>&|^~]+", e or "x"):
231
+ try:
232
+ v = _eval(ast.parse(e, mode="eval").body)
233
+ if isinstance(v, int):
234
+ return str(v)
235
+ except Exception: # noqa: BLE001
236
+ pass
237
+ return re.sub(r"\s*([()\[\],&*.])\s*", r"\1", e).replace("->", "->")
238
+
239
+
240
+ def _balanced(s: str) -> bool:
241
+ d = 0
242
+ for ch in s:
243
+ d += {"(": 1, ")": -1}.get(ch, 0)
244
+ if d < 0:
245
+ return False
246
+ return d == 0
247
+
248
+
249
+ def _eval(n):
250
+ if isinstance(n, ast.Constant) and isinstance(n.value, int):
251
+ return n.value
252
+ if isinstance(n, ast.UnaryOp):
253
+ v = _eval(n.operand)
254
+ return {ast.USub: lambda: -v, ast.UAdd: lambda: v, ast.Invert: lambda: ~v}[type(n.op)]()
255
+ if isinstance(n, ast.BinOp):
256
+ a, b = _eval(n.left), _eval(n.right)
257
+ ops = {ast.Add: lambda: a + b, ast.Sub: lambda: a - b, ast.Mult: lambda: a * b,
258
+ ast.FloorDiv: lambda: a // b, ast.Div: lambda: a // b, ast.Mod: lambda: a % b,
259
+ ast.LShift: lambda: a << b, ast.RShift: lambda: a >> b, ast.BitOr: lambda: a | b,
260
+ ast.BitAnd: lambda: a & b, ast.BitXor: lambda: a ^ b}
261
+ return ops[type(n.op)]()
262
+ raise ValueError("not constant")
263
+
264
+
265
+ def flatten(tree, res: Resolver, prefix: str = "") -> dict:
266
+ """Init tree -> {path: normalised leaf}; designators resolved to indices / field names."""
267
+ kind, val = tree
268
+ if kind == "expr":
269
+ return {prefix or "=": res.norm(val)}
270
+ out: dict = {}
271
+ pos = 0
272
+ for desig, item in val:
273
+ if desig is None:
274
+ key = f"[{pos}]"
275
+ pos += 1
276
+ elif desig[0] == "index":
277
+ idx = res.norm(desig[1])
278
+ key = f"[{idx}]"
279
+ pos = int(idx) + 1 if idx.lstrip("-").isdigit() else pos + 1
280
+ else:
281
+ key = f".{desig[1]}"
282
+ out.update(flatten(item, res, prefix + key))
283
+ return out
284
+
285
+
286
+ def rows(tree, res: Resolver) -> Optional[list]:
287
+ """Top-level elements of an array initialiser as (index key, normalised row) for order checks."""
288
+ kind, val = tree
289
+ if kind != "list":
290
+ return None
291
+ out = []
292
+ pos = 0
293
+ for desig, item in val:
294
+ if desig is not None and desig[0] == "index":
295
+ idx = res.norm(desig[1])
296
+ key = f"[{idx}]"
297
+ pos = int(idx) + 1 if idx.isdigit() else pos + 1
298
+ elif desig is None:
299
+ key = f"[{pos}]"
300
+ pos += 1
301
+ else:
302
+ return None
303
+ out.append((key, tuple(sorted(flatten(item, res).items()))))
304
+ return out
305
+
306
+
307
+ # ---------- module grouping ----------
308
+
309
+ def module_of(fname: str) -> str:
310
+ base = os.path.basename(fname)
311
+ if base.startswith("SchM_"):
312
+ return base[5:].rsplit(".", 1)[0]
313
+ stem = base.rsplit(".", 1)[0]
314
+ return stem.split("_", 1)[0]
315
+
316
+
317
+ def load_dir(d: str) -> dict:
318
+ """{file basename: (defines, objects)} for *.h / *.c directly in d (or the single file d)."""
319
+ files = [d] if os.path.isfile(d) else [os.path.join(d, f) for f in sorted(os.listdir(d))]
320
+ out = {}
321
+ for f in files:
322
+ if not f.endswith((".h", ".c")) or not os.path.isfile(f):
323
+ continue
324
+ with open(f, errors="replace") as fh:
325
+ out[os.path.basename(f)] = parse_c(fh.read())
326
+ return out
327
+
328
+
329
+ def diff_dirs(reference: str, generated: str, modules: Optional[list] = None, max_items: int = 200) -> dict:
330
+ ref, gen = load_dir(reference), load_dir(generated)
331
+ ref_defs = {k: v for _, (d, _) in sorted(ref.items()) for k, v in d.items()}
332
+ gen_defs = {k: v for _, (d, _) in sorted(gen.items()) for k, v in d.items()}
333
+ rr, gr = Resolver(ref_defs, gen_defs), Resolver(gen_defs, ref_defs)
334
+ mods_ref: dict = {}
335
+ mods_gen: dict = {}
336
+ for f in ref:
337
+ mods_ref.setdefault(module_of(f), []).append(f)
338
+ for f in gen:
339
+ mods_gen.setdefault(module_of(f), []).append(f)
340
+ names = sorted(set(mods_ref) & set(mods_gen))
341
+ if modules:
342
+ names = [m for m in names if m in modules]
343
+ report: dict = {"modules": {}, "notGenerated": sorted(set(mods_ref) - set(mods_gen)),
344
+ "notInReference": sorted(set(mods_gen) - set(mods_ref))}
345
+ for m in names:
346
+ report["modules"][m] = _diff_module(m, mods_ref[m], mods_gen[m], ref, gen, rr, gr, max_items)
347
+ tot = {"matching": 0, "missing": 0, "extra": 0, "mismatches": 0}
348
+ for r in report["modules"].values():
349
+ for k in tot:
350
+ tot[k] += r["summary"][k]
351
+ report["total"] = tot
352
+ return report
353
+
354
+
355
+ def _diff_module(m, rfiles, gfiles, ref, gen, rr: Resolver, gr: Resolver, cap: int) -> dict:
356
+ rd = {k: v for f in sorted(rfiles) for k, v in ref[f][0].items()}
357
+ gd = {k: v for f in sorted(gfiles) for k, v in gen[f][0].items()}
358
+ ro = {k: v for f in sorted(rfiles) for k, v in ref[f][1].items()}
359
+ go = {k: v for f in sorted(gfiles) for k, v in gen[f][1].items()}
360
+ skip = re.compile(r"(_H|_START_SEC_\w+|_STOP_SEC_\w+|MEMMAP_ERROR|\w*_MEMMAP_SEQUENCE_OPEN\w*)$")
361
+ rd = {k: v for k, v in rd.items() if not skip.search(k)}
362
+ gd = {k: v for k, v in gd.items() if not skip.search(k)}
363
+ dm = {"match": [], "missing": sorted(set(rd) - set(gd)), "extra": sorted(set(gd) - set(rd)), "mismatch": []}
364
+ for k in sorted(set(rd) & set(gd)):
365
+ a, b = rr.norm(tokenize(rd[k])), gr.norm(tokenize(gd[k]))
366
+ if a == b:
367
+ dm["match"].append(k)
368
+ else:
369
+ dm["mismatch"].append({"name": k, "reference": rd[k], "generated": gd[k], "normalised": [a, b]})
370
+ om = {"match": [], "missing": sorted(set(ro) - set(go)), "extra": sorted(set(go) - set(ro)), "mismatch": [],
371
+ "order": []}
372
+ elements = {"match": 0, "mismatch": 0}
373
+ for k in sorted(set(ro) & set(go)):
374
+ fa, fb = flatten(ro[k], rr), flatten(go[k], gr)
375
+ bad = [{"element": f"{k}{p}", "reference": fa.get(p), "generated": fb.get(p)}
376
+ for p in sorted(set(fa) | set(fb)) if fa.get(p) != fb.get(p)]
377
+ elements["match"] += sum(1 for p in fa if fb.get(p) == fa[p])
378
+ if not bad:
379
+ om["match"].append(k)
380
+ continue
381
+ ra, rb = rows(ro[k], rr), rows(go[k], gr)
382
+ if ra is not None and rb is not None and sorted(x[1] for x in ra) == sorted(x[1] for x in rb):
383
+ om["order"].append({"name": k, "reference": [x[0] for x in ra], "generated": [x[0] for x in rb],
384
+ "note": "same rows, different table order"})
385
+ continue
386
+ elements["mismatch"] += len(bad)
387
+ om["mismatch"].append({"name": k, "elements": bad[:cap], "count": len(bad)})
388
+ summary = {"matching": len(dm["match"]) + len(om["match"]),
389
+ "missing": len(dm["missing"]) + len(om["missing"]),
390
+ "extra": len(dm["extra"]) + len(om["extra"]),
391
+ "mismatches": len(dm["mismatch"]) + len(om["mismatch"]) + len(om["order"])}
392
+ dm["match"] = len(dm["match"])
393
+ om["match"] = om["match"]
394
+ return {"files": {"reference": sorted(rfiles), "generated": sorted(gfiles)}, "defines": dm, "objects": om,
395
+ "elements": elements, "summary": summary}
@@ -0,0 +1,150 @@
1
+ """C text rendering shared by every module generator (no module knowledge here).
2
+
3
+ Rules implemented (cited where the standard sets them):
4
+ * generated-file banner (no timestamps, so output is byte-stable) and include guards;
5
+ * `#define` alignment, STD_ON/STD_OFF for booleans (SWS_Std_00010, SWS_BSW_00029);
6
+ * MemMap wrapping `<MIP>_START_SEC_<SECTION>` / `<MIP>_STOP_SEC_<SECTION>` + `#include "<Mip>_MemMap.h"`
7
+ (SWS_MemMap_00032, SWS_BSW_00006);
8
+ * C type of a parameter from its def (Platform_Types names, SWS_PlatformTypes).
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import re
13
+ from typing import Optional
14
+
15
+ from .. import ENGINE_VERSION
16
+ from .. import defs as D
17
+ from ..values import parse_float, parse_int
18
+
19
+ GENERATOR = f"Hydron AUTOSAR codegen {ENGINE_VERSION}"
20
+ CONFIG_SECTION = "CONFIG_DATA_UNSPECIFIED"
21
+ C_IDENT = re.compile(r"^[A-Za-z_]\w*$")
22
+ C_EXPR = re.compile(r"^[&*(]*[A-Za-z_][\w.\[\]>-]*\)*$") # &x, x[2], x.y, p->q, (x)
23
+
24
+
25
+ def banner(name: str, mc_name: str, variant: str, release: tuple, inputs: list, digest: str) -> str:
26
+ rel = ".".join(str(x) for x in release)
27
+ lines = [
28
+ "/" + "*" * 78,
29
+ f" * File: {name}",
30
+ f" * Module: {mc_name} ({variant}, AUTOSAR R{rel})",
31
+ f" * Generator: {GENERATOR} (deterministic: same ECUC + param defs -> same file)",
32
+ ]
33
+ for i, f in enumerate(inputs[:8]):
34
+ lines.append(f" * {'Inputs:' if i == 0 else '':11} {f}")
35
+ if len(inputs) > 8:
36
+ lines.append(f" * {'':11} ... {len(inputs) - 8} more")
37
+ lines += [f" * Input hash: {digest}", " * DO NOT EDIT. Change the ECUC configuration and regenerate (autosar_codegen).",
38
+ " " + "*" * 78 + "/"]
39
+ return "\n".join(lines)
40
+
41
+
42
+ def guard(name: str) -> str:
43
+ return re.sub(r"\W", "_", name).upper()
44
+
45
+
46
+ def define(name: str, value: str, comment: str = "") -> str:
47
+ pad = max(1, 56 - len(name))
48
+ line = f"#define {name}{' ' * pad}{value}"
49
+ return f"{line} /* {comment} */" if comment else line
50
+
51
+
52
+ def memmap(mip: str, body: str, section: str = CONFIG_SECTION) -> str:
53
+ MIP = mip.upper()
54
+ return (f"#define {MIP}_START_SEC_{section}\n#include \"{mip}_MemMap.h\"\n\n{body.rstrip()}\n\n"
55
+ f"#define {MIP}_STOP_SEC_{section}\n#include \"{mip}_MemMap.h\"")
56
+
57
+
58
+ def c_uint(v: int, hexa: bool = False) -> str:
59
+ if v < 0:
60
+ return f"({v})"
61
+ return f"0x{v:X}u" if hexa else f"{v}u"
62
+
63
+
64
+ def c_float(raw: str) -> str:
65
+ f = parse_float(raw)
66
+ if f is None:
67
+ return raw
68
+ if f != f or f in (float("inf"), float("-inf")):
69
+ return "0.0" # not representable in a C initialiser; the validator flags such values
70
+ s = repr(float(f))
71
+ return f"({s})" if s.startswith("-") else s
72
+
73
+
74
+ def c_text(raw: str) -> str:
75
+ """STRING / FUNCTION-NAME / LINKER-SYMBOL value: a C identifier or expression verbatim, else a string literal."""
76
+ t = raw.strip()
77
+ if C_EXPR.match(t):
78
+ return t
79
+ return '"' + t.replace("\\", "\\\\").replace('"', '\\"') + '"'
80
+
81
+
82
+ def int_ctype(lo: Optional[int], hi: Optional[int]) -> str:
83
+ if lo is not None and lo < 0:
84
+ for bits in (8, 16, 32):
85
+ if (hi is None or hi < 2 ** (bits - 1)) and lo >= -(2 ** (bits - 1)):
86
+ return f"sint{bits}"
87
+ return "sint64"
88
+ if hi is None:
89
+ return "uint32"
90
+ for bits in (8, 16, 32):
91
+ if hi < 2 ** bits:
92
+ return f"uint{bits}"
93
+ return "uint64"
94
+
95
+
96
+ def param_ctype(d: D.DefNode) -> Optional[str]:
97
+ """Platform type for a param def in a generated table, or None if it has no table representation."""
98
+ k = d.kind
99
+ if k == "INTEGER":
100
+ return int_ctype(parse_int(d.min) if d.min else None, parse_int(d.max) if d.max else None)
101
+ if k == "BOOLEAN":
102
+ return "boolean"
103
+ if k == "FLOAT":
104
+ return "float64"
105
+ if k == "ENUMERATION":
106
+ return "uint8" if len(d.literals) <= 256 else "uint16"
107
+ return None # STRING / FUNCTION-NAME / LINKER-SYMBOL / MULTILINE-STRING bind to static-code prototypes
108
+
109
+
110
+ def macro_value(d: Optional[D.DefNode], raw: str) -> str:
111
+ """Value of a pre-compile scalar #define."""
112
+ k = d.kind if d is not None else None
113
+ if k == "BOOLEAN":
114
+ return "STD_ON" if raw.strip().lower() in ("true", "1") else "STD_OFF"
115
+ if k == "INTEGER":
116
+ v = parse_int(raw)
117
+ return c_uint(v) if v is not None else raw
118
+ if k == "FLOAT":
119
+ return c_float(raw)
120
+ if k == "ENUMERATION":
121
+ return raw.strip() if C_IDENT.match(raw.strip()) else c_text(raw)
122
+ t = c_text(raw)
123
+ return t if C_IDENT.match(t) or t.startswith('"') or (t.startswith("(") and t.endswith(")")) else f"({t})"
124
+
125
+
126
+ def table_value(d: Optional[D.DefNode], raw: Optional[str], hexa: bool = False) -> tuple:
127
+ """(C initialiser, comment) of a param value in a generated table."""
128
+ k = d.kind if d is not None else None
129
+ if raw is None:
130
+ raw = d.default if d is not None and d.default is not None else None
131
+ if raw is None:
132
+ return ({"BOOLEAN": "FALSE", "FLOAT": "0.0"}.get(k or "", "0u"), "not configured")
133
+ if k == "BOOLEAN":
134
+ return ("TRUE" if raw.strip().lower() in ("true", "1") else "FALSE", "")
135
+ if k == "INTEGER":
136
+ v = parse_int(raw)
137
+ return (c_uint(v, hexa) if v is not None else raw, "")
138
+ if k == "FLOAT":
139
+ return (c_float(raw), "")
140
+ if k == "ENUMERATION":
141
+ lit = raw.strip()
142
+ if lit in d.literals:
143
+ return (c_uint(d.literals.index(lit)), lit)
144
+ return ("0u", f"unknown literal {lit}")
145
+ return (c_text(raw), "")
146
+
147
+
148
+ def initializer(values: list, comment: str = "", indent: str = " ") -> str:
149
+ body = ", ".join(values)
150
+ return f"{indent}{{ {body} }}" + (f" /* {comment} */" if comment else "")