@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.
- package/README.md +11 -1
- package/agents/autosar-integrator.md +45 -2
- package/commands/autosar-trace.md +2 -2
- package/package.json +4 -1
- package/plugins/index.js +55 -23
- package/python/ecuc_engine/__init__.py +1 -1
- package/python/ecuc_engine/add_signal.py +351 -0
- package/python/ecuc_engine/arxml_edit.py +387 -0
- package/python/ecuc_engine/audit.py +352 -50
- package/python/ecuc_engine/cli.py +125 -5
- package/python/ecuc_engine/codegen/__init__.py +233 -0
- package/python/ecuc_engine/codegen/binding.py +305 -0
- package/python/ecuc_engine/codegen/bindings/sip_standin.json +378 -0
- package/python/ecuc_engine/codegen/cdiff.py +395 -0
- package/python/ecuc_engine/codegen/cwriter.py +150 -0
- package/python/ecuc_engine/codegen/emit.py +463 -0
- package/python/ecuc_engine/codegen/handles.py +176 -0
- package/python/ecuc_engine/codegen/mcal/tc4d.json +60 -0
- package/python/ecuc_engine/codegen/model.py +264 -0
- package/python/ecuc_engine/codegen/modules/__init__.py +7 -0
- package/python/ecuc_engine/codegen/modules/com.py +145 -0
- package/python/ecuc_engine/codegen/modules/comstack.py +14 -0
- package/python/ecuc_engine/codegen/modules/dem.py +22 -0
- package/python/ecuc_engine/codegen/modules/mcal.py +170 -0
- package/python/ecuc_engine/codegen/modules/memstack.py +24 -0
- package/python/ecuc_engine/codegen/registry.py +208 -0
- package/python/ecuc_engine/codegen/schm_memmap.py +159 -0
- package/python/ecuc_engine/context.py +481 -0
- package/python/ecuc_engine/csrc.py +189 -0
- package/python/ecuc_engine/defs.py +30 -0
- package/python/ecuc_engine/ecucjson.py +75 -22
- package/python/ecuc_engine/find.py +164 -0
- package/python/ecuc_engine/gen.py +105 -0
- package/python/ecuc_engine/history.py +146 -0
- package/python/ecuc_engine/model.py +4 -1
- package/python/ecuc_engine/probe.py +229 -0
- package/python/ecuc_engine/rte_api.py +935 -0
- package/python/ecuc_engine/rules/__init__.py +5 -3
- package/python/ecuc_engine/rules/mem.py +84 -0
- package/python/ecuc_engine/rules/nvm.py +131 -0
- package/python/ecuc_engine/rules/pdur.py +87 -0
- package/python/ecuc_engine/rules/system.py +162 -0
- package/python/ecuc_engine/specids.py +163 -0
- package/python/ecuc_engine/sysmodel.py +152 -0
- package/python/ecuc_engine/validate.py +15 -2
- package/python/ecuc_engine/values.py +26 -0
- package/python/ecuc_engine/verify_rte.py +445 -0
- package/python/pyproject.toml +1 -1
- package/skills/autosar/SKILL.md +78 -6
- package/skills/autosar/references/codegen.md +58 -0
- package/skills/autosar/references/ecuc-model.md +4 -1
- package/skills/autosar/references/project-layout.md +10 -1
- package/skills/autosar/references/runnable-code.md +50 -0
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
"""Deterministic context assembly for "implement / change a runnable" tasks (autosar_build_context).
|
|
2
|
+
|
|
3
|
+
Fixed section order:
|
|
4
|
+
1 TASK the task text, verbatim
|
|
5
|
+
2 SWC DEFINITION ports, interfaces, data types, runnables, events (from ARXML, by reference)
|
|
6
|
+
3 ALLOWED RTE API for the runnable(s): exact declaration in Rte_<Swc>.h when present, else the
|
|
7
|
+
SWS_RTE signature from the model; never dropped or truncated
|
|
8
|
+
4 RUNNABLE CODE the entry function body from the C sources (local parse)
|
|
9
|
+
5 CALLEES functions it reaches: bodies of in-house ones, prototypes of the rest
|
|
10
|
+
6 RELATED CONFIG Com signals / NvM blocks behind its ports and calls (ECUC values + file:line)
|
|
11
|
+
7 FUZZY: SIMILAR FUNCTIONS in other SW-Cs, by lexical/structural similarity of RTE API + call names
|
|
12
|
+
8 FUZZY: SIMILAR PAST CHANGES git-history-lite (history.similar_changes)
|
|
13
|
+
|
|
14
|
+
Budget (about 4 characters per token): drop 8, then 7, then reduce 5 to prototypes, truncate 4, truncate 6,
|
|
15
|
+
reduce 5 to names, cut 4 to its head, compact 2. Sections 1 and 3 are never reduced. Same inputs -> byte-identical
|
|
16
|
+
output: everything is sorted, paths are project-relative, nothing time- or environment-dependent is emitted.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import hashlib
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
import re
|
|
24
|
+
from typing import Optional
|
|
25
|
+
|
|
26
|
+
from . import paths
|
|
27
|
+
from .csrc import CIndex, Func
|
|
28
|
+
from .history import similar_changes, terms_of
|
|
29
|
+
from .rte_api import ArIndex, SwcModel, build_all, find_header, find_swc, header_entries
|
|
30
|
+
|
|
31
|
+
CHARS_PER_TOKEN = 4
|
|
32
|
+
SIM_TOP = 3
|
|
33
|
+
CODE_TRUNC_LINES = 40
|
|
34
|
+
CODE_HEAD_LINES = 12
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _h(text: str) -> str:
|
|
38
|
+
return hashlib.sha1(text.encode("utf-8")).hexdigest()[:12]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _cut(text: str, n: int) -> str:
|
|
42
|
+
lines = text.split("\n")
|
|
43
|
+
if len(lines) <= n:
|
|
44
|
+
return text
|
|
45
|
+
return "\n".join(lines[:n] + [f" /* … {len(lines) - n} more line(s) truncated by the context budget */", "}"])
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class Ctx:
|
|
49
|
+
def __init__(self, root: str):
|
|
50
|
+
self.root = root
|
|
51
|
+
self.ix = ArIndex(root)
|
|
52
|
+
self.models = build_all(root, self.ix)
|
|
53
|
+
self.cx = CIndex(root)
|
|
54
|
+
self._project = None
|
|
55
|
+
self._hdr_text: Optional[dict] = None
|
|
56
|
+
|
|
57
|
+
def rel(self, f: Optional[str]) -> str:
|
|
58
|
+
if not f:
|
|
59
|
+
return "?"
|
|
60
|
+
r = os.path.relpath(f, self.root)
|
|
61
|
+
return f if r.startswith("..") else r
|
|
62
|
+
|
|
63
|
+
def project(self):
|
|
64
|
+
if self._project is None:
|
|
65
|
+
try:
|
|
66
|
+
from .model import Project
|
|
67
|
+
self._project = Project(self.root)
|
|
68
|
+
except Exception: # noqa: BLE001
|
|
69
|
+
self._project = False
|
|
70
|
+
return self._project or None
|
|
71
|
+
|
|
72
|
+
# ---- C helpers
|
|
73
|
+
def headers(self) -> dict:
|
|
74
|
+
if self._hdr_text is None:
|
|
75
|
+
self._hdr_text = {}
|
|
76
|
+
for f in paths.walk(self.root, paths.is_c).files:
|
|
77
|
+
if f.lower().endswith(".h"):
|
|
78
|
+
try:
|
|
79
|
+
self._hdr_text[f] = open(f, encoding="utf-8", errors="replace").read()
|
|
80
|
+
except OSError:
|
|
81
|
+
pass
|
|
82
|
+
return self._hdr_text
|
|
83
|
+
|
|
84
|
+
def prototype(self, name: str) -> Optional[tuple]:
|
|
85
|
+
rx = re.compile(rf"^[^\n#;{{}}]*\b{re.escape(name)}\s*\([^;{{}}]*\)\s*;", re.M)
|
|
86
|
+
for f in sorted(self.headers()):
|
|
87
|
+
m = rx.search(self.headers()[f])
|
|
88
|
+
if m:
|
|
89
|
+
return " ".join(m.group(0).split()), f"{self.rel(f)}:{self.headers()[f].count(chr(10), 0, m.start()) + 1}"
|
|
90
|
+
for name2, lst in self.cx.funcs.items():
|
|
91
|
+
if name2 == name:
|
|
92
|
+
return lst[0].signature + ";", f"{self.rel(lst[0].file)}:{lst[0].line}"
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
def body(self, fn: Func) -> str:
|
|
96
|
+
return fn.text(self.cx.src[fn.file])
|
|
97
|
+
|
|
98
|
+
def swc_files(self, m: SwcModel) -> tuple:
|
|
99
|
+
"""(ARXML files, C files) of a SW-C: its description + its interfaces; C reached from its runnables or
|
|
100
|
+
including Rte_<Swc>.h."""
|
|
101
|
+
ax = {os.path.join(self.root, m.file)}
|
|
102
|
+
for p in m.ports:
|
|
103
|
+
n = self.ix.get(p.get("interface"))
|
|
104
|
+
if n is not None:
|
|
105
|
+
ax.add(self.ix.where(n)[0])
|
|
106
|
+
cf = set()
|
|
107
|
+
for r in m.runnables:
|
|
108
|
+
fn = self.cx.get(r.symbol)
|
|
109
|
+
if fn is not None:
|
|
110
|
+
for g in self.cx.reachable(fn):
|
|
111
|
+
cf.add(g.file)
|
|
112
|
+
want = f"rte_{m.name}.h".lower()
|
|
113
|
+
for f, incs in self.cx.includes.items():
|
|
114
|
+
if any(os.path.basename(i).lower() == want for i in incs):
|
|
115
|
+
cf.add(f)
|
|
116
|
+
return sorted(x for x in ax if x), sorted(cf)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# ---------------------------------------------------------------- sections
|
|
120
|
+
def sec_swc(c: Ctx, m: SwcModel, runs: list) -> list:
|
|
121
|
+
"""-> [full, compact] renderings + sources."""
|
|
122
|
+
lines = [f"SW-C {m.name} ({m.tag}) {m.path} @ {m.file}:{m.line}"
|
|
123
|
+
+ ("; supports multiple instantiation (Rte_Instance self first)" if m.multiple_instantiation else "")]
|
|
124
|
+
lines.append("Ports:")
|
|
125
|
+
for p in m.ports:
|
|
126
|
+
lines.append(f" {p['direction']}-port {p['name']} : {p['interfaceKind']} {p['interface']} @ {p['at']}")
|
|
127
|
+
for e in p["elements"]:
|
|
128
|
+
extra = ", ".join(x for x in (e.get("type"), e.get("semantics") and f"{e['semantics']} semantics",
|
|
129
|
+
e.get("kind")) if x)
|
|
130
|
+
lines.append(f" - {e['name']}" + (f" ({extra})" if extra else ""))
|
|
131
|
+
if m.types:
|
|
132
|
+
lines.append("Data types: " + "; ".join(f"{k}={v['kind']}" + (f"[{v['base']}]" if v.get("base") else "")
|
|
133
|
+
for k, v in sorted(m.types.items())))
|
|
134
|
+
lines.append("Runnables:")
|
|
135
|
+
for r in m.runnables:
|
|
136
|
+
ev = "; ".join(f"{e['kind']} {e['event']}" + (f" period {e['period']}s" if e.get("period") else "")
|
|
137
|
+
+ (f" on {e.get('port')}.{(e.get('target') or '').rsplit('/', 1)[-1]}" if e.get("port") else "")
|
|
138
|
+
for e in r.events) or "no event (not scheduled)"
|
|
139
|
+
mark = " <== target" if r in runs and len(runs) < len(m.runnables) else ""
|
|
140
|
+
lines.append(f" {r.name} symbol {r.symbol} [{ev}] @ {r.file}:{r.line}{mark}")
|
|
141
|
+
lines.append(f" entry: {r.signature}")
|
|
142
|
+
for pt in r.points:
|
|
143
|
+
lines.append(f" {pt['role']}: {pt.get('port') or ''}{'.' if pt.get('port') else ''}"
|
|
144
|
+
f"{(pt.get('target') or '').rsplit('/', 1)[-1]}" + (f" !! {pt['unresolved']}" if pt.get("unresolved") else ""))
|
|
145
|
+
full = "\n".join(lines)
|
|
146
|
+
compact = "\n".join(l for l in lines if not l.startswith(" ") or "<== target" in l or l.startswith(" -"))
|
|
147
|
+
src = [{"kind": "arxml", "ref": m.path, "at": f"{m.file}:{m.line}"}]
|
|
148
|
+
return [full, compact], src
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def sec_api(c: Ctx, m: SwcModel, runs: list, header: Optional[str]) -> tuple:
|
|
152
|
+
decl = header_entries([header]) if header else {}
|
|
153
|
+
lines, src = [], []
|
|
154
|
+
if header:
|
|
155
|
+
lines.append(f"Generated contract header: {c.rel(header)}")
|
|
156
|
+
else:
|
|
157
|
+
lines.append(f"No Rte_{m.name}.h in the project: signatures below are derived from the ARXML (SWS_RTE 5.6).")
|
|
158
|
+
for r in runs:
|
|
159
|
+
allowed = sorted(m.allowed(r.name), key=lambda a: a.name)
|
|
160
|
+
lines.append(f"Runnable {r.name}: {len(allowed)} allowed API(s) — call ONLY these from {r.symbol} and the "
|
|
161
|
+
"functions it calls:")
|
|
162
|
+
for a in allowed:
|
|
163
|
+
d = decl.get(a.name)
|
|
164
|
+
lines.append(f" {a.signature}; [{a.cite}]")
|
|
165
|
+
if d:
|
|
166
|
+
lines.append(f" declared {c.rel(d['file'])}:{d['line']}: {d['text']}")
|
|
167
|
+
src.append({"kind": "rte-decl", "ref": a.name, "at": f"{c.rel(d['file'])}:{d['line']}"})
|
|
168
|
+
else:
|
|
169
|
+
src.append({"kind": "rte-model", "ref": a.name, "at": a.target})
|
|
170
|
+
others = sorted(a.name for a in m.generated() if not a.allowed_for(r.name))
|
|
171
|
+
if others:
|
|
172
|
+
lines.append(f" NOT allowed in {r.name} (other runnables' access points; adding a call is NOT-ALLOWED "
|
|
173
|
+
f"until an access point is added): {', '.join(others)}")
|
|
174
|
+
return "\n".join(lines), src
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def sec_code(c: Ctx, runs: list) -> tuple:
|
|
178
|
+
full, trunc, head, src, missing = [], [], [], [], []
|
|
179
|
+
for r in runs:
|
|
180
|
+
fn = c.cx.get(r.symbol)
|
|
181
|
+
if fn is None:
|
|
182
|
+
missing.append(r.symbol)
|
|
183
|
+
msg = f"/* {r.symbol}: no definition found in the project C sources (new runnable?) */"
|
|
184
|
+
full.append(msg)
|
|
185
|
+
trunc.append(msg)
|
|
186
|
+
head.append(msg)
|
|
187
|
+
continue
|
|
188
|
+
b = c.body(fn)
|
|
189
|
+
hdr = f"/* {c.rel(fn.file)}:{fn.line}-{fn.end_line} */"
|
|
190
|
+
full.append(hdr + "\n" + b)
|
|
191
|
+
trunc.append(hdr + "\n" + _cut(b, CODE_TRUNC_LINES))
|
|
192
|
+
head.append(hdr + "\n" + _cut(b, CODE_HEAD_LINES))
|
|
193
|
+
src.append({"kind": "c-function", "ref": fn.name, "at": f"{c.rel(fn.file)}:{fn.line}-{fn.end_line}", "sha1": _h(b)})
|
|
194
|
+
return ["\n\n".join(full), "\n\n".join(trunc), "\n\n".join(head)], src, missing
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def sec_callees(c: Ctx, runs: list) -> tuple:
|
|
198
|
+
entries = [c.cx.get(r.symbol) for r in runs]
|
|
199
|
+
entries = [e for e in entries if e is not None]
|
|
200
|
+
entry_ids = {id(e) for e in entries}
|
|
201
|
+
own, ext = [], set()
|
|
202
|
+
seen = set()
|
|
203
|
+
for e in entries:
|
|
204
|
+
for g in c.cx.reachable(e):
|
|
205
|
+
if id(g) in entry_ids or id(g) in seen:
|
|
206
|
+
continue
|
|
207
|
+
seen.add(id(g))
|
|
208
|
+
own.append(g)
|
|
209
|
+
for g in [e] + own:
|
|
210
|
+
for name in g.calls:
|
|
211
|
+
if name.startswith("Rte_") or c.cx.callee(g, name) is not None:
|
|
212
|
+
continue
|
|
213
|
+
ext.add(name)
|
|
214
|
+
own.sort(key=lambda g: (c.rel(g.file), g.line))
|
|
215
|
+
ext_l = []
|
|
216
|
+
for name in sorted(ext):
|
|
217
|
+
p = c.prototype(name)
|
|
218
|
+
if p:
|
|
219
|
+
ext_l.append((name, p))
|
|
220
|
+
bodies = [f"/* {c.rel(g.file)}:{g.line}-{g.end_line} */\n{c.body(g)}" for g in own]
|
|
221
|
+
sigs = [f"{g.signature}; /* {c.rel(g.file)}:{g.line} */" for g in own]
|
|
222
|
+
protos = [f"{p[0]} /* {p[1]} */" for _n, p in ext_l]
|
|
223
|
+
names = ", ".join([g.name for g in own] + [n for n, _p in ext_l])
|
|
224
|
+
full = "\n\n".join(bodies + (["/* called, defined elsewhere: */\n" + "\n".join(protos)] if protos else []))
|
|
225
|
+
sig = "\n".join(sigs + protos)
|
|
226
|
+
src = [{"kind": "c-function", "ref": g.name, "at": f"{c.rel(g.file)}:{g.line}-{g.end_line}", "sha1": _h(c.body(g))}
|
|
227
|
+
for g in own] + [{"kind": "c-prototype", "ref": n, "at": p[1]} for n, p in ext_l]
|
|
228
|
+
return [full or "(no callees)", sig or "(no callees)", ("calls: " + names) if names else "(no callees)"], src
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _sysmap(c: Ctx) -> dict:
|
|
232
|
+
"""(port path, data element path) -> system signal path, from SENDER-RECEIVER-TO-SIGNAL-MAPPINGs."""
|
|
233
|
+
out = {}
|
|
234
|
+
for sysn in c.ix.of_tag("SYSTEM"):
|
|
235
|
+
for n in sysn.iter():
|
|
236
|
+
if n.tag == "SENDER-RECEIVER-TO-SIGNAL-MAPPING":
|
|
237
|
+
port = None
|
|
238
|
+
el = None
|
|
239
|
+
for x in n.iter():
|
|
240
|
+
if x.tag == "CONTEXT-PORT-REF":
|
|
241
|
+
port = x.text.strip()
|
|
242
|
+
elif x.tag == "TARGET-DATA-PROTOTYPE-REF":
|
|
243
|
+
el = x.text.strip()
|
|
244
|
+
sig = n.child_text("SYSTEM-SIGNAL-REF")
|
|
245
|
+
if port and el and sig:
|
|
246
|
+
out.setdefault((port, el), sig)
|
|
247
|
+
return out
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def sec_config(c: Ctx, m: SwcModel, runs: list, header: Optional[str]) -> tuple:
|
|
251
|
+
p = c.project()
|
|
252
|
+
if p is None:
|
|
253
|
+
return ["(ECUC configuration not loadable)", "(ECUC configuration not loadable)"], [], []
|
|
254
|
+
from .audit import signal_owners
|
|
255
|
+
from .find import _own
|
|
256
|
+
decl = header_entries([header]) if header else {}
|
|
257
|
+
by_name: dict = {}
|
|
258
|
+
for path, group in p.by_path.items():
|
|
259
|
+
e = group[0]
|
|
260
|
+
if e.kind == "container":
|
|
261
|
+
by_name.setdefault(((e.def_ref or "").rsplit("/", 1)[-1], e.name), e)
|
|
262
|
+
sysmap = _sysmap(c)
|
|
263
|
+
sig_to_com: dict = {}
|
|
264
|
+
for (dname, _n), e in sorted(by_name.items()):
|
|
265
|
+
if dname in ("ComSignal", "ComGroupSignal"):
|
|
266
|
+
ref = (_own(p, e).get("ComSystemTemplateSystemSignalRef") or [None])[0]
|
|
267
|
+
mp = c.ix.get(ref)
|
|
268
|
+
if mp is not None:
|
|
269
|
+
isig = c.ix.get(mp.child_text("I-SIGNAL-REF"))
|
|
270
|
+
ssig = isig.child_text("SYSTEM-SIGNAL-REF") if isig is not None else None
|
|
271
|
+
if ssig:
|
|
272
|
+
sig_to_com.setdefault(ssig, []).append(e)
|
|
273
|
+
lines, src, terms = [], [], []
|
|
274
|
+
want = []
|
|
275
|
+
for r in runs:
|
|
276
|
+
for a in sorted(m.allowed(r.name), key=lambda a: a.name):
|
|
277
|
+
if a.port and a not in want:
|
|
278
|
+
want.append(a)
|
|
279
|
+
for a in want:
|
|
280
|
+
hits = []
|
|
281
|
+
d = decl.get(a.name)
|
|
282
|
+
if d:
|
|
283
|
+
for mod, dname, short in re.findall(r"\b([A-Z][A-Za-z]+)Conf_([A-Za-z]+)_(\w+)", d["text"]):
|
|
284
|
+
e = by_name.get((dname, short))
|
|
285
|
+
if e is not None:
|
|
286
|
+
hits.append((e, f"via {c.rel(d['file'])}:{d['line']}"))
|
|
287
|
+
if not hits:
|
|
288
|
+
ssig = sysmap.get((a.port_path, a.target))
|
|
289
|
+
for e in sig_to_com.get(ssig or "", []):
|
|
290
|
+
hits.append((e, f"via SenderReceiverToSignalMapping -> {ssig}"))
|
|
291
|
+
for e, how in hits:
|
|
292
|
+
own = _own(p, e)
|
|
293
|
+
vals = ", ".join(f"{k}={v[0] if len(v) == 1 else v}" for k, v in sorted(own.items())
|
|
294
|
+
if k in ("ComBitPosition", "ComBitSize", "ComSignalType", "ComSignalEndianness",
|
|
295
|
+
"ComHandleId", "ComTransferProperty", "ComSignalInitValue"))
|
|
296
|
+
owners = signal_owners(p, [e.path]).get(e.path) or []
|
|
297
|
+
ip = "; ".join(f"I-PDU {o['ipdu']} {o.get('direction') or '?'}" for o in owners)
|
|
298
|
+
lines.append(f"{a.port}.{a.element} -> {e.path} [{vals}] {ip} @ {p.rel(e.file)}:{e.line} ({how})")
|
|
299
|
+
src.append({"kind": "ecuc", "ref": e.path, "at": f"{p.rel(e.file)}:{e.line}"})
|
|
300
|
+
terms.append(e.name)
|
|
301
|
+
# NvM blocks used by the runnable's code
|
|
302
|
+
blocks = set()
|
|
303
|
+
for r in runs:
|
|
304
|
+
fn = c.cx.get(r.symbol)
|
|
305
|
+
if fn is None:
|
|
306
|
+
continue
|
|
307
|
+
for g in c.cx.reachable(fn):
|
|
308
|
+
blocks.update(re.findall(r"\bNvMConf_NvMBlockDescriptor_(\w+)", c.body(g)))
|
|
309
|
+
for b in sorted(blocks):
|
|
310
|
+
e = by_name.get(("NvMBlockDescriptor", b))
|
|
311
|
+
if e is None:
|
|
312
|
+
lines.append(f"NvM block {b}: not configured (NvMConf_NvMBlockDescriptor_{b} has no NvMBlockDescriptor)")
|
|
313
|
+
continue
|
|
314
|
+
own = _own(p, e)
|
|
315
|
+
vals = ", ".join(f"{k}={v[0] if len(v) == 1 else v}" for k, v in sorted(own.items())
|
|
316
|
+
if k in ("NvMNvBlockLength", "NvMRamBlockDataAddress", "NvMBlockUseCrc", "NvMBlockCrcType",
|
|
317
|
+
"NvMBlockManagementType", "NvMNvramBlockIdentifier", "NvMSelectBlockForReadAll"))
|
|
318
|
+
lines.append(f"NvM {e.path} [{vals}] @ {p.rel(e.file)}:{e.line}")
|
|
319
|
+
src.append({"kind": "ecuc", "ref": e.path, "at": f"{p.rel(e.file)}:{e.line}"})
|
|
320
|
+
if not lines:
|
|
321
|
+
lines.append("(no Com signal or NvM block found behind this runnable's ports and calls)")
|
|
322
|
+
full = "\n".join(lines)
|
|
323
|
+
return [full, _cut(full, 15)], src, terms
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _features(c: Ctx, fn: Optional[Func], extra: set) -> set:
|
|
327
|
+
feats = set(extra)
|
|
328
|
+
if fn is None:
|
|
329
|
+
return feats
|
|
330
|
+
for g in c.cx.reachable(fn)[:20]:
|
|
331
|
+
for name, _l, _a in g.rte:
|
|
332
|
+
feats.add("api:" + name.split("_")[1] if "_" in name else name)
|
|
333
|
+
feats |= {t for t in terms_of(name)}
|
|
334
|
+
for n in g.calls:
|
|
335
|
+
if not n.startswith("Rte_"):
|
|
336
|
+
feats.add("call:" + n)
|
|
337
|
+
feats |= terms_of(c.body(g))
|
|
338
|
+
return feats
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def sec_similar(c: Ctx, m: SwcModel, runs: list, task: str) -> tuple:
|
|
342
|
+
target = set()
|
|
343
|
+
task_t = terms_of(task)
|
|
344
|
+
for r in runs:
|
|
345
|
+
target |= _features(c, c.cx.get(r.symbol), set())
|
|
346
|
+
for a in m.allowed(r.name):
|
|
347
|
+
target.add("api:" + a.api)
|
|
348
|
+
target |= terms_of(a.element or "")
|
|
349
|
+
target |= task_t
|
|
350
|
+
cands = []
|
|
351
|
+
for o in c.models:
|
|
352
|
+
if o is m:
|
|
353
|
+
continue
|
|
354
|
+
for r in o.runnables:
|
|
355
|
+
fn = c.cx.get(r.symbol)
|
|
356
|
+
if fn is None:
|
|
357
|
+
continue
|
|
358
|
+
f = _features(c, fn, {"api:" + a.api for a in o.allowed(r.name)})
|
|
359
|
+
inter, union = len(target & f), len(target | f) or 1
|
|
360
|
+
score = round(inter / union, 4)
|
|
361
|
+
if inter:
|
|
362
|
+
cands.append((-score, o.name, r.name, fn, sorted(target & f)))
|
|
363
|
+
cands.sort(key=lambda x: (x[0], x[1], x[2]))
|
|
364
|
+
top = cands[:SIM_TOP]
|
|
365
|
+
lines, src = [], []
|
|
366
|
+
for s, swc, rn, fn, common in top:
|
|
367
|
+
lines.append(f"[{swc}.{rn} similarity {-s:.3f}; shared: {', '.join(common[:12])}]\n"
|
|
368
|
+
f"/* {c.rel(fn.file)}:{fn.line}-{fn.end_line} */\n" + _cut(c.body(fn), 30))
|
|
369
|
+
src.append({"kind": "c-function", "ref": f"{swc}.{rn}", "at": f"{c.rel(fn.file)}:{fn.line}", "score": -s,
|
|
370
|
+
"sha1": _h(c.body(fn))})
|
|
371
|
+
return ("\n\n".join(lines) or "(no similar function in other SW-Cs)"), src, [x[1] for x in top]
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def sec_history(c: Ctx, m: SwcModel, runs: list, task: str, similar_swcs: list) -> tuple:
|
|
375
|
+
ax, cf = c.swc_files(m)
|
|
376
|
+
rel_files = []
|
|
377
|
+
for name in sorted(set(similar_swcs)):
|
|
378
|
+
o = find_swc(c.models, name)
|
|
379
|
+
if o is not None:
|
|
380
|
+
a2, c2 = c.swc_files(o)
|
|
381
|
+
rel_files += a2 + c2
|
|
382
|
+
terms = terms_of(task)
|
|
383
|
+
for r in runs:
|
|
384
|
+
for a in m.allowed(r.name):
|
|
385
|
+
terms |= terms_of(f"{a.port or ''} {a.element or ''}")
|
|
386
|
+
terms.add(r.name.lower())
|
|
387
|
+
res = similar_changes(c.root, ax + cf, sorted(set(rel_files) - set(ax + cf)), terms)
|
|
388
|
+
lines, src = [], []
|
|
389
|
+
for ch in res["changes"]:
|
|
390
|
+
lines.append(f"[commit {ch['commit']} score {ch['score']}: {ch['subject']}; overlap: {', '.join(ch['overlap'][:10])}]")
|
|
391
|
+
for d in ch["arxml"]:
|
|
392
|
+
lines.append(f" ARXML {d['file']}: " + (d.get("status") or d.get("error") or json.dumps(d.get("summary"))))
|
|
393
|
+
lines.extend(f" {x}" for x in d.get("items", []))
|
|
394
|
+
for h in ch["c"]:
|
|
395
|
+
lines.append(f" C {h['file']}:\n" + "\n".join(" " + x for x in h["diff"].splitlines()))
|
|
396
|
+
src.append({"kind": "git-commit", "ref": ch["commit"], "score": ch["score"]})
|
|
397
|
+
if not lines:
|
|
398
|
+
lines = [f"(no similar past change: {'; '.join(res['notes']) or 'nothing matched'})"]
|
|
399
|
+
return "\n".join(lines), src
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
# ---------------------------------------------------------------- assembly
|
|
403
|
+
def build_context(root: str, swc: str, task: str, runnable: Optional[str] = None, budget_tokens: int = 24000,
|
|
404
|
+
history: bool = True) -> dict:
|
|
405
|
+
c = Ctx(root)
|
|
406
|
+
m = find_swc(c.models, swc)
|
|
407
|
+
if m is None:
|
|
408
|
+
return {"error": f"no SW-COMPONENT-TYPE {swc!r}; known: {', '.join(x.name for x in c.models) or 'none'}"}
|
|
409
|
+
if runnable:
|
|
410
|
+
r = m.runnable(runnable)
|
|
411
|
+
if r is None:
|
|
412
|
+
return {"error": f"{m.name} has no runnable {runnable!r}; runnables: {', '.join(x.name for x in m.runnables)} "
|
|
413
|
+
"(to add a runnable, add the RUNNABLE-ENTITY + event with arxml_edit first)"}
|
|
414
|
+
runs = [r]
|
|
415
|
+
else:
|
|
416
|
+
runs = list(m.runnables)
|
|
417
|
+
header = find_header(root, m.name, sorted(c.headers()))
|
|
418
|
+
s_swc, src_swc = sec_swc(c, m, runs)
|
|
419
|
+
s_api, src_api = sec_api(c, m, runs, header)
|
|
420
|
+
s_code, src_code, missing = sec_code(c, runs)
|
|
421
|
+
s_cal, src_cal = sec_callees(c, runs)
|
|
422
|
+
s_cfg, src_cfg, _t = sec_config(c, m, runs, header)
|
|
423
|
+
s_sim, src_sim, sim_swcs = sec_similar(c, m, runs, task)
|
|
424
|
+
if history:
|
|
425
|
+
s_hist, src_hist = sec_history(c, m, runs, task, sim_swcs)
|
|
426
|
+
else:
|
|
427
|
+
s_hist, src_hist = "(history disabled)", []
|
|
428
|
+
sections = [
|
|
429
|
+
{"id": "task", "title": "1. TASK", "variants": [task.strip()], "sources": []},
|
|
430
|
+
{"id": "swc", "title": f"2. SW-C DEFINITION ({m.name})", "variants": s_swc, "sources": src_swc},
|
|
431
|
+
{"id": "api", "title": "3. ALLOWED RTE API", "variants": [s_api], "sources": src_api},
|
|
432
|
+
{"id": "code", "title": "4. CURRENT RUNNABLE CODE", "variants": s_code, "sources": src_code},
|
|
433
|
+
{"id": "callees", "title": "5. FUNCTIONS IT CALLS", "variants": s_cal, "sources": src_cal},
|
|
434
|
+
{"id": "config", "title": "6. RELATED CONFIGURATION (Com signals / NvM blocks)", "variants": s_cfg, "sources": src_cfg},
|
|
435
|
+
{"id": "similar", "title": "7. FUZZY: SIMILAR FUNCTIONS IN OTHER SW-Cs", "variants": [s_sim, ""], "sources": src_sim},
|
|
436
|
+
{"id": "history", "title": "8. FUZZY: SIMILAR PAST CHANGES", "variants": [s_hist, ""], "sources": src_hist},
|
|
437
|
+
]
|
|
438
|
+
mode_names = {"similar": ["full", "dropped"], "history": ["full", "dropped"],
|
|
439
|
+
"callees": ["bodies", "prototypes", "names"], "code": ["full", "truncated", "head"],
|
|
440
|
+
"config": ["full", "truncated"], "swc": ["full", "compact"]}
|
|
441
|
+
level = {s["id"]: 0 for s in sections}
|
|
442
|
+
steps = [("history", 1), ("similar", 1), ("callees", 1), ("code", 1), ("config", 1), ("callees", 2),
|
|
443
|
+
("code", 2), ("swc", 1)]
|
|
444
|
+
budget = max(1, budget_tokens) * CHARS_PER_TOKEN
|
|
445
|
+
applied = []
|
|
446
|
+
|
|
447
|
+
def render() -> str:
|
|
448
|
+
parts = [f"# AUTOSAR context: {m.name}" + (f" / {runs[0].name}" if len(runs) == 1 else " (all runnables)")]
|
|
449
|
+
for s in sections:
|
|
450
|
+
v = s["variants"][min(level[s["id"]], len(s["variants"]) - 1)]
|
|
451
|
+
if v == "":
|
|
452
|
+
parts.append(f"## {s['title']}\n(dropped: context budget)")
|
|
453
|
+
else:
|
|
454
|
+
parts.append(f"## {s['title']}\n{v}")
|
|
455
|
+
return "\n\n".join(parts) + "\n"
|
|
456
|
+
|
|
457
|
+
text = render()
|
|
458
|
+
for sid, lv in steps:
|
|
459
|
+
if len(text) <= budget:
|
|
460
|
+
break
|
|
461
|
+
sec = next(s for s in sections if s["id"] == sid)
|
|
462
|
+
if lv < len(sec["variants"]) and level[sid] < lv:
|
|
463
|
+
level[sid] = lv
|
|
464
|
+
applied.append(f"{sid}->{mode_names[sid][lv]}")
|
|
465
|
+
text = render()
|
|
466
|
+
manifest = []
|
|
467
|
+
for s in sections:
|
|
468
|
+
mode = mode_names.get(s["id"], ["full"])[level[s["id"]]]
|
|
469
|
+
for x in s["sources"]:
|
|
470
|
+
manifest.append({"section": s["id"], **x, "included": mode})
|
|
471
|
+
out = {"text": text, "manifest": manifest,
|
|
472
|
+
"sections": [{"id": s["id"], "title": s["title"], "mode": mode_names.get(s["id"], ["full"])[level[s["id"]]],
|
|
473
|
+
"chars": len(s["variants"][min(level[s["id"]], len(s["variants"]) - 1)])} for s in sections],
|
|
474
|
+
"swc": m.name, "runnables": [r.name for r in runs], "budgetTokens": budget_tokens,
|
|
475
|
+
"estimatedTokens": (len(text) + CHARS_PER_TOKEN - 1) // CHARS_PER_TOKEN, "reductions": applied,
|
|
476
|
+
"sha1": _h(text)}
|
|
477
|
+
if len(text) > budget:
|
|
478
|
+
out["overBudget"] = True
|
|
479
|
+
if missing:
|
|
480
|
+
out["codeMissing"] = missing
|
|
481
|
+
return out
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""Local, dependency-free C function index: definitions, bodies, callees, Rte_* calls and the call graph.
|
|
2
|
+
|
|
3
|
+
Function definitions are found at brace depth 0 in comment/string-stripped text (audit.strip_comments, which
|
|
4
|
+
keeps offsets): a '{' preceded by ')' whose matching '(' follows an identifier is a definition of that
|
|
5
|
+
identifier. Works with AUTOSAR compiler-abstraction macros (FUNC(void, X_CODE) Name(void)) because the name is
|
|
6
|
+
the identifier directly before the parameter list. Preprocessor lines are ignored.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
from . import paths
|
|
16
|
+
from .audit import is_generated, iter_calls, strip_comments
|
|
17
|
+
|
|
18
|
+
KEYWORDS = {"if", "for", "while", "switch", "return", "sizeof", "defined", "else", "do", "case", "goto", "typeof",
|
|
19
|
+
"_Alignof", "__attribute__", "__asm__", "asm", "offsetof"}
|
|
20
|
+
RTE_CALL = re.compile(r"\b(Rte_\w+)\s*\(")
|
|
21
|
+
ANY_CALL = re.compile(r"\b([A-Za-z_]\w*)\s*\(")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class Func:
|
|
26
|
+
name: str
|
|
27
|
+
file: str
|
|
28
|
+
line: int
|
|
29
|
+
end_line: int
|
|
30
|
+
start: int # offset of the declaration start
|
|
31
|
+
body_start: int # offset of '{'
|
|
32
|
+
end: int # offset after '}'
|
|
33
|
+
static: bool
|
|
34
|
+
signature: str
|
|
35
|
+
calls: list = field(default_factory=list) # callee names in order of first use
|
|
36
|
+
rte: list = field(default_factory=list) # [(name, line, args)]
|
|
37
|
+
|
|
38
|
+
def text(self, src: str) -> str:
|
|
39
|
+
return src[self.start:self.end]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _blank_preproc(text: str) -> str:
|
|
43
|
+
out = []
|
|
44
|
+
cont = False
|
|
45
|
+
for ln in text.split("\n"):
|
|
46
|
+
if cont or ln.lstrip().startswith("#"):
|
|
47
|
+
cont = ln.rstrip().endswith("\\")
|
|
48
|
+
out.append(" " * len(ln))
|
|
49
|
+
else:
|
|
50
|
+
out.append(ln)
|
|
51
|
+
return "\n".join(out)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _match_open(s: str, close: int) -> int:
|
|
55
|
+
depth = 0
|
|
56
|
+
for i in range(close, -1, -1):
|
|
57
|
+
if s[i] == ")":
|
|
58
|
+
depth += 1
|
|
59
|
+
elif s[i] == "(":
|
|
60
|
+
depth -= 1
|
|
61
|
+
if depth == 0:
|
|
62
|
+
return i
|
|
63
|
+
return -1
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def functions(path: str, raw: Optional[str] = None) -> tuple:
|
|
67
|
+
"""-> (list of Func, raw source)."""
|
|
68
|
+
if raw is None:
|
|
69
|
+
try:
|
|
70
|
+
raw = open(path, encoding="utf-8", errors="replace").read()
|
|
71
|
+
except OSError:
|
|
72
|
+
return [], ""
|
|
73
|
+
s = _blank_preproc(strip_comments(raw))
|
|
74
|
+
out = []
|
|
75
|
+
depth = 0
|
|
76
|
+
i, n = 0, len(s)
|
|
77
|
+
last_end = 0 # end of the previous top-level construct (for the declaration start)
|
|
78
|
+
while i < n:
|
|
79
|
+
c = s[i]
|
|
80
|
+
if c == "{":
|
|
81
|
+
if depth == 0:
|
|
82
|
+
j = i - 1
|
|
83
|
+
while j >= 0 and s[j].isspace():
|
|
84
|
+
j -= 1
|
|
85
|
+
fn = None
|
|
86
|
+
if j >= 0 and s[j] == ")":
|
|
87
|
+
o = _match_open(s, j)
|
|
88
|
+
k = o - 1
|
|
89
|
+
while k >= 0 and s[k].isspace():
|
|
90
|
+
k -= 1
|
|
91
|
+
m = re.search(r"([A-Za-z_]\w*)$", s[max(0, k - 200):k + 1]) if k >= 0 else None
|
|
92
|
+
if m and m.group(1) not in KEYWORDS:
|
|
93
|
+
fn = m.group(1)
|
|
94
|
+
if fn:
|
|
95
|
+
# matching close brace
|
|
96
|
+
d, e = 0, i
|
|
97
|
+
while e < n:
|
|
98
|
+
if s[e] == "{":
|
|
99
|
+
d += 1
|
|
100
|
+
elif s[e] == "}":
|
|
101
|
+
d -= 1
|
|
102
|
+
if d == 0:
|
|
103
|
+
break
|
|
104
|
+
e += 1
|
|
105
|
+
head_start = max(s.rfind(";", 0, i), s.rfind("}", 0, i), last_end - 1) + 1
|
|
106
|
+
while head_start < i and s[head_start].isspace():
|
|
107
|
+
head_start += 1
|
|
108
|
+
sig = " ".join(s[head_start:i].split())
|
|
109
|
+
f = Func(fn, path, s.count("\n", 0, head_start) + 1, s.count("\n", 0, e) + 1, head_start, i,
|
|
110
|
+
e + 1, bool(re.match(r"(?:\w+\s+)*?static\b", sig)), sig)
|
|
111
|
+
body = s[i:e + 1]
|
|
112
|
+
seen = []
|
|
113
|
+
for mm in ANY_CALL.finditer(body):
|
|
114
|
+
name = mm.group(1)
|
|
115
|
+
if name in KEYWORDS or name == fn:
|
|
116
|
+
continue
|
|
117
|
+
if name not in seen:
|
|
118
|
+
seen.append(name)
|
|
119
|
+
f.calls = seen
|
|
120
|
+
for name, off, _line, args in iter_calls(body, RTE_CALL):
|
|
121
|
+
f.rte.append((name, f.line + body.count("\n", 0, off) + (s.count("\n", head_start, i)), args))
|
|
122
|
+
out.append(f)
|
|
123
|
+
i = e + 1
|
|
124
|
+
last_end = i
|
|
125
|
+
continue
|
|
126
|
+
depth += 1
|
|
127
|
+
elif c == "}":
|
|
128
|
+
depth = max(0, depth - 1)
|
|
129
|
+
if depth == 0:
|
|
130
|
+
last_end = i + 1
|
|
131
|
+
elif c == ";" and depth == 0:
|
|
132
|
+
last_end = i + 1
|
|
133
|
+
i += 1
|
|
134
|
+
return out, raw
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class CIndex:
|
|
138
|
+
"""All function definitions under the given C files (in-house first; generated files kept separately)."""
|
|
139
|
+
|
|
140
|
+
def __init__(self, root: str, files: Optional[list] = None, include_generated: bool = False):
|
|
141
|
+
self.root = root
|
|
142
|
+
all_c = files if files is not None else paths.walk(root, paths.is_c).files
|
|
143
|
+
self.cfiles = sorted(f for f in all_c if f.lower().endswith(".c"))
|
|
144
|
+
self.generated = {f for f in self.cfiles if is_generated(f)}
|
|
145
|
+
self.funcs: dict = {} # name -> [Func]
|
|
146
|
+
self.src: dict = {}
|
|
147
|
+
self.includes: dict = {}
|
|
148
|
+
for f in self.cfiles:
|
|
149
|
+
if f in self.generated and not include_generated:
|
|
150
|
+
continue
|
|
151
|
+
fs, raw = functions(f)
|
|
152
|
+
self.src[f] = raw
|
|
153
|
+
self.includes[f] = re.findall(r'^\s*#\s*include\s*[<"]([^>"]+)[>"]', raw, re.M)
|
|
154
|
+
for fn in fs:
|
|
155
|
+
self.funcs.setdefault(fn.name, []).append(fn)
|
|
156
|
+
|
|
157
|
+
def get(self, name: str, prefer_file: Optional[str] = None) -> Optional[Func]:
|
|
158
|
+
lst = self.funcs.get(name) or []
|
|
159
|
+
if prefer_file:
|
|
160
|
+
for f in lst:
|
|
161
|
+
if f.file == prefer_file:
|
|
162
|
+
return f
|
|
163
|
+
nonstatic = [f for f in lst if not f.static]
|
|
164
|
+
return (nonstatic or lst or [None])[0]
|
|
165
|
+
|
|
166
|
+
def callee(self, caller: Func, name: str) -> Optional[Func]:
|
|
167
|
+
"""Resolve a call from ``caller``: a static function in the same file wins, else a global one."""
|
|
168
|
+
lst = self.funcs.get(name) or []
|
|
169
|
+
same = [f for f in lst if f.file == caller.file]
|
|
170
|
+
if same:
|
|
171
|
+
return same[0]
|
|
172
|
+
glob = [f for f in lst if not f.static]
|
|
173
|
+
return glob[0] if glob else None
|
|
174
|
+
|
|
175
|
+
def reachable(self, start: Func, limit: int = 2000) -> list:
|
|
176
|
+
seen, order, stack = {id(start)}, [start], [start]
|
|
177
|
+
while stack and len(order) < limit:
|
|
178
|
+
f = stack.pop()
|
|
179
|
+
for c in f.calls:
|
|
180
|
+
g = self.callee(f, c)
|
|
181
|
+
if g is not None and id(g) not in seen:
|
|
182
|
+
seen.add(id(g))
|
|
183
|
+
order.append(g)
|
|
184
|
+
stack.append(g)
|
|
185
|
+
return order
|
|
186
|
+
|
|
187
|
+
def rel(self, f: str) -> str:
|
|
188
|
+
r = os.path.relpath(f, self.root)
|
|
189
|
+
return f if r.startswith("..") else r
|