@h2loop/autosar 1.1.0 → 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.
@@ -10,6 +10,13 @@ Algorithm (stable by construction):
10
10
  * containers without a value get the lowest free integers >= max(def MIN, 0), in document order;
11
11
  * the scope is one container def within one module (the symbol namespace of TPS_ECUC_02108).
12
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.
13
20
  """
14
21
  from __future__ import annotations
15
22
 
@@ -74,16 +81,96 @@ def _plan_module(cg: CgModel, mc: ModuleCfg, out: dict):
74
81
  nxt += 1
75
82
 
76
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
+
77
118
  def apply(project, assignments: list, write: bool) -> dict:
78
- """Write the assigned values into the ECUC ARXML in place (TPS_ECUC_02107) via the engine's editor.
119
+ """Write the assigned values into the ECUC ARXML in place (TPS_ECUC_02107).
79
120
 
80
- write=False patches only the in-memory model (dry run), so the generated code still uses them.
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.
81
125
  """
82
126
  if not assignments:
83
127
  return {"written": False, "changes": []}
84
- from ..edit import edit
85
- ops = [{"op": "set", "path": a["path"], "param": a["param"], "value": a["value"]} for a in assignments]
86
- res = edit(project, ops, dry_run=not write, run_validate=False)
87
- if not res.get("ok"):
88
- raise RuntimeError(f"handle-ID write-back failed: {res.get('error')}")
89
- return {"written": bool(res.get("written")), "changes": res.get("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)
@@ -28,8 +28,13 @@ SCHEMA_RELEASE = {
28
28
 
29
29
 
30
30
  def snake(name: str) -> str:
31
- """CamelCase -> UPPER_SNAKE (NvMDevErrorDetect -> NVM_DEV_ERROR_DETECT)."""
31
+ """CamelCase -> UPPER_SNAKE (NvMDevErrorDetect -> NVM_DEV_ERROR_DETECT).
32
+
33
+ The AUTOSAR glossary terms I-PDU / N-PDU / L-PDU stay one word (ComSupportedIPduGroups ->
34
+ COM_SUPPORTED_IPDU_GROUPS), as the specifications spell them in identifiers (IPDUM, ..._IPDU_...).
35
+ """
32
36
  s = re.sub(r"(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])", "_", name)
37
+ s = re.sub(r"(?<![A-Za-z])([INL])_(Pdu)", r"\1\2", s)
33
38
  return re.sub(r"_+", "_", s).upper()
34
39
 
35
40
 
@@ -0,0 +1,145 @@
1
+ """Com: Com_Cfg.h / Com_PBcfg.c.
2
+
3
+ Def-driven (generic emitter): every ComGeneral switch, the TPS_ECUC_02108 symbols of ComSignal,
4
+ ComSignalGroup, ComGroupSignal, ComIPdu and ComIPduGroup (handle IDs), and, without a static-code
5
+ binding, one table per container def (I-PDUs, signals, signal groups, group signals with bit
6
+ position / size / endianness / type / transfer property / timeouts / update bits).
7
+
8
+ The static Com's own table layout (signal and I-PDU tables, buffers, Tx-mode ticks, deadline
9
+ ticks) comes from its binding (bindings/*.json); this module only adds what neither the defs nor
10
+ the binding path language can express:
11
+ * COM_DEV_ERROR_DETECT from ComConfigurationUseDet (ECUC_Com_00141 names that output macro);
12
+ * notification callouts, resolved for the binding through `call:` steps:
13
+ - `comUserCallback`: R25-11 Com users (SWS_Com 8.6.3.1, ECUC_Com_10034/10036): the ComUserModuleCnf
14
+ referenced by ComUserModule/ComUserModuleCnfRef, its ComUserSignal / ComUserSignalGroup with the
15
+ same system signal (group) as the ComSignal (Group), and the ComUserCallback of type COM_RX_ACK
16
+ (receiver) or COM_TX_ACK (sender). These callbacks take a ComUserCbkHandleId.
17
+ - `rteSignalCallback`: the per-signal RTE callback `Rte_COMCbk_<sn>()` (receiver) /
18
+ `Rte_COMCbkTAck_<sn>()` (sender), <sn> = ComSignal short name (SWS_Rte Figure 4.42), bound only
19
+ when the RTE's callback header (Rte_Cbk.h, from the project or the output dir) declares it with
20
+ a void(void) prototype; Com_PBcfg.c then includes that header;
21
+ * the headers named by ComGeneral/ComUserCbkHeaderFile (callback prototypes) in Com_PBcfg.c;
22
+ * notes for configuration a static Com may silently not implement.
23
+ """
24
+ from __future__ import annotations
25
+
26
+ import re
27
+
28
+ from ..registry import Generator, register
29
+
30
+ RX_ACK, TX_ACK = "COM_RX_ACK", "COM_TX_ACK"
31
+
32
+
33
+ @register("Com")
34
+ class Com(Generator):
35
+ std_header = "ComStack_Types.h"
36
+
37
+ def specific(self, mo):
38
+ gen = [c for c in self.mc.containers if c.def_name == "ComGeneral"]
39
+ g = gen[0] if gen else None
40
+ det = g.text("ComConfigurationUseDet") if g is not None else None
41
+ mo.group("Development error detection (ECUC_Com_00141: ComConfigurationUseDet -> COM_DEV_ERROR_DETECT)",
42
+ [("COM_DEV_ERROR_DETECT", "STD_ON" if (det or "").strip().lower() in ("true", "1") else "STD_OFF")])
43
+ if g is not None:
44
+ for pv in g.values("ComUserCbkHeaderFile"):
45
+ h = (pv.value or "").strip().strip('"<>')
46
+ if h:
47
+ mo.include(h)
48
+ self._dir = self._directions()
49
+ self._users = self._com_user_callbacks()
50
+ self._rte_text = self.ctx.header_text("Rte_Cbk.h")
51
+ self._limits(mo)
52
+
53
+ # ---------- call: resolvers for the binding ----------
54
+
55
+ def path_steps(self, mo) -> dict:
56
+ def com_user(c, ev):
57
+ hit = self._users.get((c.path, self._ack(c)))
58
+ if hit is None:
59
+ return None
60
+ name, header = hit
61
+ if header:
62
+ mo.include(header)
63
+ return name
64
+
65
+ def rte_signal(c, ev):
66
+ if c.def_name not in ("ComSignal", "ComSignalGroup") or not self._rte_text:
67
+ return None
68
+ name = ("Rte_COMCbk_" if self._ack(c) == RX_ACK else "Rte_COMCbkTAck_") + c.name
69
+ if not re.search(rf"\b{re.escape(name)}\s*\(\s*void\s*\)", self._rte_text):
70
+ if (c.path, self._ack(c)) in self._users:
71
+ mo.notes.append(f"{c.path}: has a Com user callback ({self._users[(c.path, self._ack(c))][0]}), "
72
+ "which takes a ComUserCbkHandleId; the static code's notification type is "
73
+ "void(void), so it is not bound")
74
+ return None
75
+ mo.include("Rte_Cbk.h")
76
+ return name
77
+
78
+ return {"comUserCallback": com_user, "rteSignalCallback": rte_signal}
79
+
80
+ # ---------- helpers ----------
81
+
82
+ def _directions(self) -> dict:
83
+ """ComSignal / ComSignalGroup path -> ComIPduDirection of the I-PDU that carries it."""
84
+ out = {}
85
+ for ip in self.cg.instances(self.mc, "ComIPdu"):
86
+ d = (ip.text("ComIPduDirection") or "").strip()
87
+ for r in ("ComIPduSignalRef", "ComIPduSignalGroupRef"):
88
+ for pv in ip.values(r):
89
+ if pv.value:
90
+ out.setdefault(pv.value.strip(), d)
91
+ return out
92
+
93
+ def _ack(self, c) -> str:
94
+ return TX_ACK if self._dir.get(c.path) == "SEND" else RX_ACK
95
+
96
+ def _com_user_callbacks(self) -> dict:
97
+ """(ComSignal/ComSignalGroup path, callback type) -> (ComUserCallbackName, ComUserHeaderInclude)."""
98
+ by_sys: dict = {}
99
+ for c in self.mc.containers:
100
+ for p in ("ComSystemTemplateSystemSignalRef", "ComSystemTemplateSignalGroupRef"):
101
+ v = c.text(p)
102
+ if v and c.def_name in ("ComSignal", "ComSignalGroup"):
103
+ by_sys.setdefault(v.strip(), c)
104
+ out: dict = {}
105
+ for um in self.cg.instances(self.mc, "ComUserModule"):
106
+ cnf = self.cg.target(um.value("ComUserModuleCnfRef"))
107
+ if cnf is None:
108
+ continue
109
+ header = (cnf.text("ComUserHeaderInclude") or "").strip().strip('"<>') or None
110
+ for u in cnf.children:
111
+ sysref = u.text("ComUserSystemTemplateSystemSignalRef") or u.text("ComUserSystemTemplateSignalGroupRef")
112
+ sig = by_sys.get((sysref or "").strip())
113
+ if sig is None:
114
+ continue
115
+ for pv in u.values("ComUserCallbackRef"):
116
+ cb = self.cg.target(pv)
117
+ if cb is None:
118
+ continue
119
+ kind = (cb.text("ComUserCallbackType") or "").strip()
120
+ name = (cb.text("ComUserCallbackName") or "").strip()
121
+ if name:
122
+ out.setdefault((sig.path, kind), (name, header))
123
+ return out
124
+
125
+ def _limits(self, mo):
126
+ """Configuration that is valid ECUC but that a static Com may not implement: say so, never drop it silently."""
127
+ if mo.binding is None:
128
+ return
129
+ cnt = lambda pred: sum(1 for c in self.mc.containers if pred(c)) # noqa: E731
130
+ notes = []
131
+ n = cnt(lambda c: c.def_name == "ComIPdu" and (c.text("ComIPduSignalProcessing") or "").strip() == "DEFERRED")
132
+ if n:
133
+ notes.append(f"{n} I-PDU(s) use ComIPduSignalProcessing=DEFERRED; the binding has no field for it "
134
+ "(the static code decides when Rx notifications run)")
135
+ n = cnt(lambda c: c.def_name == "ComSignal" and (c.text("ComRxDataTimeoutAction") or "NONE").strip() != "NONE")
136
+ if n:
137
+ notes.append(f"{n} signal(s) configure ComRxDataTimeoutAction; the binding carries only the I-PDU "
138
+ "deadline (TimeoutTicks), the action is up to the static code")
139
+ n = cnt(lambda c: c.def_name == "ComTxMode" and int(float(c.text("ComTxModeNumberOfRepetitions") or 0)) > 0)
140
+ if n:
141
+ notes.append(f"{n} Tx mode(s) configure ComTxModeNumberOfRepetitions > 0; the binding has no repetition field")
142
+ n = cnt(lambda c: c.def_name == "ComTxIPdu" and float(c.text("ComMinimumDelayTime") or 0) > 0)
143
+ if n:
144
+ notes.append(f"{n} Tx I-PDU(s) configure ComMinimumDelayTime > 0; the binding has no MDT field")
145
+ mo.notes += [f"{(mo.binding or {}).get('_binding')}: {x}" for x in notes]
@@ -57,9 +57,16 @@ class Generator:
57
57
  if mb is None or mb.get("genericTables", False):
58
58
  E.generic_tables(self.cg, mo, set(self.skip_tables(mo)))
59
59
  if mb is not None:
60
- apply_binding(self.cg, mo, mb)
60
+ try:
61
+ apply_binding(self.cg, mo, mb, self.path_steps(mo))
62
+ except B.BindingError as ex:
63
+ mo.errors.append(str(ex))
61
64
  return mo
62
65
 
66
+ def path_steps(self, mo) -> dict:
67
+ """call:<name> resolvers for the binding path language: {name: fn(container, Eval) -> token | None}."""
68
+ return {}
69
+
63
70
  def default_std_header(self) -> str:
64
71
  """ComStack_Types.h for modules that reference EcuC PDUs (communication stack), else Std_Types.h."""
65
72
  md = self.mc.defn
@@ -77,24 +84,53 @@ class Generator:
77
84
  """Module-specific emission (only what the param defs cannot express)."""
78
85
 
79
86
 
80
- def apply_binding(cg, mo: E.ModuleOutput, mb: dict):
87
+ def apply_binding(cg, mo: E.ModuleOutput, mb: dict, ext: Optional[dict] = None):
81
88
  ev = B.Eval(cg, mo.mc, mo.ctx)
89
+ ev.ext = ext or {}
90
+ sets = mb.get("sets", {})
91
+ for req in mb.get("require", []):
92
+ bad = [r for r in ev.rows(req["rows"], sets) if not ev.matches(r, [req])]
93
+ if bad:
94
+ vals = sorted({str(ev.raw(r, req["path"])) for r in bad})
95
+ mo.errors.append(f"{len(bad)} {req['rows']} container(s) have {req['path']} = {', '.join(vals)} "
96
+ f"({', '.join(r.name for r in bad[:5])}{', ...' if len(bad) > 5 else ''}); the "
97
+ f"{mb['_binding']} static code supports only "
98
+ f"{', '.join('(not configured)' if x is None else str(x) for x in req['in'])}: {req.get('why', '')}")
99
+ for un in mb.get("unsupported", []):
100
+ rows = cg.instances(mo.mc, un["rows"])
101
+ if rows:
102
+ mo.errors.append(f"{len(rows)} {un['rows']} container(s) configured ({', '.join(r.name for r in rows[:5])}"
103
+ f"{', ...' if len(rows) > 5 else ''}), which the {mb['_binding']} static code cannot "
104
+ f"represent: {un.get('why', '')}")
82
105
  for inc in mb.get("sourceIncludes", []):
83
106
  mo.include(inc)
84
107
  macros = []
108
+ values: dict = {}
85
109
  for m in mb.get("macros", []):
86
110
  val = m["value"]
87
- macros.append((m["name"], ev.macro(val) if re.match(r"^(count|max):", val) else val))
111
+ v = ev.macro(val, sets) if re.match(r"^(count|max):", val) else val
112
+ macros.append((m["name"], v))
113
+ values[m["name"]] = v
88
114
  mo.group(f"Static-code interface ({mb['_binding']} binding)", macros)
89
- ranks: dict = {}
90
115
  pending: list = []
116
+ for vs in mb.get("vars", []):
117
+ ev.rank = 1
118
+ lines = []
119
+ for r in ev.rows(vs["rows"], sets):
120
+ size, _ = ev.field(r, {"name": vs["name"], "path": vs["size"]})
121
+ name = vs["name"].replace("{}", r.name)
122
+ lines.append(f"{'static ' if vs.get('static') else ''}VAR({vs['type']}, {vs['qualifier']}) {name}[{size}];")
123
+ pending.append(("var", ev.rank, vs.get("section", "VAR_NOINIT_UNSPECIFIED"), "\n".join(lines)))
91
124
  for t in mb.get("tables", []):
92
125
  ev.rank = 1
93
- rows = list(cg.sorted_instances(mo.mc, t["rows"]))
126
+ rows = ev.rows(t["rows"], sets)
94
127
  if t.get("order"):
95
128
  from ..values import parse_int
96
129
  rows.sort(key=lambda r: (parse_int(r.text(t["order"]) or "") is None, parse_int(r.text(t["order"]) or "") or 0))
97
130
  lines, externs = [], []
131
+ des = t.get("designated")
132
+ if des:
133
+ _check_index(cg, mo, t, rows, des, values)
98
134
  for r in rows:
99
135
  vals = []
100
136
  for f in t["fields"]:
@@ -103,10 +139,12 @@ def apply_binding(cg, mo: E.ModuleOutput, mb: dict):
103
139
  if f.get("extern") and v not in ("NULL_PTR", "0", "0u") and not v.startswith('"'):
104
140
  m = re.search(r"[A-Za-z_]\w*", v)
105
141
  if m:
106
- ext = "extern " + f["extern"].replace("{}", m.group(0)) + ";"
107
- if ext not in externs:
108
- externs.append(ext)
109
- lead = f"[{cg.symbol(r)}] = " if t.get("designated") else ""
142
+ ext_ = "extern " + f["extern"].replace("{}", m.group(0)) + ";"
143
+ if ext_ not in externs:
144
+ externs.append(ext_)
145
+ lead = ""
146
+ if des:
147
+ lead = f"[{des.replace('{sym}', cg.symbol(r)) if isinstance(des, str) else cg.symbol(r)}] = "
110
148
  comment = t.get("comment", "name")
111
149
  cm = {"name": r.name, "sym": cg.symbol(r) or r.name}.get(comment, r.name)
112
150
  if t.get("scalar"):
@@ -117,17 +155,54 @@ def apply_binding(cg, mo: E.ModuleOutput, mb: dict):
117
155
  size = t.get("size", "")
118
156
  body = (f"/* {t.get('doc', t['rows'])} */\n{st}CONST({t['type']}, {t['qualifier']}) {t['name']}[{size}] = {{\n" +
119
157
  ",\n".join(lines) + "\n};")
120
- ranks[t["name"]] = ev.rank
121
- pending.append((ev.rank, body, externs))
158
+ pending.append(("table", ev.rank, body, externs))
122
159
  objects = mb.get("objects", [])
123
160
  if objects:
124
- # a root config object points at the (static) tables, so they all live in its file:
161
+ # a root config object points at the (static) tables and buffers, so they all live in its file:
125
162
  # the most dynamic config class among them (e.g. <Mip>_PBcfg.c for a post-build PduR_Config)
126
- top = max([r for r, _, _ in pending] + [1])
127
- pending = [(top, b, e) for _, b, e in pending]
163
+ top = max([p[1] for p in pending] + [1])
164
+ pending = [(p[0], top) + p[2:] for p in pending]
128
165
  for o in objects:
129
166
  body = (f"CONST({o['type']}, {o['qualifier']}) {o['name']} = {{\n " + ", ".join(o["init"]) + "\n};")
130
- pending.append((top, body, []))
131
- for rank, body, externs in pending:
132
- mo.table(rank, body, externs)
167
+ pending.append(("table", top, body, []))
168
+ for p in pending:
169
+ if p[0] == "var":
170
+ mo.var(p[1], p[2], p[3])
171
+ else:
172
+ mo.table(p[1], p[2], p[3])
133
173
  mo.used_mips |= ev.used_mips
174
+
175
+
176
+ def _check_index(cg, mo, t: dict, rows: list, des, values: dict):
177
+ """A designated table is indexed by handle (minus an offset): every row must land in [0, size) exactly once,
178
+ or the static code's table lookup would be wrong. Reported as an error, never renumbered here."""
179
+ from ..values import parse_int
180
+ off_name = None
181
+ if isinstance(des, str):
182
+ m = re.fullmatch(r"\{sym\}\s*-\s*(\w+)", des.strip())
183
+ if not m:
184
+ return
185
+ off_name = m.group(1)
186
+ num = lambda v: parse_int(re.sub(r"[uUlL]+$", "", str(v))) # noqa: E731
187
+ off = num(values.get(off_name, "0")) if off_name else 0
188
+ size = num(values[t["size"]]) if t.get("size") in values else len(rows)
189
+ seen: dict = {}
190
+ bad = 0
191
+ for r in rows:
192
+ h = parse_int(cg.symbol_value(r) or "")
193
+ if h is None:
194
+ continue
195
+ i = h - (off or 0)
196
+ if not 0 <= i < (size or 0) or i in seen:
197
+ bad += 1
198
+ if bad > 5:
199
+ continue
200
+ pm = cg.symbolic_param(r)
201
+ mo.errors.append(f"{r.path}: {pm.name if pm else 'handle'}={h} does not fit the static code's "
202
+ f"{t['name']}[{t.get('size')}] indexed by {des if isinstance(des, str) else '{sym}'} "
203
+ f"({'collides with ' + seen[i] if i in seen else f'index {i} outside 0..{(size or 0) - 1}'}); "
204
+ f"the {t['rows']} handles must be contiguous in the order the static code expects")
205
+ continue
206
+ seen[i] = r.name
207
+ if bad > 5:
208
+ mo.errors.append(f"... {bad - 5} more {t['rows']} handle(s) outside {t['name']}")
@@ -8,6 +8,10 @@ Keyed by def names, not XML structure:
8
8
  "ComSignal:DoorLatch": {...}}}}}
9
9
  A container key is "<DefName>:<ShortName>", shortened to "<ShortName>" when the two are equal.
10
10
  Multi-valued parameters are arrays. References are "->/AUTOSAR/path".
11
+ Every container carries "@at" (file:line of the container element). Every value is followed by
12
+ "@at:<Param>" = file:line of its VALUE / VALUE-REF element, and integer-typed values by "@hex:<Param>"
13
+ (and "@dec:<Param>" when the ARXML text is not decimal), so citations and base conversions are never guessed.
14
+ Keys starting with "@" are metadata; ecuc_edit ignores them.
11
15
  """
12
16
  from __future__ import annotations
13
17
 
@@ -16,7 +20,7 @@ from typing import Optional
16
20
  from . import defs as D
17
21
  from .model import Elem, Project
18
22
  from .validate import _resolve_def
19
- from .values import to_json
23
+ from .values import int_forms, to_json
20
24
 
21
25
 
22
26
  def key_of(e: Elem) -> str:
@@ -24,27 +28,55 @@ def key_of(e: Elem) -> str:
24
28
  return e.name if dn == e.name else f"{dn}:{e.name}"
25
29
 
26
30
 
31
+ def value_at(project: Project, e: Elem, pv) -> str:
32
+ """file:line of a parameter's VALUE / VALUE-REF element (not its container)."""
33
+ n = pv.value_node if pv.value_node is not None else pv.node
34
+ return f"{project.rel(e.file)}:{n.line}"
35
+
36
+
37
+ def value_info(project: Project, e: Elem, pv) -> dict:
38
+ """One configured value with its exact location and, for integers, both bases."""
39
+ pd = _resolve_def(project.defs, pv.def_ref)
40
+ numerical = pv.tag == "ECUC-NUMERICAL-PARAM-VALUE"
41
+ out = {"value": to_json(pd, pv.value, pv.is_ref, numerical), "at": value_at(project, e, pv)}
42
+ nums = int_forms(pd, pv.value, pv.is_ref, numerical, pv.def_dest)
43
+ if nums:
44
+ out.update(nums)
45
+ return out
46
+
47
+
48
+ def _put(out: dict, name: str, items: list, multi: bool):
49
+ one = lambda xs: xs if multi else xs[0] # noqa: E731
50
+ out[name] = one([x["value"] for x in items])
51
+ out[f"@at:{name}"] = one([x["at"] for x in items])
52
+ if any("hex" in x for x in items):
53
+ out[f"@hex:{name}"] = one([x.get("hex") for x in items])
54
+ if any("dec" in x and x["value"] != x["dec"] for x in items):
55
+ out[f"@dec:{name}"] = one([x.get("dec") for x in items])
56
+
57
+
58
+ def annotated_values(project: Project, group: list) -> dict:
59
+ """{param: value, "@at:param": file:line, "@hex:param": ...} for the params of one element (all its files)."""
60
+ items: dict = {}
61
+ multi: dict = {}
62
+ for e in group:
63
+ for pv in e.params:
64
+ pd = _resolve_def(project.defs, pv.def_ref)
65
+ items.setdefault(pv.name, []).append(value_info(project, e, pv))
66
+ multi[pv.name] = multi.get(pv.name, False) or (pd is not None and (pd.upper is None or pd.upper > 1))
67
+ out: dict = {}
68
+ for name, xs in items.items():
69
+ _put(out, name, xs, multi[name] or len(xs) > 1)
70
+ return out
71
+
72
+
27
73
  def render(project: Project, group: list, depth: int, top: bool = True) -> dict:
28
- defs = project.defs
29
74
  e0 = group[0]
30
75
  out: dict = {"@path": e0.path}
31
76
  if top:
32
77
  out["@def"] = e0.def_ref
33
- out["@at"] = ", ".join(f"{project.rel(e.file)}:{e.line}" for e in group)
34
- vals: dict = {}
35
- for e in group:
36
- for pv in e.params:
37
- pd = _resolve_def(defs, pv.def_ref)
38
- v = to_json(pd, pv.value, pv.is_ref, pv.tag == "ECUC-NUMERICAL-PARAM-VALUE")
39
- name = pv.name
40
- multi = pd is not None and (pd.upper is None or pd.upper > 1)
41
- if name in vals:
42
- if not isinstance(vals[name], list) or not multi:
43
- vals[name] = vals[name] if isinstance(vals[name], list) else [vals[name]]
44
- vals[name].append(v)
45
- else:
46
- vals[name] = [v] if multi else v
47
- out.update(vals)
78
+ out["@at"] = ", ".join(f"{project.rel(e.file)}:{e.line}" for e in group)
79
+ out.update(annotated_values(project, group))
48
80
  seen = set()
49
81
  for e in group:
50
82
  for c in e.children:
@@ -61,6 +93,24 @@ def render(project: Project, group: list, depth: int, top: bool = True) -> dict:
61
93
  return out
62
94
 
63
95
 
96
+ def value_sites(project: Project, paths: list) -> dict:
97
+ """AUTOSAR path -> {"at": container file:line, "values": {param: {value, at, dec?, hex?} | [..]}} from the
98
+ local ECUC, or None when the element is not in the local checkout."""
99
+ out: dict = {}
100
+ for p in paths:
101
+ g = project.find(p)
102
+ if not g:
103
+ out[p] = None
104
+ continue
105
+ vals: dict = {}
106
+ for e in g:
107
+ for pv in e.params:
108
+ vals.setdefault(pv.name, []).append(value_info(project, e, pv))
109
+ out[p] = {"at": ", ".join(f"{project.rel(e.file)}:{e.line}" for e in g),
110
+ "values": {k: (v[0] if len(v) == 1 else v) for k, v in vals.items()}}
111
+ return out
112
+
113
+
64
114
  def read(project: Project, path: Optional[str], depth: int = 2, schema: bool = False) -> dict:
65
115
  res: dict = {"defsLoaded": len(project.defs.modules)}
66
116
  if project.warnings:
@@ -101,12 +151,15 @@ def read(project: Project, path: Optional[str], depth: int = 2, schema: bool = F
101
151
  parent, _, last = path.rstrip("/").rpartition("/")
102
152
  pg = project.find(parent)
103
153
  if pg:
104
- hits = [pv for e in pg for pv in e.params if pv.name == last]
154
+ hits = [(e, pv) for e in pg for pv in e.params if pv.name == last]
105
155
  if hits:
106
- pd = _resolve_def(project.defs, hits[0].def_ref)
107
- res["param"] = {"@path": path, "@def": hits[0].def_ref,
108
- "@at": [f"{project.rel(hits[0].node and pg[0].file)}:{h.node.line}" for h in hits],
109
- "value": [to_json(pd, h.value, h.is_ref, h.tag == "ECUC-NUMERICAL-PARAM-VALUE") for h in hits]}
156
+ pd = _resolve_def(project.defs, hits[0][1].def_ref)
157
+ info = [value_info(project, e, pv) for e, pv in hits]
158
+ res["param"] = {"@path": path, "@def": hits[0][1].def_ref,
159
+ "@at": [x["at"] for x in info], "value": [x["value"] for x in info]}
160
+ if any("hex" in x for x in info):
161
+ res["param"]["dec"] = [x.get("dec") for x in info]
162
+ res["param"]["hex"] = [x.get("hex") for x in info]
110
163
  if pd is not None:
111
164
  res["schema"] = D.describe(pd, project.defs)
112
165
  return res
@@ -73,7 +73,7 @@ def _refs(project: Project, e, depth: int) -> set:
73
73
 
74
74
  def find(project: Project, module: Optional[str] = None, container_def: Optional[str] = None, where=None,
75
75
  ref_to: Optional[str] = None, referenced_by: Optional[str] = None, limit: int = 50, depth: int = 2) -> dict:
76
- from .ecucjson import resolve
76
+ from .ecucjson import annotated_values, resolve
77
77
  conds = _where(where)
78
78
  targets = None
79
79
  if ref_to:
@@ -116,9 +116,11 @@ def find(project: Project, module: Optional[str] = None, container_def: Optional
116
116
  continue
117
117
  total += 1
118
118
  if len(hits) < limit:
119
- hits.append({"path": path, "def": dname, "at": f"{project.rel(e.file)}:{e.line}",
120
- "values": {k: (v[0] if len(v) == 1 else v) for k, v in own.items()}})
121
- return {"count": total, "returned": len(hits), "matches": hits}
119
+ hits.append({"path": path, "def": dname, "at": ", ".join(f"{project.rel(x.file)}:{x.line}" for x in group),
120
+ "values": annotated_values(project, group)})
121
+ return {"count": total, "returned": len(hits), "matches": hits,
122
+ "legend": "at = container element; values[\"@at:P\"] = file:line of P's VALUE element; "
123
+ "@hex:/@dec: = the same integer in the other base"}
122
124
 
123
125
 
124
126
  ELEMENTS = {