@h2loop/autosar 1.0.1 → 1.2.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 (53) hide show
  1. package/README.md +11 -1
  2. package/agents/autosar-integrator.md +45 -2
  3. package/commands/autosar-trace.md +2 -2
  4. package/package.json +4 -1
  5. package/plugins/index.js +55 -23
  6. package/python/ecuc_engine/__init__.py +1 -1
  7. package/python/ecuc_engine/add_signal.py +351 -0
  8. package/python/ecuc_engine/arxml_edit.py +387 -0
  9. package/python/ecuc_engine/audit.py +352 -50
  10. package/python/ecuc_engine/cli.py +125 -5
  11. package/python/ecuc_engine/codegen/__init__.py +233 -0
  12. package/python/ecuc_engine/codegen/binding.py +305 -0
  13. package/python/ecuc_engine/codegen/bindings/sip_standin.json +378 -0
  14. package/python/ecuc_engine/codegen/cdiff.py +395 -0
  15. package/python/ecuc_engine/codegen/cwriter.py +150 -0
  16. package/python/ecuc_engine/codegen/emit.py +463 -0
  17. package/python/ecuc_engine/codegen/handles.py +176 -0
  18. package/python/ecuc_engine/codegen/mcal/tc4d.json +60 -0
  19. package/python/ecuc_engine/codegen/model.py +264 -0
  20. package/python/ecuc_engine/codegen/modules/__init__.py +7 -0
  21. package/python/ecuc_engine/codegen/modules/com.py +145 -0
  22. package/python/ecuc_engine/codegen/modules/comstack.py +14 -0
  23. package/python/ecuc_engine/codegen/modules/dem.py +22 -0
  24. package/python/ecuc_engine/codegen/modules/mcal.py +170 -0
  25. package/python/ecuc_engine/codegen/modules/memstack.py +24 -0
  26. package/python/ecuc_engine/codegen/registry.py +208 -0
  27. package/python/ecuc_engine/codegen/schm_memmap.py +159 -0
  28. package/python/ecuc_engine/context.py +481 -0
  29. package/python/ecuc_engine/csrc.py +189 -0
  30. package/python/ecuc_engine/defs.py +30 -0
  31. package/python/ecuc_engine/ecucjson.py +75 -22
  32. package/python/ecuc_engine/find.py +164 -0
  33. package/python/ecuc_engine/gen.py +105 -0
  34. package/python/ecuc_engine/history.py +146 -0
  35. package/python/ecuc_engine/model.py +4 -1
  36. package/python/ecuc_engine/probe.py +229 -0
  37. package/python/ecuc_engine/rte_api.py +935 -0
  38. package/python/ecuc_engine/rules/__init__.py +5 -3
  39. package/python/ecuc_engine/rules/mem.py +84 -0
  40. package/python/ecuc_engine/rules/nvm.py +131 -0
  41. package/python/ecuc_engine/rules/pdur.py +87 -0
  42. package/python/ecuc_engine/rules/system.py +162 -0
  43. package/python/ecuc_engine/specids.py +163 -0
  44. package/python/ecuc_engine/sysmodel.py +152 -0
  45. package/python/ecuc_engine/validate.py +15 -2
  46. package/python/ecuc_engine/values.py +26 -0
  47. package/python/ecuc_engine/verify_rte.py +445 -0
  48. package/python/pyproject.toml +1 -1
  49. package/skills/autosar/SKILL.md +78 -6
  50. package/skills/autosar/references/codegen.md +58 -0
  51. package/skills/autosar/references/ecuc-model.md +4 -1
  52. package/skills/autosar/references/project-layout.md +10 -1
  53. package/skills/autosar/references/runnable-code.md +50 -0
@@ -0,0 +1,463 @@
1
+ """The shared BSW emitter: one ModuleOutput per module, rendered into the standard file set.
2
+
3
+ File placement follows each parameter's effective config class for the module's
4
+ IMPLEMENTATION-CONFIG-VARIANT (from the param defs' VALUE-CONFIG-CLASSES):
5
+ PRE-COMPILE / PUBLISHED-INFORMATION scalars -> #defines in <Mip>_Cfg.h
6
+ tables whose fields are all pre-compile -> <Mip>_Cfg.c (SWS_BSW_00103 Table 5.1)
7
+ tables with a link-time field -> <Mip>_Lcfg.c (SWS_BSW_00013)
8
+ tables with a post-build field -> <Mip>_PBcfg.c (SWS_BSW_00015)
9
+ Everything generic is driven by the defs: scalars of singleton containers become macros, every
10
+ symbolicNameValue parameter a TPS_ECUC_02108 symbol, every multiplicity-n container a const table.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import hashlib
15
+ from dataclasses import dataclass, field
16
+ from typing import Optional
17
+
18
+ from .. import ENGINE_VERSION
19
+ from .. import defs as D
20
+ from . import cwriter as W
21
+ from .model import CLASS_FILE, CLASS_RANK, CgModel, Container, ModuleCfg, snake
22
+
23
+
24
+ @dataclass
25
+ class Ctx:
26
+ cg: CgModel
27
+ headers: dict # project header basename -> path (static code, excluding the output dir)
28
+ bindings: dict
29
+ binding_name: Optional[str]
30
+ release: tuple
31
+ sources: list = field(default_factory=list) # project .c/.h files (static + in-house), for SchM scans
32
+ generating: set = field(default_factory=set) # module names generated in this run
33
+ available: set = field(default_factory=set) # header basenames reachable by the build (incl. the output dir)
34
+ out_headers: dict = field(default_factory=dict) # headers of other generators (e.g. the RTE) in the project's output dir
35
+
36
+ def module_release(self, mip: str) -> tuple:
37
+ """AUTOSAR release the module's generated configuration is built for: the published information of its
38
+ static code (<MIP>_AR_RELEASE_*_VERSION in <Mip>.h, SWS_BSW_00059), which SWS_BSW_00036-style checks compare
39
+ against; the ECUC schema release only when the project has no such header."""
40
+ f = self.headers.get(f"{mip}.h")
41
+ rel = published_release(f, mip.upper()) if f else None
42
+ return rel or self.release
43
+
44
+ def header_text(self, name: str) -> Optional[str]:
45
+ """Text of a project header or of another generator's header in the output dir (None if absent)."""
46
+ f = self.headers.get(name) or self.out_headers.get(name)
47
+ if not f:
48
+ return None
49
+ try:
50
+ with open(f, errors="replace") as fh:
51
+ return fh.read()
52
+ except OSError:
53
+ return None
54
+
55
+ def xsym(self, c: Container, here: ModuleCfg, used: set) -> str:
56
+ """Symbol of a container for use in module `here`: the TPS_ECUC_02108 name when its provider header
57
+ (<Mip>_Cfg.h) is generated now or already exists, else the configured value (named in a comment)."""
58
+ sym = self.cg.symbol(c)
59
+ if c.module is here:
60
+ return sym
61
+ if c.module.name in self.generating or f"{c.module.mip}_Cfg.h" in self.available:
62
+ used.add(c.module.mip)
63
+ return sym
64
+ from ..values import parse_int
65
+ v = parse_int(self.cg.symbol_value(c) or "")
66
+ return f"{W.c_uint(v)} /* {sym} */" if v is not None else sym
67
+
68
+
69
+ def published_release(path: str, MIP: str) -> Optional[tuple]:
70
+ """(major, minor, revision) from <MIP>_AR_RELEASE_{MAJOR,MINOR,REVISION}_VERSION in a header, if defined."""
71
+ import re
72
+ try:
73
+ with open(path, errors="replace") as fh:
74
+ t = fh.read()
75
+ except OSError:
76
+ return None
77
+ v = []
78
+ for k in ("MAJOR", "MINOR", "REVISION"):
79
+ m = re.search(rf"#\s*define\s+{re.escape(MIP)}_AR_RELEASE_{k}_VERSION\s+\(?(\d+)", t)
80
+ v.append(int(m.group(1)) if m else None)
81
+ if v[0] is None or v[1] is None:
82
+ return None
83
+ return (v[0], v[1], v[2] or 0)
84
+
85
+
86
+ class ModuleOutput:
87
+ def __init__(self, ctx: Ctx, mc: ModuleCfg):
88
+ self.ctx = ctx
89
+ self.mc = mc
90
+ self.mip = mc.mip
91
+ self.MIP = mc.MIP
92
+ self.release = ctx.module_release(mc.mip)
93
+ self.vars: dict = {} # rank -> [(section, text)]: RAM objects (e.g. I-PDU buffers) next to the tables
94
+ self.errors: list = [] # configuration the static-code binding cannot represent (generation fails)
95
+ self.groups: list = [] # (title, [define lines])
96
+ self.types: list = []
97
+ self.hdecls: list = []
98
+ self.h_includes: list = []
99
+ self.tables: dict = {} # rank -> [text]
100
+ self.c_externs: dict = {} # rank -> [text]
101
+ self.c_includes: list = []
102
+ self.used_mips: set = set()
103
+ self.defined: set = set()
104
+ self.notes: list = []
105
+ self.extra_files: dict = {} # name -> text (module-specific additional files)
106
+ self.symbols: list = [] # (name, value, container path)
107
+
108
+ # ---------- collection ----------
109
+ def group(self, title: str, defs: list):
110
+ lines = []
111
+ for d in defs:
112
+ name = d[0]
113
+ if name in self.defined:
114
+ self.notes.append(f"duplicate #define {name} suppressed ({title})")
115
+ continue
116
+ self.defined.add(name)
117
+ lines.append(W.define(*d))
118
+ if lines:
119
+ self.groups.append((title, lines))
120
+
121
+ def table(self, rank: int, text: str, externs: Optional[list] = None):
122
+ rank = max(rank, 1)
123
+ self.tables.setdefault(rank, []).append(text)
124
+ for e in externs or []:
125
+ lst = self.c_externs.setdefault(rank, [])
126
+ if e not in lst:
127
+ lst.append(e)
128
+
129
+ def var(self, rank: int, section: str, text: str):
130
+ """A RAM object owned by the configuration (e.g. a buffer the const tables point to)."""
131
+ rank = max(rank, 1)
132
+ self.vars.setdefault(rank, []).append((section, text))
133
+ self.tables.setdefault(rank, [])
134
+
135
+ def include(self, h: str):
136
+ if h not in self.c_includes:
137
+ self.c_includes.append(h)
138
+
139
+ # ---------- rendering ----------
140
+ def digest(self) -> str:
141
+ h = hashlib.sha256(ENGINE_VERSION.encode())
142
+ for f in sorted({f.file for c in self.mc.containers for f in c.frags} | {self.mc.elem.file}):
143
+ h.update(self.ctx.cg.p.files[f].data)
144
+ for f in sorted(self.ctx.cg.defs.files):
145
+ h.update(f.rsplit("/", 1)[-1].encode())
146
+ return "sha256:" + h.hexdigest()[:16]
147
+
148
+ def _banner(self, name: str) -> str:
149
+ return W.banner(name, self.mc.name, self.mc.variant, self.release, self.ctx.cg.rel_files(self.mc),
150
+ self._digest)
151
+
152
+ def render(self) -> dict:
153
+ self._digest = self.digest()
154
+ out = {}
155
+ hname = f"{self.mip}_Cfg.h"
156
+ g = W.guard(hname)
157
+ parts = [self._banner(hname), "", f"#ifndef {g}", f"#define {g}", ""]
158
+ for inc in self.h_includes:
159
+ parts.append(f'#include "{inc}"')
160
+ maj, mnr, rev = self.release
161
+ sw = [int(x) if x.isdigit() else 0 for x in ENGINE_VERSION.split("-")[0].split(".")][:3]
162
+ ver = [(f"{self.MIP}_CFG_AR_RELEASE_MAJOR_VERSION", f"{maj}u"),
163
+ (f"{self.MIP}_CFG_AR_RELEASE_MINOR_VERSION", f"{mnr}u"),
164
+ (f"{self.MIP}_CFG_AR_RELEASE_REVISION_VERSION", f"{rev}u"),
165
+ (f"{self.MIP}_CFG_SW_MAJOR_VERSION", f"{sw[0]}u"),
166
+ (f"{self.MIP}_CFG_SW_MINOR_VERSION", f"{sw[1]}u"),
167
+ (f"{self.MIP}_CFG_SW_PATCH_VERSION", f"{sw[2]}u")]
168
+ parts += ["", "/* Published information of this generated configuration (SWS_BSW_00059) */"]
169
+ parts += [W.define(n, v) for n, v in ver]
170
+ for title, lines in self.groups:
171
+ parts += ["", f"/* {title} */", *lines]
172
+ if self.types:
173
+ parts += ["", "/* Generated configuration types (one struct per multiplicity-n container def) */"]
174
+ parts += self.types
175
+ if self.hdecls:
176
+ parts += ["", *self.hdecls]
177
+ parts += ["", f"#endif /* {g} */", ""]
178
+ out[hname] = "\n".join(parts)
179
+ for rank in sorted(self.tables):
180
+ fname = f"{self.mip}_{CLASS_FILE[rank]}"
181
+ if fname in out: # PUBLISHED-INFORMATION and PRE-COMPILE share <Mip>_Cfg.c
182
+ continue
183
+ ranks = [r for r in self.tables if CLASS_FILE[r] == CLASS_FILE[rank]]
184
+ body = []
185
+ externs = []
186
+ for r in sorted(ranks):
187
+ body += self.tables[r]
188
+ externs += [e for e in self.c_externs.get(r, []) if e not in externs]
189
+ incs = list(self.c_includes) or [f"{self.mip}_Cfg.h"]
190
+ for m in sorted(self.used_mips - {self.mip}):
191
+ h = f"{m}_Cfg.h"
192
+ if h not in incs:
193
+ incs.append(h)
194
+ text = [self._banner(fname), ""]
195
+ text += [f'#include "{i}"' for i in incs]
196
+ text += ["", self._version_check(), ""]
197
+ if externs:
198
+ text += ["/* Objects owned by other modules / the application, referenced by this configuration */",
199
+ *externs, ""]
200
+ sections: dict = {}
201
+ for r in sorted(ranks):
202
+ for sec, v in self.vars.get(r, []):
203
+ sections.setdefault(sec, []).append(v)
204
+ for sec in sorted(sections):
205
+ text += [W.memmap(self.mip, "\n".join(sections[sec]), sec), ""]
206
+ text += [W.memmap(self.mip, "\n\n".join(body)), ""]
207
+ out[fname] = "\n".join(text)
208
+ out.update(self.extra_files)
209
+ return out
210
+
211
+ def _version_check(self) -> str:
212
+ M = self.MIP
213
+ return "\n".join([
214
+ f"/* SWS_BSW_00036: the static code ({self.mip}.h) and this configuration must use the same AUTOSAR release */",
215
+ f"#if defined({M}_AR_RELEASE_MAJOR_VERSION) && defined({M}_AR_RELEASE_MINOR_VERSION)",
216
+ f"#if ({M}_AR_RELEASE_MAJOR_VERSION != {M}_CFG_AR_RELEASE_MAJOR_VERSION) || "
217
+ f"({M}_AR_RELEASE_MINOR_VERSION != {M}_CFG_AR_RELEASE_MINOR_VERSION)",
218
+ f'#error "{self.mip}: AUTOSAR release of the generated configuration does not match {self.mip}.h"',
219
+ "#endif",
220
+ "#endif",
221
+ ])
222
+
223
+
224
+ # ======================================================================================
225
+ # Generic emission, driven only by the param defs
226
+ # ======================================================================================
227
+
228
+ def strip_prefix(mip: str, name: str) -> str:
229
+ return name[len(mip):] if name.lower().startswith(mip.lower()) and len(name) > len(mip) else name
230
+
231
+
232
+ def scalar_macros(cg: CgModel, mo: ModuleOutput):
233
+ """PRE-COMPILE / PUBLISHED-INFORMATION params of singleton containers -> #define <MIP>_<PARAM>."""
234
+ mc = mo.mc
235
+ names: dict = {}
236
+ for c in mc.containers:
237
+ if cg.is_multi(c) or c.defn is None:
238
+ continue
239
+ lines = []
240
+ for pd_path in c.defn.params:
241
+ d = cg.defs.get(pd_path)
242
+ if d is None or d.upper != 1 or cg.eff_class(mc, d) > CLASS_RANK["PRE-COMPILE"]:
243
+ continue
244
+ raw = c.text(d.name)
245
+ if raw is None:
246
+ raw = d.default
247
+ if raw is None:
248
+ continue
249
+ base = f"{mo.MIP}_{snake(strip_prefix(mo.mip, d.name))}"
250
+ if base in names and names[base] != c.path:
251
+ base = f"{mo.MIP}_{snake(strip_prefix(mo.mip, c.def_name))}_{snake(strip_prefix(mo.mip, d.name))}"
252
+ names[base] = c.path
253
+ lines.append((base, W.macro_value(d, raw)))
254
+ if d.kind == "ENUMERATION":
255
+ enum_literals(mo, d)
256
+ mo.group(f"Pre-compile parameters of {c.def_name} ({c.path})", lines)
257
+
258
+
259
+ def enum_literal_values(literals: list) -> list:
260
+ """C values of enumeration literals used in #if comparisons.
261
+
262
+ When every literal is <common stem><decimal number> with distinct numbers (NVM_API_CONFIG_CLASS_1..3), the
263
+ number in the name is the value: the name states the ordinal (SWS_NvM_00560..00562 API configuration class
264
+ 1/2/3), so #if (X >= X_2) keeps its meaning and no literal is 0. Otherwise the def literal order is used (the
265
+ ECUC def lists literals alphabetically, so that order carries no meaning beyond being stable).
266
+ """
267
+ import re
268
+ m = [re.fullmatch(r"(.*?)(\d+)", x) for x in literals]
269
+ if all(m) and len({x.group(1) for x in m}) == 1 and len({int(x.group(2)) for x in m}) == len(literals):
270
+ return [int(x.group(2)) for x in m]
271
+ return list(range(len(literals)))
272
+
273
+
274
+ def enum_literals(mo: ModuleOutput, d: D.DefNode):
275
+ """Guarded values for module-prefixed enum literals used in #if comparisons (see enum_literal_values)."""
276
+ lits = [x for x in d.literals if x.upper().startswith(mo.MIP + "_")]
277
+ if not lits or len(lits) != len(d.literals):
278
+ return
279
+ key = f"__enum__{d.path}"
280
+ if key in mo.defined:
281
+ return
282
+ mo.defined.add(key)
283
+ vals = enum_literal_values(d.literals)
284
+ lines = [f"#ifndef {lit}\n" + W.define(lit, W.c_uint(v)) + "\n#endif" for v, lit in zip(vals, d.literals)]
285
+ mo.groups.append((f"Literals of {d.name} (the static code may predefine them)", lines))
286
+
287
+
288
+ def symbolic_names(cg: CgModel, mo: ModuleOutput):
289
+ """TPS_ECUC_02108 / SWS_BSW_00200 symbols for every symbolicNameValue parameter of the module."""
290
+ by_def: dict = {}
291
+ for c in mo.mc.containers:
292
+ d = cg.symbolic_param(c)
293
+ if d is None:
294
+ continue
295
+ v = c.text(d.name)
296
+ if v is None:
297
+ continue
298
+ by_def.setdefault(c.def_name, []).append((c, d, v))
299
+ for dname, items in by_def.items():
300
+ lines = []
301
+ items = sorted(items, key=lambda t: (cg.sorted_instances(mo.mc, dname).index(t[0])))
302
+ for c, d, v in items:
303
+ sym = cg.symbol(c)
304
+ val = W.macro_value(d, v)
305
+ lines.append((sym, val))
306
+ mo.symbols.append((sym, val, c.path))
307
+ mo.group(f"Symbolic names of {dname} (TPS_ECUC_02108, {items[0][1].name})", lines)
308
+
309
+
310
+ # ---------- generic tables ----------
311
+
312
+ @dataclass
313
+ class Field:
314
+ name: str
315
+ ctype: str
316
+ steps: list # sub-container def names to the owner of the param
317
+ d: Optional[D.DefNode] = None # param/ref def
318
+ choice: Optional[D.DefNode] = None # for the choice selector field
319
+ parent: bool = False
320
+
321
+
322
+ def _fields(cg: CgModel, cdef: D.DefNode, prefix: str = "", steps: Optional[list] = None, depth: int = 0) -> list:
323
+ steps = steps or []
324
+ out: list = []
325
+ skipped: list = []
326
+ for p in cdef.params + cdef.refs:
327
+ d = cg.defs.get(p)
328
+ if d is None:
329
+ continue
330
+ if d.upper != 1:
331
+ if d.kind == "ENUMERATION" and 0 < len(d.literals) <= 32:
332
+ out.append(Field(prefix + d.name + "Mask", "uint32", steps, d)) # set of literals as a bitmask
333
+ else:
334
+ skipped.append(f"{d.name} (multi-valued)")
335
+ continue
336
+ if D.is_ref_kind(d.kind or ""):
337
+ if d.kind not in ("REFERENCE", "SYMBOLIC-NAME-REFERENCE", "CHOICE-REFERENCE"):
338
+ skipped.append(f"{d.name} ({d.kind.lower()})")
339
+ continue
340
+ out.append(Field(prefix + d.name, "uint16", steps, d))
341
+ continue
342
+ ct = W.param_ctype(d)
343
+ if ct is None:
344
+ skipped.append(f"{d.name} ({d.kind.lower()})")
345
+ continue
346
+ out.append(Field(prefix + d.name, ct, steps, d))
347
+ if depth < 4:
348
+ for sp in cdef.containers:
349
+ sd = cg.defs.get(sp)
350
+ if sd is None or sd.upper != 1:
351
+ continue # multiplicity-n sub-containers get their own table
352
+ if sd.tag == D.CHOICE_TAG:
353
+ out.append(Field(prefix + sd.name, "uint8", steps + [sd.name], choice=sd))
354
+ for bp in sd.containers:
355
+ bd = cg.defs.get(bp)
356
+ if bd is not None:
357
+ f2 = _fields(cg, bd, f"{prefix}{sd.name}_{bd.name}_", steps + [sd.name, bd.name], depth + 1)
358
+ out += f2
359
+ continue
360
+ out += _fields(cg, sd, f"{prefix}{sd.name}_", steps + [sd.name], depth + 1)
361
+ return out
362
+
363
+
364
+ def _owner(c: Optional[Container], steps: list) -> Optional[Container]:
365
+ for s in steps:
366
+ if c is None:
367
+ return None
368
+ subs = c.sub(s)
369
+ c = subs[0] if subs else None
370
+ return c
371
+
372
+
373
+ def _value(cg: CgModel, mo: ModuleOutput, row: Container, f: Field) -> tuple:
374
+ if f.parent:
375
+ p = row.parent
376
+ while p is not None and not cg.is_multi(p):
377
+ p = p.parent
378
+ return (W.c_uint(cg.index_of(p)) if p is not None else "0u", p.name if p is not None else "")
379
+ owner = _owner(row, f.steps)
380
+ if f.choice is not None:
381
+ if owner is None or not owner.children:
382
+ return ("0xFFu", "no choice")
383
+ br = owner.children[0]
384
+ names = [x.rsplit("/", 1)[-1] for x in f.choice.containers]
385
+ return (W.c_uint(names.index(br.def_name)) if br.def_name in names else "0xFFu", br.def_name)
386
+ d = f.d
387
+ if D.is_ref_kind(d.kind or ""):
388
+ t = cg.target(owner.value(d.name)) if owner is not None else None
389
+ if t is None:
390
+ return ("0xFFFFu", "no reference")
391
+ sym = cg.symbol(t)
392
+ if sym is not None:
393
+ return (mo.ctx.xsym(t, mo.mc, mo.used_mips), "")
394
+ if cg.is_multi(t):
395
+ return (W.c_uint(cg.index_of(t)), t.name)
396
+ return ("0u", t.name)
397
+ if d.upper != 1: # multi-valued enumeration -> bitmask of def literal indices
398
+ lits = [pv.value for pv in owner.values(d.name)] if owner is not None else []
399
+ mask = 0
400
+ for x in lits:
401
+ if x in d.literals:
402
+ mask |= 1 << d.literals.index(x)
403
+ return (W.c_uint(mask, hexa=True), "|".join(lits))
404
+ raw = owner.text(d.name) if owner is not None else None
405
+ return W.table_value(d, raw)
406
+
407
+
408
+ def generic_tables(cg: CgModel, mo: ModuleOutput, skip: set):
409
+ """One const struct array per multiplicity-n container def (plus singleton structs for non-PC scalars)."""
410
+ mc = mo.mc
411
+ seen: list = []
412
+ for c in mc.containers:
413
+ if c.def_name in skip or c.defn is None or c.def_name in seen:
414
+ continue
415
+ if c.defn.upper == 1 and not _has_dynamic(cg, mc, c):
416
+ continue
417
+ if c.defn.upper == 1 and cg.is_multi(c):
418
+ continue # flattened into its multi ancestor's row
419
+ seen.append(c.def_name)
420
+ for dname in seen:
421
+ rows = cg.sorted_instances(mc, dname)
422
+ cdef = rows[0].defn
423
+ fields = _fields(cg, cdef)
424
+ if rows[0].parent is not None and cg.is_multi(rows[0].parent):
425
+ fields = [Field("Parent", "uint16", [], parent=True)] + fields
426
+ if not fields:
427
+ continue
428
+ tname = f"{mo.mip}_Cfg{dname}Type"
429
+ oname = f"{mo.mip}_Cfg{dname}"
430
+ rank = max([cg.eff_class(mc, f.d) for f in fields if f.d is not None] + [cg.eff_class(mc, cdef), 1])
431
+ mo.types.append("typedef struct {\n" + "\n".join(f" {f.ctype} {f.name};" for f in fields) +
432
+ f"\n}} {tname};")
433
+ lines = []
434
+ for r in rows:
435
+ vals = []
436
+ notes = []
437
+ for f in fields:
438
+ v, n = _value(cg, mo, r, f)
439
+ vals.append(v)
440
+ sym = cg.symbol(r)
441
+ lines.append(W.initializer(vals, sym or r.name))
442
+ multi = cdef.upper != 1 or len(rows) > 1
443
+ cnt = f"{mo.MIP}_CFG_NUM_{snake(strip_prefix(mo.mip, dname))}"
444
+ if multi:
445
+ mo.group(f"Size of {oname}[]", [(cnt, W.c_uint(len(rows)))])
446
+ decl = f"CONST({tname}, {mo.MIP}_CONST) {oname}[{cnt}]"
447
+ body = f"{decl} = {{\n" + ",\n".join(lines) + "\n};"
448
+ else:
449
+ decl = f"CONST({tname}, {mo.MIP}_CONST) {oname}"
450
+ body = f"{decl} =\n" + lines[0].strip() + ";"
451
+ mo.hdecls.append(f"extern {decl};")
452
+ names = ", ".join(f.name for f in fields)
453
+ mo.table(rank, f"/* {dname}: {len(rows)} row(s), fields {names} */\n{body}")
454
+
455
+
456
+ def _has_dynamic(cg: CgModel, mc: ModuleCfg, c: Container) -> bool:
457
+ """A singleton container holding link-time/post-build scalars needs a const object, not macros."""
458
+ for p in c.defn.params:
459
+ d = cg.defs.get(p)
460
+ if d is not None and d.upper == 1 and W.param_ctype(d) and cg.eff_class(mc, d) > CLASS_RANK["PRE-COMPILE"]:
461
+ if c.text(d.name) is not None:
462
+ return True
463
+ return False
@@ -0,0 +1,176 @@
1
+ """Deterministic handle-ID assignment for symbolicNameValue parameters, written back in place.
2
+
3
+ TPS_ECUC_02107: values of symbolicNameValue parameters that the configuration editor or module
4
+ generator assigns are stored in the ECUC XML. TPS_ECUC_02108 turns them into the
5
+ `<Mip>Conf_<ContainerDef>_<ContainerValue>` #defines.
6
+
7
+ Algorithm (stable by construction):
8
+ * values already in the ECUC are kept as they are (so regeneration never renumbers a handle that
9
+ code or another ECU already uses), duplicates among them are reported, not "fixed";
10
+ * containers without a value get the lowest free integers >= max(def MIN, 0), in document order;
11
+ * the scope is one container def within one module (the symbol namespace of TPS_ECUC_02108).
12
+ Only INTEGER parameters are assigned. A missing STRING symbolic value is reported.
13
+
14
+ Static-code layout (layout()): a static code that indexes its tables by handle (e.g. a Com whose
15
+ signal table holds the Tx signals at 0..nTx-1 and the Rx signals after them) declares that order in
16
+ its binding ("handleLayout"). When the configured values break it (a Tx signal added after the Rx
17
+ ones), the generator renumbers that handle param, keeping the relative order inside each row set,
18
+ but only for withAuto params (the def says the configuration tool computes the value); otherwise the
19
+ mismatch is an error for the integrator.
20
+ """
21
+ from __future__ import annotations
22
+
23
+ from typing import Optional
24
+
25
+ from ..values import parse_int
26
+ from .model import CgModel, ModuleCfg
27
+
28
+
29
+ def plan(cg: CgModel, modules: list) -> dict:
30
+ """-> {"assign": [{path, param, value, def}], "errors": [...], "notes": [...]}"""
31
+ out: dict = {"assign": [], "errors": [], "notes": []}
32
+ for name in modules:
33
+ mc = cg.module(name)
34
+ if mc is None:
35
+ continue
36
+ _plan_module(cg, mc, out)
37
+ return out
38
+
39
+
40
+ def _plan_module(cg: CgModel, mc: ModuleCfg, out: dict):
41
+ seen_defs: list = []
42
+ for c in mc.containers:
43
+ if c.def_name not in seen_defs:
44
+ seen_defs.append(c.def_name)
45
+ for dname in seen_defs:
46
+ insts = cg.instances(mc, dname)
47
+ d = cg.symbolic_param(insts[0])
48
+ if d is None:
49
+ continue
50
+ if d.kind != "INTEGER":
51
+ for c in insts:
52
+ if c.text(d.name) in (None, ""):
53
+ out["notes"].append(f"{c.path}: {d.name} (symbolic {d.kind}) has no value and cannot be "
54
+ "auto-assigned; its symbol is not generated")
55
+ continue
56
+ lo = max(parse_int(d.min) if d.min else 0, 0)
57
+ hi: Optional[int] = parse_int(d.max) if d.max else None
58
+ used: dict = {}
59
+ missing = []
60
+ for c in insts:
61
+ v = parse_int(c.text(d.name) or "")
62
+ if v is None:
63
+ missing.append(c)
64
+ continue
65
+ if v in used:
66
+ out["errors"].append(f"{d.name}={v} is used by both {used[v]} and {c.path} "
67
+ f"(handle IDs must be unique per module, TPS_ECUC_02108)")
68
+ used.setdefault(v, c.path)
69
+ nxt = lo
70
+ for c in missing:
71
+ while nxt in used:
72
+ nxt += 1
73
+ if hi is not None and nxt > hi:
74
+ out["errors"].append(f"{c.path}: no free {d.name} left in [{lo}..{hi}]")
75
+ break
76
+ used[nxt] = c.path
77
+ out["assign"].append({"path": c.path, "param": d.name, "value": nxt, "def": d.path})
78
+ if not d.with_auto:
79
+ out["notes"].append(f"{c.path}: assigned {d.name}={nxt}; the def has no withAuto, so check that "
80
+ "the number carries no hardware meaning")
81
+ nxt += 1
82
+
83
+
84
+ def layout(cg: CgModel, modules: list, selected: dict) -> dict:
85
+ """-> {"assign": [...], "errors": [...], "notes": [...]} renumbering handles to each binding's handleLayout."""
86
+ from .binding import Eval
87
+ out: dict = {"assign": [], "errors": [], "notes": []}
88
+ for name in modules:
89
+ mb, mc = selected.get(name), cg.module(name)
90
+ if not mb or mc is None:
91
+ continue
92
+ ev = Eval(cg, mc)
93
+ for hl in mb.get("handleLayout", []):
94
+ rows: list = []
95
+ for st in hl["sets"]:
96
+ rows += [r for r in ev.rows("@" + st, mb.get("sets", {})) if r not in rows]
97
+ insts = cg.instances(mc, mb["sets"][hl["sets"][0]]["rows"])
98
+ rows += [r for r in cg.sorted_instances(mc, insts[0].def_name) if r not in rows] if insts else []
99
+ if not rows:
100
+ continue
101
+ d = cg.symbolic_param(rows[0])
102
+ changes = [(r, i) for i, r in enumerate(rows) if parse_int(r.text(d.name) or "") != i]
103
+ if not changes:
104
+ continue
105
+ what = f"{d.name}: the {mb.get('_binding')} static code indexes its tables by handle in the order " \
106
+ f"{' then '.join(hl['sets'])}"
107
+ if not d.with_auto:
108
+ out["errors"].append(f"{what}; {len(changes)} value(s) break that order and the def has no withAuto, "
109
+ f"so they are not renumbered (first: {changes[0][0].path}={changes[0][0].text(d.name)})")
110
+ continue
111
+ for r, i in changes:
112
+ out["assign"].append({"path": r.path, "param": d.name, "value": i, "def": d.path})
113
+ out["notes"].append(f"{what}; renumbered {len(changes)} {d.name} value(s) (withAuto, TPS_ECUC_02107), "
114
+ f"e.g. {changes[0][0].name}: {changes[0][0].text(d.name)} -> {changes[0][1]}")
115
+ return out
116
+
117
+
118
+ def apply(project, assignments: list, write: bool) -> dict:
119
+ """Write the assigned values into the ECUC ARXML in place (TPS_ECUC_02107).
120
+
121
+ Values that replace an existing VALUE are patched in one pass per file (only the VALUE text bytes change,
122
+ so a renumbering of thousands of handles stays linear); new values go through the engine's editor, which
123
+ places the element per the schema. write=False patches only the in-memory model (dry run), so the
124
+ generated code still uses them.
125
+ """
126
+ if not assignments:
127
+ return {"written": False, "changes": []}
128
+ import difflib
129
+ import os
130
+ from ..xmltree import escape
131
+ by_path = {}
132
+ for e in _elems(project):
133
+ by_path.setdefault(e.path, []).append(e)
134
+ patches: dict = {}
135
+ rest = []
136
+ for a in assignments:
137
+ hits = [(e, pv) for e in by_path.get(a["path"], []) for pv in e.params if pv.name == a["param"]]
138
+ vn = hits[0][1].value_node if len(hits) == 1 else None
139
+ if vn is None or vn.cstart is None or vn.tag != "VALUE":
140
+ rest.append(a)
141
+ continue
142
+ patches.setdefault(hits[0][0].file, []).append((vn.cstart, vn.cend, escape(str(a["value"])).encode()))
143
+ changes = []
144
+ for f in sorted(patches):
145
+ before = project.files[f].data
146
+ after = bytearray(before)
147
+ for start, end, repl in sorted(patches[f], reverse=True):
148
+ after[start:end] = repl
149
+ after = bytes(after)
150
+ project.reload(f, after)
151
+ rel = project.rel(f)
152
+ a_, b_ = before.decode("utf-8", "replace").splitlines(True), after.decode("utf-8", "replace").splitlines(True)
153
+ changes.append({"file": rel, "diff": "".join(difflib.unified_diff(a_, b_, f"a/{rel}", f"b/{rel}", n=1))})
154
+ if write:
155
+ tmp = f + ".ecuc-tmp"
156
+ with open(tmp, "wb") as fh:
157
+ fh.write(after)
158
+ os.replace(tmp, f)
159
+ written = bool(patches) and write
160
+ if rest:
161
+ from ..edit import edit
162
+ ops = [{"op": "set", "path": a["path"], "param": a["param"], "value": a["value"]} for a in rest]
163
+ res = edit(project, ops, dry_run=not write, run_validate=False)
164
+ if not res.get("ok"):
165
+ raise RuntimeError(f"handle-ID write-back failed: {res.get('error')}")
166
+ written = written or bool(res.get("written"))
167
+ changes += res.get("changes", [])
168
+ return {"written": written, "changes": changes}
169
+
170
+
171
+ def _elems(project):
172
+ stack = list(project.modules)
173
+ while stack:
174
+ e = stack.pop()
175
+ yield e
176
+ stack.extend(e.children)