@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.
- package/README.md +7 -1
- package/agents/autosar-integrator.md +11 -0
- package/commands/autosar-trace.md +2 -2
- package/package.json +1 -1
- package/plugins/index.js +47 -45
- package/python/ecuc_engine/__init__.py +1 -1
- package/python/ecuc_engine/add_signal.py +59 -11
- package/python/ecuc_engine/audit.py +24 -2
- package/python/ecuc_engine/cli.py +12 -1
- package/python/ecuc_engine/codegen/__init__.py +51 -3
- package/python/ecuc_engine/codegen/binding.py +108 -13
- package/python/ecuc_engine/codegen/bindings/sip_standin.json +304 -37
- package/python/ecuc_engine/codegen/emit.py +73 -5
- package/python/ecuc_engine/codegen/handles.py +95 -8
- package/python/ecuc_engine/codegen/model.py +6 -1
- package/python/ecuc_engine/codegen/modules/com.py +145 -0
- package/python/ecuc_engine/codegen/registry.py +92 -17
- package/python/ecuc_engine/ecucjson.py +75 -22
- package/python/ecuc_engine/find.py +6 -4
- package/python/ecuc_engine/specids.py +163 -0
- package/python/ecuc_engine/values.py +26 -0
- package/python/pyproject.toml +1 -1
- package/skills/autosar/SKILL.md +31 -5
- package/skills/autosar/references/codegen.md +28 -0
- package/skills/autosar/references/ecuc-model.md +4 -1
- package/skills/autosar/references/project-layout.md +4 -1
|
@@ -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
|
-
|
|
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
|
|
251
|
-
"""
|
|
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
|
|
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
|
|
332
|
-
|
|
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
|
-
|
|
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>]
|
|
18
|
-
module (or from <Module>): how a module
|
|
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']}
|
|
193
|
-
|
|
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
|
|
197
|
-
|
|
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:
|