@h2loop/autosar 1.1.0 → 1.2.1

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.
@@ -18,7 +18,7 @@ import re
18
18
  from typing import Optional
19
19
 
20
20
  from . import paths
21
- from .arxml_edit import ArxmlEditError, ArxmlEditor
21
+ from .arxml_edit import ArxmlEditError, ArxmlEditor, _ad, _etype, _version
22
22
  from .model import Project
23
23
  from .rules import RuleContext
24
24
  from .rules.com import _layout, bits
@@ -102,11 +102,11 @@ def add_signal(project: Project, name: str, ipdu: str, bit_size: Optional[int] =
102
102
  # ---------- system extract ----------
103
103
  sysi = project.system
104
104
  ref_pv = next((pv for g in project.find(sib.path) for pv in g.params if pv.is_ref and sysi.get(pv.value or "")), None)
105
- sys_writes, new_mapping = {}, None
105
+ sys_writes, new_mapping, not_cloned = {}, None, []
106
106
  if ref_pv is not None and sysi.by_path:
107
107
  ed = ArxmlEditor(project)
108
108
  try:
109
- new_mapping = _clone_system_chain(project, ed, sysi.get(ref_pv.value), sib.name, name)
109
+ new_mapping = _clone_system_chain(project, ed, sysi.get(ref_pv.value), sib.name, name, not_cloned)
110
110
  m_path = new_mapping
111
111
  ed.op_set({"path": m_path, "element": "START-POSITION", "value": pos})
112
112
  ed.op_set({"path": m_path, "element": "PACKING-BYTE-ORDER", "value": ORDER.get(endianness, endianness)})
@@ -127,6 +127,11 @@ def add_signal(project: Project, name: str, ipdu: str, bit_size: Optional[int] =
127
127
  if probs:
128
128
  raise AddSignalError("system extract edit would leave: " + "; ".join(probs[:5]))
129
129
  sys_writes = {f: ft.data for f, ft in ed.files.items() if ft.data != ed.original[f]}
130
+ for sk in not_cloned:
131
+ notes.append(f"not copied from {sib.name}: its {sk['ref']} in {sk['element']} ({sk['holder']}) is a "
132
+ f"single-valued reference of the template's own relation. If {name} needs the same kind of "
133
+ f"relation (e.g. a SWC data element mapped to it), add a new {sk['element']} with "
134
+ f"{sk['ref']} = {sk['new']} (arxml_edit)")
130
135
  else:
131
136
  notes.append("no system-extract link on the template signal; extract not changed")
132
137
 
@@ -188,7 +193,8 @@ def add_signal(project: Project, name: str, ipdu: str, bit_size: Optional[int] =
188
193
  ecuc_writes = {f: project.files[f].data for f in ced.original if project.files[f].data != ced.original[f]}
189
194
 
190
195
  # ---------- DBC ----------
191
- dbc_writes = _dbc_line(project, pdu, name, pos, size, endianness, signal_type, notes)
196
+ dbc_writes = _dbc_line(project, pdu, name, pos, size, endianness, signal_type, notes,
197
+ {m.name for m in members if m is not None})
192
198
 
193
199
  writes = {**sys_writes, **ecuc_writes, **dbc_writes}
194
200
  originals = {f: open(f, "rb").read() for f in writes}
@@ -200,7 +206,8 @@ def add_signal(project: Project, name: str, ipdu: str, bit_size: Optional[int] =
200
206
  for f, b in writes.items():
201
207
  with open(f, "wb") as fh:
202
208
  fh.write(b)
203
- after = validate(Project(project.root), check_generated=False)
209
+ # same inputs as `before` (explicit --defs/--config included), so only the edit's own findings count as new
210
+ after = validate(Project(project.root, project.sources.config, project.sources.defs), check_generated=False)
204
211
  new = [d for d in after["diagnostics"] if d["severity"] in ("error", "warning")
205
212
  and (d["rule"], d["path"], d["message"]) not in before]
206
213
  if new:
@@ -247,8 +254,24 @@ def _renamed(path, old: str, new: str) -> Optional[str]:
247
254
  return f"{head}/{last.replace(old, new)}" if old in last else None
248
255
 
249
256
 
250
- def _clone_system_chain(project: Project, ed: ArxmlEditor, mapping, old: str, new: str) -> str:
251
- """Clone the sibling's extract chain under the new name; return the new mapping path."""
257
+ def _repeatable(ft, wrap) -> bool:
258
+ """May wrap's parent hold another element like wrap? Schema content mode when autosar-data is available
259
+ (Bag = collection, Sequence/Choice = single slots); otherwise AUTOSAR's plural-wrapper naming or an existing
260
+ repetition of the same tag."""
261
+ parent = wrap.parent
262
+ if parent is None:
263
+ return False
264
+ et = _etype(ft.chain(parent), _version(ft.data))
265
+ if et is not None:
266
+ return et.content_mode == _ad().ContentMode.Bag
267
+ return parent.tag.endswith("S") or sum(c.tag == wrap.tag for c in parent.children) > 1
268
+
269
+
270
+ def _clone_system_chain(project: Project, ed: ArxmlEditor, mapping, old: str, new: str,
271
+ skipped: Optional[list] = None) -> str:
272
+ """Clone the sibling's extract chain under the new name; return the new mapping path. Reference entries
273
+ that cannot be duplicated (single-valued slots) are appended to `skipped` instead."""
274
+ skipped = [] if skipped is None else skipped
252
275
  sysi = project.system
253
276
  if mapping is None or mapping.tag != "I-SIGNAL-TO-I-PDU-MAPPING":
254
277
  raise ArxmlEditError("template signal's system reference is not an I-SIGNAL-TO-I-PDU-MAPPING")
@@ -293,7 +316,10 @@ def _clone_system_chain(project: Project, ed: ArxmlEditor, mapping, old: str, ne
293
316
  ed.patch(ft, node.end, node.end, ft.nl + ind + text.encode())
294
317
  if p == mapping.path:
295
318
  mapping_new = subst[p]
296
- # reference entries held by shared elements (not renamed): clone the wrapper next to the original
319
+ # reference entries held by shared elements (not renamed): clone the wrapper next to the original, but only
320
+ # where the holder is a collection (FIBEX-ELEMENTS, I-SIGNAL-TRIGGERINGS, ...). A reference in a single-valued
321
+ # slot (SYSTEM-SIGNAL-REF of a SENDER-RECEIVER-TO-SIGNAL-MAPPING) belongs to the sibling's own relation, e.g.
322
+ # its SWC data element; duplicating it is a schema error and the relation does not carry over to a new signal.
297
323
  for p in chain:
298
324
  for r in sysi.refs_to(p):
299
325
  if r.path in chain:
@@ -306,6 +332,10 @@ def _clone_system_chain(project: Project, ed: ArxmlEditor, mapping, old: str, ne
306
332
  while wrap.parent is not None and wrap.parent is not node and len(wrap.parent.children) == 1 \
307
333
  and not wrap.parent.tag.endswith("S"):
308
334
  wrap = wrap.parent
335
+ if not _repeatable(ft, wrap):
336
+ skipped.append({"holder": r.path, "element": wrap.parent.tag if wrap.parent is not None else node.tag,
337
+ "ref": leaf.tag, "template": p, "new": subst[p]})
338
+ continue
309
339
  text = ft.data[wrap.start:wrap.end].decode().replace(f">{p}<", f">{subst[p]}<")
310
340
  ind = line_indent(ft.data, wrap.start) or b""
311
341
  ed.patch(ft, wrap.end, wrap.end, ft.nl + ind + text.encode())
@@ -314,7 +344,15 @@ def _clone_system_chain(project: Project, ed: ArxmlEditor, mapping, old: str, ne
314
344
  return mapping_new
315
345
 
316
346
 
317
- def _dbc_line(project: Project, pdu, name, pos, size, endian, typ, notes) -> dict:
347
+ def _frame_signals(text: str, m) -> set:
348
+ body = text[m.end():].split("\n\n", 1)[0]
349
+ return set(re.findall(r"^\s*SG_ (\w+)", body, re.M))
350
+
351
+
352
+ def _dbc_line(project: Project, pdu, name, pos, size, endian, typ, notes, members: set = frozenset()) -> dict:
353
+ """Add an SG_ line to the DBC frame of the I-PDU. CAN IDs are unique per bus only, so when several DBC files
354
+ have a frame with the ID, the frame that already carries the I-PDU's signals is the one; a frame that carries
355
+ none of them while another does belongs to another bus and is left alone."""
318
356
  from .audit import can_info
319
357
  infos = can_info(project, pdu.path if pdu is not None else None)
320
358
  dbcs = paths.walk(project.root, lambda n: n.lower().endswith(".dbc")).files
@@ -325,11 +363,21 @@ def _dbc_line(project: Project, pdu, name, pos, size, endian, typ, notes) -> dic
325
363
  cid = info.get("canId")
326
364
  if cid is None:
327
365
  continue
366
+ cands = []
328
367
  for f in dbcs:
329
368
  text = open(f, encoding="utf-8", errors="replace").read()
330
369
  m = re.search(rf"^BO_ ({cid}|{cid | 0x80000000}) (\w+)\s*:.*$", text, re.M)
331
- if not m:
332
- continue
370
+ if m:
371
+ cands.append((f, text, m))
372
+ owning = [c for c in cands if members & _frame_signals(c[1], c[2])]
373
+ if owning:
374
+ cands = owning
375
+ elif len(cands) > 1:
376
+ notes.append(f"DBC not changed: {len(cands)} frames have CAN ID {cid} "
377
+ f"({', '.join(project.rel(c[0]) + ':' + c[2].group(2) for c in cands)}) and none carries "
378
+ f"the I-PDU's signals")
379
+ cands = []
380
+ for f, text, m in cands:
333
381
  block_end = m.end()
334
382
  sg = list(re.finditer(r"^\s*SG_ .*$", text[m.end():].split("\n\n", 1)[0], re.M))
335
383
  recv = "Vector__XXX"
@@ -22,7 +22,7 @@ from dataclasses import dataclass
22
22
  from typing import Optional
23
23
 
24
24
  from .model import Elem, Project
25
- from .values import parse_int
25
+ from .values import hex_str, parse_int
26
26
 
27
27
  from . import paths
28
28
 
@@ -261,9 +261,31 @@ class Auditor:
261
261
  return list(self._owners.get(key, []))
262
262
 
263
263
  # ---------- findings ----------
264
+ def located(self, evidence):
265
+ """Evidence plus, computed (never left to the reader): <k>At = file:line of each configured element named
266
+ by AUTOSAR path, <Param>At = file:line of that parameter's VALUE element, and <k>Hex for integers."""
267
+ if not isinstance(evidence, dict):
268
+ return evidence
269
+ from .ecucjson import value_at
270
+ out = dict(evidence)
271
+ elems = []
272
+ for k, v in evidence.items():
273
+ if isinstance(v, str) and v.startswith("/"):
274
+ g = self.p.find(v)
275
+ if g:
276
+ out[f"{k}At"] = ", ".join(f"{self.p.rel(e.file)}:{e.line}" for e in g)
277
+ elems.extend(g)
278
+ for k, v in evidence.items():
279
+ if isinstance(v, int) and not isinstance(v, bool) and not k.lower().endswith("line"):
280
+ out[f"{k}Hex"] = hex_str(v)
281
+ sites = [value_at(self.p, e, pv) for e in elems for pv in e.params if pv.name == k]
282
+ if sites and f"{k}At" not in out:
283
+ out[f"{k}At"] = sites[0] if len(sites) == 1 else sites
284
+ return out
285
+
264
286
  def add(self, rule, sev, f, line, call, msg, cite, evidence, fix=None):
265
287
  d = {"rule": rule, "severity": sev, "file": self.p.rel(f), "line": line, "call": call,
266
- "message": msg, "cite": cite, "evidence": evidence}
288
+ "message": msg, "cite": cite, "evidence": self.located(evidence)}
267
289
  if fix:
268
290
  d["fix"] = fix
269
291
  self.findings.append(d)
@@ -82,6 +82,11 @@ def main(argv=None) -> int:
82
82
  cx.add_argument("module")
83
83
  so = sub.add_parser("signal-owners")
84
84
  so.add_argument("paths", nargs="+")
85
+ vs = sub.add_parser("value-sites", help="per AUTOSAR path: each value with its VALUE element file:line, dec/hex")
86
+ vs.add_argument("paths", nargs="+")
87
+ si = sub.add_parser("spec-ids", help="check requirement IDs against the configured spec text docs")
88
+ si.add_argument("ids", nargs="+")
89
+ si.add_argument("--docs", action="append", help="spec text file/dir/glob (default: specDocs / $AUTOSAR_SPEC_DOCS)")
85
90
  d = sub.add_parser("diff")
86
91
  d.add_argument("a")
87
92
  d.add_argument("b")
@@ -111,7 +116,7 @@ def main(argv=None) -> int:
111
116
  bc.add_argument("--task", required=True, help="task text, @file, or - for stdin")
112
117
  bc.add_argument("--budget-tokens", dest="budget_tokens", type=int, default=24000)
113
118
  bc.add_argument("--no-history", dest="history", action="store_false")
114
- for p in (r, e, v, d, au, so, fd, cx, ae, asg, cg, cd, ra, vr, bc): # allow --json after the subcommand too
119
+ for p in (r, e, v, d, au, so, vs, si, fd, cx, ae, asg, cg, cd, ra, vr, bc): # allow --json after the subcommand too
115
120
  p.add_argument("--json", action="store_true", help=argparse.SUPPRESS)
116
121
  a = ap.parse_args(argv)
117
122
  t0 = time.time()
@@ -169,6 +174,9 @@ def run(a) -> dict:
169
174
  task = fh.read()
170
175
  return build_context(os.path.abspath(a.root), a.swc, task, runnable=a.runnable,
171
176
  budget_tokens=a.budget_tokens, history=a.history)
177
+ if a.cmd == "spec-ids":
178
+ from .specids import check_ids
179
+ return check_ids(os.path.abspath(a.root), a.ids, docs=a.docs)
172
180
  from .model import Project
173
181
  project = Project(a.root, config_files=a.config, def_files=a.defs, load_input=a.cmd in ("validate", "edit", "find", "crosscheck", "signal-owners", "arxml-edit", "add-signal"))
174
182
  if a.cmd == "add-signal":
@@ -198,6 +206,9 @@ def run(a) -> dict:
198
206
  if project.warnings:
199
207
  res["warnings"] = project.warnings
200
208
  return res
209
+ if a.cmd == "value-sites":
210
+ from .ecucjson import value_sites
211
+ return {"sites": value_sites(project, a.paths)}
201
212
  if a.cmd == "signal-owners":
202
213
  from .audit import signal_owners
203
214
  return {"owners": signal_owners(project, a.paths)}
@@ -103,25 +103,44 @@ def generate(project: Project, modules: Optional[list] = None, out_dir: Optional
103
103
  wb = H.apply(project, pl["assign"], write=write_back)
104
104
  if pl["assign"]:
105
105
  cg = CgModel(project, opts.get("mip"))
106
+ # handle layout the static code indexes by (binding "handleLayout"), only for withAuto handle params
107
+ proj_out = resolve_out(root, "project", opts)
108
+ headers, sources = _scan_c(root, [out, proj_out, os.path.join(root, ".hydron")])
109
+ bindings = B.load_all(root)
110
+ lay = H.layout(cg, want, {m: B.select(bindings, m, headers, binding) for m in want})
111
+ if lay["errors"]:
112
+ return {"ok": False, "error": "handle IDs do not fit the static code's table layout", "handleErrors": lay["errors"]}
113
+ if lay["assign"]:
114
+ wb2 = H.apply(project, lay["assign"], write=write_back)
115
+ wb = {"written": wb["written"] or wb2["written"], "changes": wb["changes"] + wb2["changes"]}
116
+ pl["assign"] += lay["assign"]
117
+ cg = CgModel(project, opts.get("mip"))
118
+ pl["notes"] += lay["notes"]
106
119
  timings["handles_ms"] = int((time.time() - t1) * 1000)
107
120
 
108
121
  # 2. emit
109
122
  t2 = time.time()
110
- proj_out = resolve_out(root, "project", opts)
111
- headers, sources = _scan_c(root, [out, proj_out, os.path.join(root, ".hydron")])
112
- ctx = Ctx(cg=cg, headers=headers, bindings=B.load_all(root), binding_name=binding, release=cg.release(),
123
+ ctx = Ctx(cg=cg, headers=headers, bindings=bindings, binding_name=binding, release=cg.release(),
113
124
  sources=sources)
114
125
  ctx.options = opts
115
126
  ctx.generating = set(want)
116
127
  ctx.available = set(headers) | (set(os.listdir(proj_out)) if os.path.isdir(proj_out) else set())
128
+ ctx.out_headers = {f: os.path.join(proj_out, f) for f in sorted(os.listdir(proj_out))
129
+ if f.endswith(".h")} if os.path.isdir(proj_out) else {}
117
130
  files: dict = {}
118
131
  report: dict = {}
119
132
  memmaps = []
120
133
  notes = list(pl["notes"])
134
+ errors: dict = {}
135
+ outputs = []
121
136
  for name in want:
122
137
  mc = cg.module(name)
123
138
  gen = generator_for(name)(ctx, mc)
124
139
  mo = gen.run()
140
+ if mo.errors:
141
+ errors[name] = mo.errors
142
+ continue
143
+ outputs.append(mo)
125
144
  rendered = mo.render()
126
145
  n, text = S.schm_header(ctx, mo)
127
146
  rendered[n] = text
@@ -146,6 +165,11 @@ def generate(project: Project, modules: Optional[list] = None, out_dir: Optional
146
165
  notes.append(f"project MemMap.h ({P_rel(root, headers['MemMap.h'])}) kept; generated code includes "
147
166
  "<Mip>_MemMap.h (SWS_MemMap_00032)")
148
167
  timings["emit_ms"] = int((time.time() - t2) * 1000)
168
+ if errors:
169
+ return {"ok": False, "error": "the configuration cannot be represented by the static code's binding; "
170
+ "nothing was written", "moduleErrors": errors, "notes": notes,
171
+ "handles": {"assigned": pl["assign"], "writtenBack": wb["written"], "changes": wb["changes"]}}
172
+ notes += dangling_symbols(ctx, outputs)
149
173
 
150
174
  # 3. write
151
175
  os.makedirs(out, exist_ok=True)
@@ -166,6 +190,30 @@ def generate(project: Project, modules: Optional[list] = None, out_dir: Optional
166
190
  "notes": notes, "timings": timings, "release": ".".join(map(str, ctx.release))}
167
191
 
168
192
 
193
+ def dangling_symbols(ctx, outputs: list) -> list:
194
+ """Symbolic names (<Mip>Conf_<Def>_<Name>) that the project's C uses but the configuration no longer has.
195
+ The old generated header may still define them, so the stale use only shows once this output replaces it."""
196
+ defined = {s for mo in outputs for s, _, _ in mo.symbols}
197
+ rx = re.compile(r"\b(" + "|".join(sorted(re.escape(mo.mip) for mo in outputs)) + r")Conf_\w+") if outputs else None
198
+ out = []
199
+ if rx is None:
200
+ return out
201
+ seen: dict = {}
202
+ for f in ctx.sources:
203
+ try:
204
+ with open(f, errors="replace") as fh:
205
+ text = fh.read()
206
+ except OSError:
207
+ continue
208
+ for m in rx.finditer(text):
209
+ if m.group(0) not in defined:
210
+ seen.setdefault(m.group(0), ctx.cg.p.rel(f))
211
+ for sym in sorted(seen):
212
+ out.append(f"{seen[sym]} uses {sym}, which the configuration does not define (container deleted or "
213
+ "renamed?); the build will fail on it")
214
+ return out
215
+
216
+
169
217
  def P_rel(root: str, f: str) -> str:
170
218
  r = os.path.relpath(f, root)
171
219
  return f if r.startswith("..") else r
@@ -14,12 +14,24 @@ Path language (a list of steps, evaluated left to right from the row container):
14
14
  sub:<Def> first sub-container of def <Def>
15
15
  ref:<R> follow ECUC reference R to its target container
16
16
  back:<Def>/<R> the container of def <Def> (any module) whose reference R points here
17
- peer:<R>[@<Module>] follow R to target T, then the container that references T from another
18
- module (or from <Module>): how a module finds its neighbour's PDU handle
19
- (TPS_ECUC 3.4.1.3, via EcucPduCollection/Pdu)
17
+ peer:<R>[@<Module>[/<Def>]] follow R to target T, then the container that references T from another
18
+ module (or from <Module>, optionally only containers of def <Def>): how a module
19
+ finds its neighbour's PDU handle (TPS_ECUC 3.4.1.3, via EcucPduCollection/Pdu)
20
+ call:<name> a module generator's resolver (Generator.path_steps()) for what the defs cannot
21
+ express as a path, e.g. Com's notification callouts (terminal, token)
20
22
  sym TPS_ECUC_02108 symbol of the current container (terminal)
21
23
  module | name | index the current container's module name / SHORT-NAME / table index (terminal)
22
- Macro values: count:<Def> | max:<Def>/<Param>, optionally followed by +N or -N.
24
+ Macro values: count:<Def> | count:@<Set> | max:<Def>/<Param>, optionally followed by +N or -N.
25
+
26
+ Module binding keys besides tables/objects/macros:
27
+ sets {"<Set>": {"rows": "<Def>", "where": [{"path": <path>, "in": [literal, ...]}]}}: named row
28
+ selections (e.g. the Tx signals: ComSignals whose I-PDU is SEND); tables use "rows": "@<Set>"
29
+ vars RAM objects per row that tables point to (e.g. I-PDU buffers), sized by a path
30
+ require [{"rows": "<Def>", "path": <path>, "in": [...], "why": "..."}]: what the static code supports;
31
+ a configuration outside it is an error, never silently mis-generated
32
+ Field keys: path, default, enum (literal -> C token), fmt ("hex" | "int" | template with {} / {MIP} / {Mip}),
33
+ divide (a path from the row whose value divides this one, rounded: seconds -> main-function ticks),
34
+ agg ({"over": <multi-ref>, "params": [...], "op": "min_nonzero" | "max", "skipIf": <param>}).
23
35
  """
24
36
  from __future__ import annotations
25
37
 
@@ -91,6 +103,7 @@ class Eval:
91
103
  self.mc = mc
92
104
  self.rank = 1
93
105
  self.used_mips: set = set()
106
+ self.ext: dict = {} # call:<name> resolvers contributed by the module generator
94
107
 
95
108
  def steps(self, start: Container, steps) -> tuple:
96
109
  """-> ("container", Container) | ("value", (raw, def, owner)) | ("token", str) | (None, None)"""
@@ -124,12 +137,14 @@ class Eval:
124
137
  self._touch(cur, self.cg.param_def(cur, rname))
125
138
  elif op == "peer":
126
139
  rname, _, want = arg.partition("@")
140
+ want, _, wdef = want.partition("/")
127
141
  self._touch(cur, self.cg.param_def(cur, rname))
128
142
  t = self.cg.target(cur.value(rname))
129
143
  if t is None:
130
144
  return (None, None)
131
145
  hits = [c for c in self.cg.referrers(t) if c is not cur and
132
- (c.module.name == want if want else c.module is not cur.module)]
146
+ (c.module.name == want if want else c.module is not cur.module) and
147
+ (not wdef or c.def_name == wdef)]
133
148
  hits.sort(key=lambda c: (c.module.name, self.cg.by_path_index(c)))
134
149
  cur = hits[0] if hits else None
135
150
  elif op == "sym":
@@ -137,6 +152,12 @@ class Eval:
137
152
  if s and self.ctx is not None:
138
153
  s = self.ctx.xsym(cur, self.mc, self.used_mips)
139
154
  return ("token", s) if s else (None, None)
155
+ elif op == "call":
156
+ fn = self.ext.get(arg)
157
+ if fn is None:
158
+ raise BindingError(f"path step call:{arg}: the {self.mc.name} generator provides no such resolver")
159
+ v = fn(cur, self)
160
+ return ("token", v) if v else (None, None)
140
161
  elif op == "module":
141
162
  return ("token", cur.module.mip)
142
163
  elif op == "name":
@@ -149,6 +170,32 @@ class Eval:
149
170
  return (None, None)
150
171
  return ("container", cur)
151
172
 
173
+ def rows(self, spec: str, sets: dict) -> list:
174
+ """Rows of a table: a container def (ordered by symbolic handle) or a named set "@<Set>"."""
175
+ if not spec.startswith("@"):
176
+ return list(self.cg.sorted_instances(self.mc, spec))
177
+ st = sets.get(spec[1:])
178
+ if st is None:
179
+ raise BindingError(f"unknown row set {spec} (sets: {', '.join(sorted(sets)) or 'none'})")
180
+ return [r for r in self.cg.sorted_instances(self.mc, st["rows"]) if self.matches(r, st.get("where", []))]
181
+
182
+ def raw(self, row: Container, path) -> Optional[str]:
183
+ kind, v = self.steps(row, path)
184
+ if kind == "value":
185
+ return (v[0] or "").strip() if v[0] is not None else None
186
+ if kind == "token":
187
+ return v
188
+ return None
189
+
190
+ def matches(self, row: Container, where: list) -> bool:
191
+ for w in where:
192
+ v = self.raw(row, w["path"])
193
+ if "in" in w and v not in w["in"]:
194
+ return False
195
+ if "notIn" in w and v in w["notIn"]:
196
+ return False
197
+ return True
198
+
152
199
  def _touch(self, c: Container, d):
153
200
  if c.module is self.mc:
154
201
  self.rank = max(self.rank, self.cg.eff_class(self.mc, d))
@@ -161,6 +208,8 @@ class Eval:
161
208
  ok = kind == "value" and (v[0] or "").strip().lower() in ("true", "1")
162
209
  if not ok:
163
210
  return (spec.get("default", "0u"), "")
211
+ if "agg" in spec:
212
+ return self._agg(row, spec)
164
213
  paths = spec["path"]
165
214
  alts = paths if isinstance(paths, list) and paths and isinstance(paths[0], list) else [paths]
166
215
  kind, v = None, None
@@ -182,23 +231,69 @@ class Eval:
182
231
  s = self.ctx.xsym(v, self.mc, self.used_mips)
183
232
  return (s, "")
184
233
  if kind == "token":
185
- if fmt:
186
- return (fmt.replace("{MIP}", v.upper()).replace("{Mip}", v), "")
234
+ if fmt and fmt not in ("hex", "int"):
235
+ return (fmt.replace("{MIP}", v.upper()).replace("{Mip}", v).replace("{}", v), "")
187
236
  return (v, "")
188
237
  raw, d, owner = v
189
238
  if "enum" in spec and raw is not None:
190
239
  m = spec["enum"]
191
240
  if raw.strip() not in m:
192
- raise BindingError(f"{row.path}: {spec['path']} literal {raw} not in the binding's enum map")
193
- return (str(m[raw.strip()]), raw.strip())
241
+ raise BindingError(f"{row.path}: {spec['path']} = {raw.strip()} is not supported by the static code "
242
+ f"(the {self.mc.name} binding maps only {', '.join(m)})")
243
+ e = m[raw.strip()]
244
+ return (W.c_uint(e) if isinstance(e, int) else str(e), raw.strip())
245
+ if "divide" in spec:
246
+ return (self._ticks(row, raw, spec), "")
247
+ if fmt == "int": # a numeric value held in a STRING param (e.g. ComSignalInitValue)
248
+ n = parse_int(raw.strip())
249
+ if n is None:
250
+ raise BindingError(f"{row.path}: {spec['path']} = {raw.strip()!r} is not an integer; the static code's "
251
+ f"{spec.get('name')} field cannot hold it")
252
+ return (W.c_uint(n), "")
194
253
  return W.table_value(d, raw, hexa=fmt == "hex")
195
254
 
196
- def macro(self, expr: str) -> str:
197
- m = re.match(r"^(count|max):([\w]+)(?:/(\w+))?\s*([+-]\s*\d+)?$", expr.strip())
255
+ def _ticks(self, row: Container, raw, spec: dict) -> str:
256
+ from ..values import parse_float
257
+ v = parse_float(str(raw).strip())
258
+ base = self.raw(row, spec["divide"])
259
+ b = parse_float(base) if base is not None else None
260
+ if v is None or not b:
261
+ raise BindingError(f"{row.path}: cannot convert {spec.get('name')} = {raw} to ticks of {spec['divide']} = {base}")
262
+ return W.c_uint(int(round(v / b)))
263
+
264
+ def _agg(self, row: Container, spec: dict) -> tuple:
265
+ """Aggregate params over the targets of a multi-valued reference of the row (e.g. the smallest non-zero
266
+ timeout of an I-PDU's signals, SWS_Com_00291)."""
267
+ from ..values import parse_float
268
+ a = spec["agg"]
269
+ vals = []
270
+ for pv in row.values(a["over"]):
271
+ t = self.cg.target(pv)
272
+ if t is None or (a.get("skipIf") and t.value(a["skipIf"]) is not None):
273
+ continue
274
+ for p in a["params"]:
275
+ x = parse_float((t.text(p) or "").strip())
276
+ if x is not None:
277
+ vals.append(x)
278
+ if a["op"] == "min_nonzero":
279
+ vals = [x for x in vals if x > 0]
280
+ v = min(vals) if vals else None
281
+ elif a["op"] == "max":
282
+ v = max(vals) if vals else None
283
+ else:
284
+ raise BindingError(f"unknown aggregate {a['op']!r}")
285
+ if v is None:
286
+ return (spec.get("default", "0u"), "")
287
+ if "divide" in spec:
288
+ return (self._ticks(row, repr(v), spec), "")
289
+ return (W.c_float(repr(v)), "")
290
+
291
+ def macro(self, expr: str, sets: Optional[dict] = None) -> str:
292
+ m = re.match(r"^(count|max):(@?[\w]+)(?:/(\w+))?\s*([+-]\s*\d+)?$", expr.strip())
198
293
  if not m:
199
- raise BindingError(f"bad macro expression {expr!r} (count:<Def> | max:<Def>/<Param> [+N])")
294
+ raise BindingError(f"bad macro expression {expr!r} (count:<Def> | count:@<Set> | max:<Def>/<Param> [+N])")
200
295
  op, dname, pname, off = m.groups()
201
- insts = self.cg.instances(self.mc, dname)
296
+ insts = self.rows(dname, sets or {}) if dname.startswith("@") else self.cg.instances(self.mc, dname)
202
297
  if op == "count":
203
298
  v = len(insts)
204
299
  else: