@h2loop/autosar 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +4 -0
  2. package/agents/autosar-integrator.md +34 -2
  3. package/package.json +4 -1
  4. package/plugins/index.js +53 -23
  5. package/python/ecuc_engine/__init__.py +1 -1
  6. package/python/ecuc_engine/add_signal.py +351 -0
  7. package/python/ecuc_engine/arxml_edit.py +387 -0
  8. package/python/ecuc_engine/audit.py +328 -48
  9. package/python/ecuc_engine/cli.py +114 -5
  10. package/python/ecuc_engine/codegen/__init__.py +185 -0
  11. package/python/ecuc_engine/codegen/binding.py +210 -0
  12. package/python/ecuc_engine/codegen/bindings/sip_standin.json +111 -0
  13. package/python/ecuc_engine/codegen/cdiff.py +395 -0
  14. package/python/ecuc_engine/codegen/cwriter.py +150 -0
  15. package/python/ecuc_engine/codegen/emit.py +395 -0
  16. package/python/ecuc_engine/codegen/handles.py +89 -0
  17. package/python/ecuc_engine/codegen/mcal/tc4d.json +60 -0
  18. package/python/ecuc_engine/codegen/model.py +259 -0
  19. package/python/ecuc_engine/codegen/modules/__init__.py +7 -0
  20. package/python/ecuc_engine/codegen/modules/comstack.py +14 -0
  21. package/python/ecuc_engine/codegen/modules/dem.py +22 -0
  22. package/python/ecuc_engine/codegen/modules/mcal.py +170 -0
  23. package/python/ecuc_engine/codegen/modules/memstack.py +24 -0
  24. package/python/ecuc_engine/codegen/registry.py +133 -0
  25. package/python/ecuc_engine/codegen/schm_memmap.py +159 -0
  26. package/python/ecuc_engine/context.py +481 -0
  27. package/python/ecuc_engine/csrc.py +189 -0
  28. package/python/ecuc_engine/defs.py +30 -0
  29. package/python/ecuc_engine/find.py +162 -0
  30. package/python/ecuc_engine/gen.py +105 -0
  31. package/python/ecuc_engine/history.py +146 -0
  32. package/python/ecuc_engine/model.py +4 -1
  33. package/python/ecuc_engine/probe.py +229 -0
  34. package/python/ecuc_engine/rte_api.py +935 -0
  35. package/python/ecuc_engine/rules/__init__.py +5 -3
  36. package/python/ecuc_engine/rules/mem.py +84 -0
  37. package/python/ecuc_engine/rules/nvm.py +131 -0
  38. package/python/ecuc_engine/rules/pdur.py +87 -0
  39. package/python/ecuc_engine/rules/system.py +162 -0
  40. package/python/ecuc_engine/sysmodel.py +152 -0
  41. package/python/ecuc_engine/validate.py +15 -2
  42. package/python/ecuc_engine/verify_rte.py +445 -0
  43. package/python/pyproject.toml +1 -1
  44. package/skills/autosar/SKILL.md +47 -1
  45. package/skills/autosar/references/codegen.md +30 -0
  46. package/skills/autosar/references/project-layout.md +7 -1
  47. package/skills/autosar/references/runnable-code.md +50 -0
@@ -0,0 +1,935 @@
1
+ """Expected RTE API per SW-C and runnable, derived from the SWC description ARXML (pure model layer).
2
+
3
+ Everything is found by following references from the SW-COMPONENT-TYPE: ports -> port interfaces -> data
4
+ elements / operations / mode groups / parameters / triggers, the SWC-INTERNAL-BEHAVIOR's runnables and their
5
+ access points, inter-runnable variables, per-instance memory, internal calibration parameters and exclusive
6
+ areas; data types through IMPLEMENTATION-DATA-TYPEs and the behaviour's DATA-TYPE-MAPPING-SETs. No name or
7
+ layout of any particular project is assumed.
8
+
9
+ Three tiers per API name (AUTOSAR CP R25-11 SWS_RTE, chapter 5.6):
10
+ allowed the runnable has the access point the API's "Existence" requirement names (e.g. a dataSendPoint
11
+ for Rte_Write, SWS_Rte_01280); only these may be called from that runnable
12
+ (SWS_Rte_CONSTR_09017/09083/... "may only be used by the runnable that contains ...")
13
+ generated some runnable of the SW-C is allowed it, so the RTE generator emits it into Rte_<Swc>.h
14
+ derivable the port/element (or IRV, parameter, ...) exists with a compatible direction and semantics, so the
15
+ API could be enabled by adding an access point; calling it now does not compile
16
+ Anything else is not an API of this SW-C.
17
+
18
+ Each entry carries a full C signature by the SWS_RTE parameter-passing rules (IN primitive by value
19
+ SWS_Rte_01017, IN struct/union by const reference SWS_Rte_01018/07086, IN array as pointer to the base type
20
+ SWS_Rte_05107, OUT by reference SWS_Rte_01019/07082/05108, return values SWS_Rte_07069..07073,
21
+ Rte_Instance first for multiple instantiation), so a header generator can emit Rte_<Swc>.h from this model.
22
+ Matching against an existing generated Rte_<Swc>.h is the separate, optional ``match_header`` step.
23
+ """
24
+ from __future__ import annotations
25
+
26
+ import os
27
+ import re
28
+ from dataclasses import dataclass, field
29
+ from typing import Optional
30
+
31
+ from . import paths
32
+ from .xmltree import Node, parse
33
+
34
+ SWC_TAGS = ("APPLICATION-SW-COMPONENT-TYPE", "SENSOR-ACTUATOR-SW-COMPONENT-TYPE", "SERVICE-SW-COMPONENT-TYPE",
35
+ "COMPLEX-DEVICE-DRIVER-SW-COMPONENT-TYPE", "ECU-ABSTRACTION-SW-COMPONENT-TYPE",
36
+ "NV-BLOCK-SW-COMPONENT-TYPE", "SERVICE-PROXY-SW-COMPONENT-TYPE", "PARAMETER-SW-COMPONENT-TYPE")
37
+ MARKERS = (b"SW-COMPONENT-TYPE", b"INTERFACE>", b"DATA-TYPE", b"MODE-DECLARATION-GROUP", b"SW-BASE-TYPE")
38
+ PRIMITIVE_NAMES = {"boolean", "uint8", "uint16", "uint32", "uint64", "sint8", "sint16", "sint32", "sint64",
39
+ "float32", "float64", "uint8_least", "uint16_least", "uint32_least", "sint8_least",
40
+ "sint16_least", "sint32_least", "Std_ReturnType"}
41
+
42
+ # API kind -> (signature requirement, existence requirement)
43
+ CITE = {
44
+ "Write": ("SWS_Rte_01071", "SWS_Rte_01280"), "Send": ("SWS_Rte_01072", "SWS_Rte_01281"),
45
+ "Switch": ("SWS_Rte_02631", "SWS_Rte_02632"), "Invalidate": ("SWS_Rte_01206", "SWS_Rte_01282"),
46
+ "Feedback": ("SWS_Rte_01083", "SWS_Rte_01285"), "SwitchAck": ("SWS_Rte_02725", "SWS_Rte_02677"),
47
+ "Read": ("SWS_Rte_01091", "SWS_Rte_01289"), "DRead": ("SWS_Rte_07394", "SWS_Rte_07395"),
48
+ "Receive": ("SWS_Rte_01092", "SWS_Rte_01288"), "Call": ("SWS_Rte_01102", "SWS_Rte_01293/01294"),
49
+ "Result": ("SWS_Rte_01111", "SWS_Rte_01296/01297"), "Pim": ("SWS_Rte_01118", "SWS_Rte_01299"),
50
+ "CData": ("SWS_Rte_01252", "SWS_Rte_01300"), "Prm": ("SWS_Rte_03928", "SWS_Rte_03929"),
51
+ "IRead": ("SWS_Rte_03741", "SWS_Rte_01301"), "IReadRef": ("SWS_Rte_04577", "SWS_Rte_04578"),
52
+ "IWrite": ("SWS_Rte_03744", "SWS_Rte_01302"), "IWriteRef": ("SWS_Rte_05509", "SWS_Rte_05510"),
53
+ "IInvalidate": ("SWS_Rte_03800", "SWS_Rte_03801"), "IStatus": ("SWS_Rte_02599", "SWS_Rte_02600"),
54
+ "IrvIRead": ("SWS_Rte_03550", "SWS_Rte_01303"), "IrvIReadRef": ("SWS_Rte_04579", "SWS_Rte_04580"),
55
+ "IrvIWrite": ("SWS_Rte_03553", "SWS_Rte_01304"), "IrvIWriteRef": ("SWS_Rte_06207", "SWS_Rte_06208"),
56
+ "IrvRead": ("SWS_Rte_03560", "SWS_Rte_01305"), "IrvWrite": ("SWS_Rte_03565", "SWS_Rte_01306"),
57
+ "Enter": ("SWS_Rte_01120", "SWS_Rte_01307"), "Exit": ("SWS_Rte_01123", "SWS_Rte_01308"),
58
+ "Mode": ("SWS_Rte_02628", "SWS_Rte_02629"), "Trigger": ("SWS_Rte_07200", "SWS_Rte_07201"),
59
+ "IrTrigger": ("SWS_Rte_07203", "SWS_Rte_07204"), "IsUpdated": ("SWS_Rte_07390", "SWS_Rte_07391"),
60
+ }
61
+ # longest first, so "IrvIReadRef" is tried before "IrvIRead" before "IrvRead"
62
+ KINDS = sorted(CITE, key=len, reverse=True)
63
+ PER_RUNNABLE = {"IRead", "IReadRef", "IWrite", "IWriteRef", "IInvalidate", "IStatus", "IrvIRead", "IrvIReadRef",
64
+ "IrvIWrite", "IrvIWriteRef", "IrvRead", "IrvWrite", "IrTrigger"}
65
+ ROLE = { # access-point collection -> role name used in reports and suggestions
66
+ "DATA-SEND-POINTS": "dataSendPoint", "DATA-RECEIVE-POINT-BY-ARGUMENTS": "dataReceivePointByArgument",
67
+ "DATA-RECEIVE-POINT-BY-VALUES": "dataReceivePointByValue", "DATA-READ-ACCESSS": "dataReadAccess",
68
+ "DATA-WRITE-ACCESSS": "dataWriteAccess", "SERVER-CALL-POINTS": "serverCallPoint",
69
+ "ASYNCHRONOUS-SERVER-CALL-RESULT-POINTS": "asynchronousServerCallResultPoint",
70
+ "MODE-ACCESS-POINTS": "modeAccessPoint", "MODE-SWITCH-POINTS": "modeSwitchPoint",
71
+ "PARAMETER-ACCESSS": "parameterAccess", "READ-LOCAL-VARIABLES": "readLocalVariable",
72
+ "WRITTEN-LOCAL-VARIABLES": "writtenLocalVariable", "EXTERNAL-TRIGGERING-POINTS": "externalTriggeringPoint",
73
+ "INTERNAL-TRIGGERING-POINTS": "internalTriggeringPoint",
74
+ }
75
+ COLLECTION = {v: k for k, v in ROLE.items()}
76
+ INVALIDATING = {"KEEP", "REPLACE", "EXTERNAL-REPLACEMENT"}
77
+
78
+
79
+ # ---------------------------------------------------------------- ARXML index
80
+ class ArIndex:
81
+ """Identifiables of all non-ECUC ARXML that can hold SWC descriptions, interfaces and data types."""
82
+
83
+ def __init__(self, root: str, files: Optional[list] = None):
84
+ self.root = root
85
+ self.by_path: dict = {}
86
+ self.path_of: dict = {}
87
+ self.file_of: dict = {}
88
+ self.files: list = []
89
+ for f in sorted(files if files is not None else paths.walk(root, paths.is_arxml).files):
90
+ try:
91
+ data = open(f, "rb").read()
92
+ except OSError:
93
+ continue
94
+ if not any(m in data for m in MARKERS):
95
+ continue
96
+ try:
97
+ doc = parse(data)
98
+ except Exception: # noqa: BLE001 (malformed file: skipped, other files still indexed)
99
+ continue
100
+ self.files.append(f)
101
+ stack = [(doc, "")]
102
+ while stack:
103
+ n, prefix = stack.pop()
104
+ sn = n.short_name
105
+ p = f"{prefix}/{sn}" if sn is not None else prefix
106
+ if sn is not None:
107
+ self.by_path.setdefault(p, n)
108
+ self.path_of[id(n)] = p
109
+ self.file_of[id(n)] = f
110
+ stack.extend((c, p) for c in reversed(n.children))
111
+
112
+ def get(self, path: Optional[str]) -> Optional[Node]:
113
+ return self.by_path.get(path.strip()) if path else None
114
+
115
+ def path(self, n: Node) -> str:
116
+ return self.path_of.get(id(n), "")
117
+
118
+ def where(self, n: Node) -> tuple:
119
+ return self.file_of.get(id(n)), n.line
120
+
121
+ def of_tag(self, *tags) -> list:
122
+ return [n for p, n in sorted(self.by_path.items()) if n.tag in tags]
123
+
124
+
125
+ def _desc(n: Node, tag: str) -> Optional[Node]:
126
+ for c in n.iter():
127
+ if c is not n and c.tag == tag:
128
+ return c
129
+ return None
130
+
131
+
132
+ def _desc_text(n: Node, tag: str) -> Optional[str]:
133
+ c = _desc(n, tag)
134
+ return c.text.strip() if c is not None else None
135
+
136
+
137
+ def _own(n: Node, coll: str) -> list:
138
+ """Children of the collection element ``coll`` directly under n (identifiables in an aggregation)."""
139
+ c = n.child(coll)
140
+ return list(c.children) if c is not None else []
141
+
142
+
143
+ def _ref_ending(n: Node, *suffixes) -> Optional[str]:
144
+ for c in n.iter():
145
+ if c is not n and c.tag.endswith(suffixes):
146
+ t = c.text.strip()
147
+ if t:
148
+ return t
149
+ return None
150
+
151
+
152
+ # ---------------------------------------------------------------- types
153
+ @dataclass
154
+ class TypeInfo:
155
+ name: str
156
+ kind: str # primitive | struct | union | array | pointer | unknown
157
+ path: Optional[str] = None
158
+ base: Optional[str] = None # array element C type
159
+ note: Optional[str] = None
160
+
161
+ def to_dict(self):
162
+ return {k: v for k, v in self.__dict__.items() if v is not None}
163
+
164
+
165
+ class Types:
166
+ def __init__(self, ix: ArIndex):
167
+ self.ix = ix
168
+ self._maps: dict = {}
169
+
170
+ def mapping(self, set_paths: list) -> tuple:
171
+ key = tuple(set_paths)
172
+ if key not in self._maps:
173
+ app, mode = {}, {}
174
+ sets = [self.ix.get(p) for p in set_paths] if set_paths else self.ix.of_tag("DATA-TYPE-MAPPING-SET")
175
+ for s in [x for x in sets if x is not None]:
176
+ for m in _own(s, "DATA-TYPE-MAPS"):
177
+ a, i = m.child_text("APPLICATION-DATA-TYPE-REF"), m.child_text("IMPLEMENTATION-DATA-TYPE-REF")
178
+ if a and i:
179
+ app.setdefault(a, i)
180
+ for m in _own(s, "MODE-REQUEST-TYPE-MAPS"):
181
+ g, i = m.child_text("MODE-GROUP-REF"), m.child_text("IMPLEMENTATION-DATA-TYPE-REF")
182
+ if g and i:
183
+ mode.setdefault(g, i)
184
+ self._maps[key] = (app, mode)
185
+ return self._maps[key]
186
+
187
+ def resolve(self, ref: Optional[str], maps: list, depth: int = 0) -> TypeInfo:
188
+ if not ref:
189
+ return TypeInfo("void", "unknown", note="no type reference")
190
+ n = self.ix.get(ref)
191
+ name = ref.rstrip("/").rsplit("/", 1)[-1]
192
+ if n is None:
193
+ return TypeInfo(name, "primitive" if name in PRIMITIVE_NAMES else "unknown", ref,
194
+ note=f"type {ref} not found in the project ARXML")
195
+ if n.tag.startswith("APPLICATION-") and n.tag.endswith("-DATA-TYPE"):
196
+ impl = self.mapping(maps)[0].get(ref) or (self.mapping([])[0].get(ref) if maps else None)
197
+ if impl is None:
198
+ return TypeInfo(name, "unknown", ref, note=f"application data type {name} has no DataTypeMap to an "
199
+ "ImplementationDataType (SWS_Rte_07069 needs one)")
200
+ return self.resolve(impl, maps, depth + 1)
201
+ cat = (n.child_text("CATEGORY") or "VALUE").upper()
202
+ if cat == "TYPE_REFERENCE" and depth < 8: # redefinition: passed like what it redefines (SWS_Rte_07084)
203
+ inner = self.resolve(_desc_text(n, "IMPLEMENTATION-DATA-TYPE-REF"), maps, depth + 1)
204
+ return TypeInfo(name, inner.kind, ref, inner.base, inner.note)
205
+ if cat in ("STRUCTURE", "UNION"):
206
+ return TypeInfo(name, cat.lower() if cat == "UNION" else "struct", ref)
207
+ if cat == "ARRAY":
208
+ el = _desc(n, "IMPLEMENTATION-DATA-TYPE-ELEMENT")
209
+ base = None
210
+ if el is not None:
211
+ r = _desc_text(el, "IMPLEMENTATION-DATA-TYPE-REF")
212
+ if r:
213
+ base = r.rsplit("/", 1)[-1]
214
+ else:
215
+ b = self.ix.get(_desc_text(el, "BASE-TYPE-REF"))
216
+ base = (_desc_text(b, "NATIVE-DECLARATION") or b.short_name) if b is not None else None
217
+ return TypeInfo(name, "array", ref, base)
218
+ if cat == "DATA_REFERENCE":
219
+ return TypeInfo(name, "pointer", ref)
220
+ return TypeInfo(name, "primitive", ref)
221
+
222
+ def mode_type(self, group_proto: Optional[Node], maps: list) -> TypeInfo:
223
+ if group_proto is None:
224
+ return TypeInfo("uint8", "primitive", note="mode group not found; uint8 assumed")
225
+ mdg = group_proto.child_text("TYPE-TREF")
226
+ impl = self.mapping(maps)[1].get(mdg or "") or self.mapping([])[1].get(mdg or "")
227
+ if impl:
228
+ t = self.resolve(impl, maps)
229
+ t.note = None
230
+ return t
231
+ return TypeInfo("uint8", "primitive", note=f"no ModeRequestTypeMap for {mdg}; the <mode> type is the "
232
+ "ImplementationDataType mapped to the ModeDeclarationGroup (SWS_Rte_02632 description); uint8 assumed")
233
+
234
+
235
+ def in_param(t: TypeInfo, name: str = "data") -> str:
236
+ if t.kind in ("struct", "union"):
237
+ return f"const {t.name}* {name}" # SWS_Rte_01018, SWS_Rte_07086
238
+ if t.kind == "array":
239
+ return f"const {t.base or t.name}* {name}" # SWS_Rte_05107, SWS_Rte_07086
240
+ return f"{t.name} {name}" # SWS_Rte_01017 (and DATA_REFERENCE: the pointer value itself)
241
+
242
+
243
+ def out_param(t: TypeInfo, name: str = "data") -> str:
244
+ if t.kind == "array":
245
+ return f"{t.base or t.name}* {name}" # SWS_Rte_05108
246
+ return f"{t.name}* {name}" # SWS_Rte_01019, SWS_Rte_07082, SWS_Rte_07083
247
+
248
+
249
+ def ret_of(t: TypeInfo) -> str:
250
+ if t.kind in ("struct", "union"):
251
+ return f"const {t.name}*" # SWS_Rte_07071, SWS_Rte_08300
252
+ if t.kind == "array":
253
+ return f"const {t.base or t.name}*" # SWS_Rte_07072
254
+ return t.name # SWS_Rte_07070, SWS_Rte_07073
255
+
256
+
257
+ # ---------------------------------------------------------------- model
258
+ @dataclass
259
+ class ApiEntry:
260
+ name: str
261
+ api: str
262
+ ret: str
263
+ params: list # [{"name", "type", "dir"}]
264
+ target: str # what the API accesses: AUTOSAR path of the data element / operation / IRV / ...
265
+ port: Optional[str] = None
266
+ port_path: Optional[str] = None
267
+ element: Optional[str] = None
268
+ runnable: Optional[str] = None # the <re> of per-runnable APIs (IRead, IrvWrite, ...)
269
+ type: Optional[TypeInfo] = None
270
+ semantics: Optional[str] = None # data | event (S/R)
271
+ access: list = field(default_factory=list) # [{"runnable", "role", "point"}]
272
+ any_runnable: bool = False # Rte_Pim: every runnable of the SW-C
273
+ condition: Optional[str] = None # extra configuration the API depends on
274
+
275
+ @property
276
+ def cite(self) -> str:
277
+ s, e = CITE[self.api]
278
+ return f"{s} (signature), {e} (existence)"
279
+
280
+ @property
281
+ def signature(self) -> str:
282
+ ps = ", ".join(p["type"] for p in self.params) or "void"
283
+ return f"{self.ret} {self.name}({ps})"
284
+
285
+ def allowed_for(self, runnable: str) -> bool:
286
+ return self.any_runnable or any(a["runnable"] == runnable for a in self.access)
287
+
288
+ @property
289
+ def generated(self) -> bool:
290
+ return self.any_runnable or bool(self.access)
291
+
292
+ def to_dict(self, brief: bool = False) -> dict:
293
+ d = {"name": self.name, "api": self.api, "signature": self.signature, "cite": self.cite, "target": self.target}
294
+ if not brief:
295
+ d.update({"ret": self.ret, "params": self.params})
296
+ for k in ("port", "element", "runnable", "semantics", "condition"):
297
+ if getattr(self, k) is not None:
298
+ d[k] = getattr(self, k)
299
+ if self.type is not None:
300
+ d["type"] = self.type.to_dict()
301
+ d["access"] = self.access
302
+ d["generated"] = self.generated
303
+ return d
304
+
305
+
306
+ @dataclass
307
+ class Runnable:
308
+ name: str
309
+ path: str
310
+ symbol: str
311
+ file: str
312
+ line: int
313
+ events: list = field(default_factory=list)
314
+ points: list = field(default_factory=list) # [{"role", "point", "port", "target"}]
315
+ concurrent: bool = False
316
+ signature: str = "" # the runnable entry point (SWS_Rte_01126 / SWS_Rte_01166 for servers)
317
+ exclusive_areas: list = field(default_factory=list)
318
+
319
+ def to_dict(self) -> dict:
320
+ return {"name": self.name, "path": self.path, "symbol": self.symbol, "at": f"{self.file}:{self.line}",
321
+ "signature": self.signature, "events": self.events, "accessPoints": self.points,
322
+ "canBeInvokedConcurrently": self.concurrent}
323
+
324
+
325
+ @dataclass
326
+ class SwcModel:
327
+ name: str
328
+ path: str
329
+ tag: str
330
+ file: str
331
+ line: int
332
+ behavior: Optional[str]
333
+ multiple_instantiation: bool
334
+ ports: list
335
+ runnables: list
336
+ apis: dict # name -> ApiEntry (every derivable API)
337
+ types: dict # type name -> TypeInfo dict (data types used by the SW-C)
338
+ notes: list = field(default_factory=list)
339
+ unresolved: list = field(default_factory=list) # access points whose reference does not resolve
340
+
341
+ def runnable(self, name_or_symbol: str) -> Optional[Runnable]:
342
+ for r in self.runnables:
343
+ if name_or_symbol in (r.name, r.symbol):
344
+ return r
345
+ return None
346
+
347
+ def allowed(self, runnable: str) -> list:
348
+ return [a for a in self.apis.values() if a.allowed_for(runnable)]
349
+
350
+ def generated(self) -> list:
351
+ return [a for a in self.apis.values() if a.generated]
352
+
353
+ def classify(self, name: str, runnable: Optional[str]) -> tuple:
354
+ """-> (status, entry|None, reason). status: OK | NOT-ALLOWED | INVENTED."""
355
+ e = self.apis.get(name)
356
+ if e is None:
357
+ return "INVENTED", None, explain_unknown(self, name)
358
+ if runnable is None:
359
+ return ("OK" if e.generated else "NOT-ALLOWED"), e, (
360
+ "generated for this SW-C" if e.generated else "derivable, but no runnable has the access point")
361
+ if e.allowed_for(runnable):
362
+ return "OK", e, f"{runnable} has the access point"
363
+ others = sorted({a["runnable"] for a in e.access})
364
+ return "NOT-ALLOWED", e, (f"{name} exists on {self.name} but runnable {runnable} has no "
365
+ f"{needed_role(e)} for it" + (f" (only {', '.join(others)} do)" if others else
366
+ " (no runnable has one, so it is not generated)"))
367
+
368
+ def to_dict(self, runnable: Optional[str] = None) -> dict:
369
+ d = {"swc": self.name, "path": self.path, "tag": self.tag, "at": f"{self.file}:{self.line}",
370
+ "behavior": self.behavior, "multipleInstantiation": self.multiple_instantiation, "ports": self.ports,
371
+ "runnables": [r.to_dict() for r in self.runnables if runnable in (None, r.name, r.symbol)],
372
+ "counts": {"derivable": len(self.apis), "generated": len(self.generated())}}
373
+ if runnable:
374
+ r = self.runnable(runnable)
375
+ d["allowed"] = [a.to_dict() for a in self.allowed(r.name)] if r else []
376
+ else:
377
+ d["allowedByRunnable"] = {r.name: sorted(a.name for a in self.allowed(r.name)) for r in self.runnables}
378
+ d["apis"] = [a.to_dict() for a in sorted(self.apis.values(), key=lambda a: a.name)]
379
+ if self.notes:
380
+ d["notes"] = self.notes
381
+ if self.unresolved:
382
+ d["unresolved"] = self.unresolved
383
+ return d
384
+
385
+
386
+ def needed_role(e: ApiEntry) -> str:
387
+ return {"Write": "dataSendPoint", "Send": "dataSendPoint", "Invalidate": "dataSendPoint",
388
+ "Feedback": "dataSendPoint", "Read": "dataReceivePointByArgument",
389
+ "Receive": "dataReceivePointByArgument", "IsUpdated": "dataReceivePointByArgument",
390
+ "DRead": "dataReceivePointByValue", "IRead": "dataReadAccess", "IReadRef": "dataReadAccess",
391
+ "IStatus": "dataReadAccess", "IWrite": "dataWriteAccess", "IWriteRef": "dataWriteAccess",
392
+ "IInvalidate": "dataWriteAccess", "Call": "serverCallPoint",
393
+ "Result": "asynchronousServerCallResultPoint", "Mode": "modeAccessPoint", "Switch": "modeSwitchPoint",
394
+ "SwitchAck": "modeSwitchPoint", "Prm": "parameterAccess", "CData": "parameterAccess",
395
+ "IrvRead": "readLocalVariable", "IrvIRead": "readLocalVariable", "IrvIReadRef": "readLocalVariable",
396
+ "IrvWrite": "writtenLocalVariable", "IrvIWrite": "writtenLocalVariable",
397
+ "IrvIWriteRef": "writtenLocalVariable", "Enter": "canEnterExclusiveArea",
398
+ "Exit": "canEnterExclusiveArea", "Trigger": "externalTriggeringPoint",
399
+ "IrTrigger": "internalTriggeringPoint"}.get(e.api, "access point")
400
+
401
+
402
+ def split_name(name: str) -> tuple:
403
+ """'Rte_IWrite_Re_Port_El' -> ('IWrite', 'Re_Port_El'); unknown kinds -> (None, rest)."""
404
+ if not name.startswith("Rte_"):
405
+ return None, name
406
+ rest = name[4:]
407
+ if rest.startswith("Byps_"):
408
+ rest = rest[5:]
409
+ for k in KINDS:
410
+ if rest.startswith(k + "_"):
411
+ return k, rest[len(k) + 1:]
412
+ return None, rest
413
+
414
+
415
+ def explain_unknown(m: SwcModel, name: str) -> str:
416
+ kind, rest = split_name(name)
417
+ if kind is None:
418
+ return f"{name} is not an RTE API name pattern of SWS_RTE 5.6 (Rte_<Write|Read|Call|...>_...)"
419
+ if kind in PER_RUNNABLE:
420
+ re_names = [r.name for r in m.runnables if rest.startswith(r.name + "_") or rest == r.name]
421
+ if not re_names:
422
+ return (f"{name}: Rte_{kind} names the runnable first (Rte_{kind}_<re>_...); no runnable of {m.name} "
423
+ f"prefixes '{rest}' (runnables: {', '.join(r.name for r in m.runnables)})")
424
+ rest = rest[len(max(re_names, key=len)) + 1:]
425
+ if kind in ("Pim", "CData", "Enter", "Exit") or kind.startswith("Irv") or kind == "IrTrigger":
426
+ return f"{name}: {m.name} declares no {kind} target named '{rest}'"
427
+ ports = [p for p in m.ports if rest.startswith(p["name"] + "_")]
428
+ if not ports:
429
+ return f"{name}: {m.name} has no port that prefixes '{rest}' (ports: {', '.join(p['name'] for p in m.ports)})"
430
+ p = max(ports, key=lambda x: len(x["name"]))
431
+ el = rest[len(p["name"]) + 1:]
432
+ els = [e["name"] for e in p["elements"]]
433
+ if el not in els:
434
+ return f"{name}: interface {p['interface']} of port {p['name']} has no element '{el}' (has: {', '.join(els)})"
435
+ e = next(e for e in p["elements"] if e["name"] == el)
436
+ return (f"{name}: Rte_{kind} does not apply to {p['direction']}-port {p['name']} element {el} "
437
+ f"({p['interfaceKind']}{', ' + e['semantics'] + ' semantics' if e.get('semantics') else ''}"
438
+ f"{'; ' + e['needs'] if e.get('needs') and kind in ('Invalidate', 'IInvalidate', 'IsUpdated') else ''})")
439
+
440
+
441
+ class Builder:
442
+ def __init__(self, ix: ArIndex, types: Types):
443
+ self.ix, self.t = ix, types
444
+
445
+ def rel(self, f: Optional[str]) -> str:
446
+ if not f:
447
+ return "?"
448
+ r = os.path.relpath(f, self.ix.root)
449
+ return f if r.startswith("..") else r
450
+
451
+ def build(self, swc: Node) -> SwcModel:
452
+ ix = self.ix
453
+ spath = ix.path(swc)
454
+ f, line = ix.where(swc)
455
+ ib = next(iter(_own(swc, "INTERNAL-BEHAVIORS")), None)
456
+ maps = [r.text.strip() for r in (ib.child("DATA-TYPE-MAPPING-REFS").children if ib is not None and
457
+ ib.child("DATA-TYPE-MAPPING-REFS") is not None else [])]
458
+ multi = (ib.child_text("SUPPORTS-MULTIPLE-INSTANTIATION") or "").lower() == "true" if ib is not None else False
459
+ m = SwcModel(swc.short_name, spath, swc.tag, self.rel(f), line, ix.path(ib) if ib is not None else None,
460
+ multi, [], [], {}, {})
461
+ self.m, self.maps = m, maps
462
+ inst = [{"name": "self", "type": "Rte_Instance self", "dir": "IN"}] if multi else []
463
+ self.inst = inst
464
+
465
+ # ---- ports and everything derivable from them
466
+ for p in _own(swc, "PORTS"):
467
+ direction = {"P-PORT-PROTOTYPE": "P", "R-PORT-PROTOTYPE": "R", "PR-PORT-PROTOTYPE": "PR"}.get(p.tag)
468
+ if direction is None:
469
+ continue
470
+ iref = p.child_text("PROVIDED-INTERFACE-TREF") or p.child_text("REQUIRED-INTERFACE-TREF") or \
471
+ p.child_text("PROVIDED-REQUIRED-INTERFACE-TREF")
472
+ itf = ix.get(iref)
473
+ port = {"name": p.short_name, "path": ix.path(p), "direction": direction, "interface": iref,
474
+ "interfaceKind": itf.tag if itf is not None else "?", "elements": [],
475
+ "at": f"{self.rel(ix.where(p)[0])}:{p.line}"}
476
+ m.ports.append(port)
477
+ if itf is None:
478
+ m.notes.append(f"port {p.short_name}: interface {iref} not found in the project ARXML")
479
+ continue
480
+ self.port_apis(p, port, itf, direction)
481
+
482
+ # ---- internal behaviour: IRVs, PIM, internal parameters, exclusive areas
483
+ irvs = {}
484
+ if ib is not None:
485
+ for coll, kind in (("EXPLICIT-INTER-RUNNABLE-VARIABLES", "explicit"),
486
+ ("IMPLICIT-INTER-RUNNABLE-VARIABLES", "implicit")):
487
+ for v in _own(ib, coll):
488
+ irvs[ix.path(v)] = (v, kind)
489
+ for pim in _own(ib, "PER-INSTANCE-MEMORYS"):
490
+ tname = pim.child_text("TYPE") or "void"
491
+ self.add(ApiEntry(f"Rte_Pim_{pim.short_name}", "Pim", f"{tname}*", list(inst), ix.path(pim),
492
+ type=TypeInfo(tname, "unknown"), any_runnable=True))
493
+ for pim in _own(ib, "AR-TYPED-PER-INSTANCE-MEMORYS"):
494
+ t = self.t.resolve(pim.child_text("TYPE-TREF"), maps)
495
+ self.add(ApiEntry(f"Rte_Pim_{pim.short_name}", "Pim", f"{t.name}*", list(inst), ix.path(pim),
496
+ type=t, any_runnable=True))
497
+ for coll in ("PER-INSTANCE-PARAMETERS", "SHARED-PARAMETERS"):
498
+ for prm in _own(ib, coll):
499
+ t = self.t.resolve(prm.child_text("TYPE-TREF"), maps)
500
+ self.add(ApiEntry(f"Rte_CData_{prm.short_name}", "CData", ret_of(t), list(inst), ix.path(prm),
501
+ element=prm.short_name, type=t))
502
+ for ea in _own(ib, "EXCLUSIVE-AREAS"):
503
+ for k in ("Enter", "Exit"):
504
+ self.add(ApiEntry(f"Rte_{k}_{ea.short_name}", k, "void", list(inst), ix.path(ea),
505
+ element=ea.short_name))
506
+
507
+ # ---- runnables, events and their access points
508
+ events: dict = {}
509
+ if ib is not None:
510
+ for ev in _own(ib, "EVENTS"):
511
+ tgt = ev.child_text("START-ON-EVENT-REF")
512
+ if tgt:
513
+ d = {"event": ev.short_name, "kind": ev.tag}
514
+ if ev.child_text("PERIOD"):
515
+ d["period"] = ev.child_text("PERIOD")
516
+ trig = _ref_ending(ev, "TARGET-DATA-ELEMENT-REF", "TARGET-PROVIDED-OPERATION-REF",
517
+ "TARGET-MODE-DECLARATION-REF", "TARGET-REQUIRED-OPERATION-REF", "TARGET-TRIGGER-REF")
518
+ port_ref = _ref_ending(ev, "PORT-REF")
519
+ if trig:
520
+ d["target"] = trig
521
+ if port_ref:
522
+ d["port"] = port_ref.rsplit("/", 1)[-1]
523
+ events.setdefault(tgt, []).append(d)
524
+ for r in (_own(ib, "RUNNABLES") if ib is not None else []):
525
+ rp = ix.path(r)
526
+ run = Runnable(r.short_name, rp, r.child_text("SYMBOL") or r.short_name, self.rel(ix.where(r)[0]), r.line,
527
+ events.get(rp, []),
528
+ concurrent=(r.child_text("CAN-BE-INVOKED-CONCURRENTLY") or "").lower() == "true")
529
+ run.signature = self.entry_signature(run, inst)
530
+ m.runnables.append(run)
531
+ for coll in ROLE:
532
+ for ap in _own(r, coll):
533
+ self.access_point(run, coll, ap, irvs)
534
+ eas = r.child("CAN-ENTER-EXCLUSIVE-AREA-REFS")
535
+ for ref in (eas.children if eas is not None else []):
536
+ ea = ref.text.strip()
537
+ run.exclusive_areas.append(ea)
538
+ for k in ("Enter", "Exit"):
539
+ e = self.m.apis.get(f"Rte_{k}_{ea.rsplit('/', 1)[-1]}")
540
+ if e is not None:
541
+ e.access.append({"runnable": run.name, "role": "canEnterExclusiveArea", "point": ea})
542
+ # per-runnable IRV APIs that exist for every runnable but are allowed only with an access point
543
+ for run in m.runnables:
544
+ for vp, (v, kind) in irvs.items():
545
+ t = self.t.resolve(v.child_text("TYPE-TREF"), maps)
546
+ self.irv_apis(run.name, v.short_name, vp, t, kind)
547
+ for r in m.runnables:
548
+ for pt in r.points:
549
+ if pt.get("unresolved"):
550
+ m.unresolved.append({"runnable": r.name, **pt})
551
+ for e in m.apis.values():
552
+ if e.type is not None and e.type.name not in m.types:
553
+ m.types[e.type.name] = e.type.to_dict()
554
+ return m
555
+
556
+ # -- helpers
557
+ def add(self, e: ApiEntry) -> ApiEntry:
558
+ if e.name in self.m.apis:
559
+ return self.m.apis[e.name]
560
+ self.m.apis[e.name] = e
561
+ return e
562
+
563
+ def entry_signature(self, run: Runnable, inst: list) -> str:
564
+ """Runnable entry point: void <symbol>([Rte_Instance]) (SWS_Rte_01126); a server runnable returns
565
+ Std_ReturnType and takes the operation's arguments (SWS_Rte_01166)."""
566
+ for ev in run.events:
567
+ if ev["kind"] == "OPERATION-INVOKED-EVENT":
568
+ op = self.ix.get(ev.get("target"))
569
+ if op is not None:
570
+ args = [p["type"] for p in self.op_args(op, ("IN", "INOUT", "OUT"))]
571
+ ps = ", ".join([x["type"] for x in inst] + args) or "void"
572
+ return f"Std_ReturnType {run.symbol}({ps})"
573
+ return f"void {run.symbol}({'Rte_Instance self' if inst else 'void'})"
574
+
575
+ def op_args(self, op: Node, dirs: tuple) -> list:
576
+ out = []
577
+ for a in _own(op, "ARGUMENTS"):
578
+ d = (a.child_text("DIRECTION") or "IN").upper()
579
+ if d not in dirs:
580
+ continue
581
+ t = self.t.resolve(a.child_text("TYPE-TREF"), self.maps)
582
+ typ = in_param(t, a.short_name) if d == "IN" else out_param(t, a.short_name)
583
+ out.append({"name": a.short_name, "type": typ, "dir": d})
584
+ return out
585
+
586
+ def port_apis(self, p: Node, port: dict, itf: Node, direction: str):
587
+ ix, inst, maps = self.ix, self.inst, self.maps
588
+ pname, ppath = p.short_name, ix.path(p)
589
+ prov, req = direction in ("P", "PR"), direction in ("R", "PR")
590
+ kind = itf.tag
591
+ if kind in ("SENDER-RECEIVER-INTERFACE", "NV-DATA-INTERFACE"):
592
+ inv = {}
593
+ for ip in _own(itf, "INVALIDATION-POLICYS"):
594
+ inv[ip.child_text("DATA-ELEMENT-REF") or ""] = (ip.child_text("HANDLE-INVALID") or "").upper()
595
+ ack = any(_desc(cs, "TRANSMISSION-ACKNOWLEDGE") is not None for cs in _own(p, "PROVIDED-COM-SPECS"))
596
+ for el in _own(itf, "DATA-ELEMENTS") + _own(itf, "NV-DATAS"):
597
+ ep = ix.path(el)
598
+ t = self.t.resolve(el.child_text("TYPE-TREF"), maps)
599
+ queued = (_desc_text(el, "SW-IMPL-POLICY") or "").upper() == "QUEUED"
600
+ sem = "event" if queued else "data"
601
+ invalid = inv.get(ep, "") in INVALIDATING
602
+ upd = self.rcv_comspec(p, ep, "ENABLE-UPDATE") == "true"
603
+ status = any(self.rcv_comspec(p, ep, k) not in (None, "", "0", "false", "none", "NONE")
604
+ for k in ("ALIVE-TIMEOUT", "HANDLE-NEVER-RECEIVED", "HANDLE-OUT-OF-RANGE",
605
+ "HANDLE-DATA-STATUS")) or invalid
606
+ needs = []
607
+ port["elements"].append({"name": el.short_name, "path": ep, "type": t.name, "semantics": sem})
608
+ common = dict(port=pname, port_path=ppath, element=el.short_name, type=t, semantics=sem, target=ep)
609
+ if prov:
610
+ if queued:
611
+ self.add(ApiEntry(f"Rte_Send_{pname}_{el.short_name}", "Send", "Std_ReturnType",
612
+ inst + [{"name": "data", "type": in_param(t), "dir": "IN"}], **common))
613
+ else:
614
+ self.add(ApiEntry(f"Rte_Write_{pname}_{el.short_name}", "Write", "Std_ReturnType",
615
+ inst + [{"name": "data", "type": in_param(t), "dir": "IN"}], **common))
616
+ if invalid:
617
+ self.add(ApiEntry(f"Rte_Invalidate_{pname}_{el.short_name}", "Invalidate",
618
+ "Std_ReturnType", list(inst), condition="InvalidationPolicy", **common))
619
+ else:
620
+ needs.append("Rte_Invalidate/IInvalidate need an InvalidationPolicy keep/replace")
621
+ if ack:
622
+ self.add(ApiEntry(f"Rte_Feedback_{pname}_{el.short_name}", "Feedback", "Std_ReturnType",
623
+ list(inst), condition="TransmissionAcknowledgementRequest", **common))
624
+ if req:
625
+ if queued:
626
+ self.add(ApiEntry(f"Rte_Receive_{pname}_{el.short_name}", "Receive", "Std_ReturnType",
627
+ inst + [{"name": "data", "type": out_param(t), "dir": "OUT"}], **common))
628
+ else:
629
+ self.add(ApiEntry(f"Rte_Read_{pname}_{el.short_name}", "Read", "Std_ReturnType",
630
+ inst + [{"name": "data", "type": out_param(t), "dir": "OUT"}], **common))
631
+ if t.kind in ("primitive", "unknown", "pointer"): # SWS_Rte_07395: primitive types only
632
+ self.add(ApiEntry(f"Rte_DRead_{pname}_{el.short_name}", "DRead", t.name, list(inst),
633
+ **common))
634
+ if upd:
635
+ self.add(ApiEntry(f"Rte_IsUpdated_{pname}_{el.short_name}", "IsUpdated", "boolean",
636
+ list(inst), condition="enableUpdate", **common))
637
+ else:
638
+ needs.append("Rte_IsUpdated needs enableUpdate in the NonqueuedReceiverComSpec")
639
+ port["elements"][-1]["_flags"] = {"invalid": invalid, "status": status, "queued": queued, "t": t}
640
+ if needs:
641
+ port["elements"][-1]["needs"] = "; ".join(needs)
642
+ elif kind == "CLIENT-SERVER-INTERFACE":
643
+ for op in _own(itf, "OPERATIONS"):
644
+ port["elements"].append({"name": op.short_name, "path": ix.path(op), "kind": "operation"})
645
+ if req: # sync signature; the async variant replaces it when an async call point is found
646
+ self.add(ApiEntry(f"Rte_Call_{pname}_{op.short_name}", "Call", "Std_ReturnType",
647
+ inst + self.op_args(op, ("IN", "INOUT", "OUT")), ix.path(op), port=pname,
648
+ port_path=ppath, element=op.short_name))
649
+ elif kind == "MODE-SWITCH-INTERFACE":
650
+ # the ModeDeclarationGroupPrototype is serialised under its role name <MODE-GROUP>
651
+ for mg in [c for c in itf.children if c.tag in ("MODE-GROUP", "MODE-DECLARATION-GROUP-PROTOTYPE")]:
652
+ t = self.t.mode_type(mg, maps)
653
+ port["elements"].append({"name": mg.short_name, "path": ix.path(mg), "kind": "modeGroup", "type": t.name})
654
+ common = dict(port=pname, port_path=ppath, element=mg.short_name, type=t, target=ix.path(mg))
655
+ self.add(ApiEntry(f"Rte_Mode_{pname}_{mg.short_name}", "Mode", t.name, list(inst), **common))
656
+ if prov:
657
+ self.add(ApiEntry(f"Rte_Switch_{pname}_{mg.short_name}", "Switch", "Std_ReturnType",
658
+ inst + [{"name": "mode", "type": f"{t.name} mode", "dir": "IN"}], **common))
659
+ if any(_desc(cs, "MODE-SWITCHED-ACK") is not None for cs in _own(p, "PROVIDED-COM-SPECS")):
660
+ self.add(ApiEntry(f"Rte_SwitchAck_{pname}_{mg.short_name}", "SwitchAck", "Std_ReturnType",
661
+ list(inst), condition="ModeSwitchedAckRequest", **common))
662
+ elif kind == "PARAMETER-INTERFACE":
663
+ for prm in _own(itf, "PARAMETERS"):
664
+ t = self.t.resolve(prm.child_text("TYPE-TREF"), maps)
665
+ port["elements"].append({"name": prm.short_name, "path": ix.path(prm), "kind": "parameter", "type": t.name})
666
+ if req:
667
+ self.add(ApiEntry(f"Rte_Prm_{pname}_{prm.short_name}", "Prm", ret_of(t), list(inst), ix.path(prm),
668
+ port=pname, port_path=ppath, element=prm.short_name, type=t))
669
+ elif kind == "TRIGGER-INTERFACE":
670
+ for tr in _own(itf, "TRIGGERS"):
671
+ port["elements"].append({"name": tr.short_name, "path": ix.path(tr), "kind": "trigger"})
672
+ if prov:
673
+ self.add(ApiEntry(f"Rte_Trigger_{pname}_{tr.short_name}", "Trigger", "Std_ReturnType", list(inst),
674
+ ix.path(tr), port=pname, port_path=ppath, element=tr.short_name))
675
+
676
+ def rcv_comspec(self, p: Node, elem_path: str, tag: str) -> Optional[str]:
677
+ for cs in _own(p, "REQUIRED-COM-SPECS"):
678
+ if (cs.child_text("DATA-ELEMENT-REF") or "") == elem_path:
679
+ v = cs.child_text(tag)
680
+ if v is not None:
681
+ return v.lower()
682
+ return None
683
+
684
+ def per_runnable_sr(self, run: str, port: dict, el: dict):
685
+ """Implicit APIs for one runnable / port / element (names carry the runnable)."""
686
+ f = el.get("_flags")
687
+ if not f or f["queued"]:
688
+ return
689
+ t, inst = f["t"], self.inst
690
+ pn, en = port["name"], el["name"]
691
+ common = dict(port=pn, port_path=port["path"], element=en, type=t, semantics="data", target=el["path"],
692
+ runnable=run)
693
+ if port["direction"] in ("R", "PR"):
694
+ self.add(ApiEntry(f"Rte_IRead_{run}_{pn}_{en}", "IRead", ret_of(t), list(inst), **common))
695
+ self.add(ApiEntry(f"Rte_IReadRef_{run}_{pn}_{en}", "IReadRef", f"const {t.base or t.name}*"
696
+ if t.kind == "array" else f"const {t.name}*", list(inst), **common))
697
+ if f["status"]:
698
+ self.add(ApiEntry(f"Rte_IStatus_{run}_{pn}_{en}", "IStatus", "Std_ReturnType", list(inst),
699
+ condition="data element status (aliveTimeout/handleNeverReceived/...)", **common))
700
+ if port["direction"] in ("P", "PR"):
701
+ self.add(ApiEntry(f"Rte_IWrite_{run}_{pn}_{en}", "IWrite", "void",
702
+ inst + [{"name": "data", "type": in_param(t), "dir": "IN"}], **common))
703
+ self.add(ApiEntry(f"Rte_IWriteRef_{run}_{pn}_{en}", "IWriteRef", f"{t.base or t.name}*"
704
+ if t.kind == "array" else f"{t.name}*", list(inst), **common))
705
+ if f["invalid"]:
706
+ self.add(ApiEntry(f"Rte_IInvalidate_{run}_{pn}_{en}", "IInvalidate", "void", list(inst),
707
+ condition="InvalidationPolicy", **common))
708
+
709
+ def irv_apis(self, run: str, name: str, vpath: str, t: TypeInfo, kind: str):
710
+ inst = self.inst
711
+ common = dict(element=name, type=t, target=vpath, runnable=run)
712
+ if kind == "explicit":
713
+ if t.kind in ("struct", "union", "array"): # complex type signature (SWS_Rte_03560)
714
+ self.add(ApiEntry(f"Rte_IrvRead_{run}_{name}", "IrvRead", "void",
715
+ inst + [{"name": "data", "type": out_param(t), "dir": "OUT"}], **common))
716
+ else:
717
+ self.add(ApiEntry(f"Rte_IrvRead_{run}_{name}", "IrvRead", t.name, list(inst), **common))
718
+ self.add(ApiEntry(f"Rte_IrvWrite_{run}_{name}", "IrvWrite", "void",
719
+ inst + [{"name": "data", "type": in_param(t), "dir": "IN"}], **common))
720
+ else:
721
+ self.add(ApiEntry(f"Rte_IrvIRead_{run}_{name}", "IrvIRead", ret_of(t), list(inst), **common))
722
+ self.add(ApiEntry(f"Rte_IrvIReadRef_{run}_{name}", "IrvIReadRef", f"const {t.base or t.name}*"
723
+ if t.kind == "array" else f"const {t.name}*", list(inst), **common))
724
+ self.add(ApiEntry(f"Rte_IrvIWrite_{run}_{name}", "IrvIWrite", "void",
725
+ inst + [{"name": "data", "type": in_param(t), "dir": "IN"}], **common))
726
+ self.add(ApiEntry(f"Rte_IrvIWriteRef_{run}_{name}", "IrvIWriteRef", f"{t.base or t.name}*"
727
+ if t.kind == "array" else f"{t.name}*", list(inst), **common))
728
+
729
+ def access_point(self, run: Runnable, coll: str, ap: Node, irvs: dict):
730
+ ix, m = self.ix, self.m
731
+ role = ROLE[coll]
732
+ point = ix.path(ap)
733
+ rec = {"role": role, "point": point}
734
+ grant = lambda name: (m.apis[name].access.append({"runnable": run.name, "role": role, "point": point}) # noqa: E731
735
+ if name in m.apis else None)
736
+ if coll in ("READ-LOCAL-VARIABLES", "WRITTEN-LOCAL-VARIABLES"):
737
+ vref = _ref_ending(ap, "LOCAL-VARIABLE-REF")
738
+ rec["target"] = vref
739
+ run.points.append(rec)
740
+ hit = irvs.get(vref or "")
741
+ if hit is None:
742
+ rec["unresolved"] = f"inter-runnable variable {vref} not declared in the behaviour"
743
+ return
744
+ v, kind = hit
745
+ t = self.t.resolve(v.child_text("TYPE-TREF"), self.maps)
746
+ self.irv_apis(run.name, v.short_name, vref, t, kind)
747
+ names = (["IrvRead"] if kind == "explicit" else ["IrvIRead", "IrvIReadRef"]) if coll.startswith("READ") else \
748
+ (["IrvWrite"] if kind == "explicit" else ["IrvIWrite", "IrvIWriteRef"])
749
+ for k in names:
750
+ grant(f"Rte_{k}_{run.name}_{v.short_name}")
751
+ return
752
+ if coll == "PARAMETER-ACCESSS":
753
+ local = _ref_ending(ap, "LOCAL-PARAMETER-REF")
754
+ if local:
755
+ rec["target"] = local
756
+ run.points.append(rec)
757
+ name = f"Rte_CData_{local.rsplit('/', 1)[-1]}"
758
+ if name not in m.apis:
759
+ rec["unresolved"] = f"internal parameter {local} not declared"
760
+ grant(name)
761
+ return
762
+ if coll == "INTERNAL-TRIGGERING-POINTS":
763
+ rec["target"] = point
764
+ run.points.append(rec)
765
+ self.add(ApiEntry(f"Rte_IrTrigger_{run.name}_{ap.short_name}", "IrTrigger", "void", list(self.inst), point,
766
+ element=ap.short_name, runnable=run.name))
767
+ grant(f"Rte_IrTrigger_{run.name}_{ap.short_name}")
768
+ return
769
+ if coll == "ASYNCHRONOUS-SERVER-CALL-RESULT-POINTS":
770
+ call = ix.get(ap.child_text("ASYNCHRONOUS-SERVER-CALL-POINT-REF"))
771
+ if call is None:
772
+ rec["unresolved"] = "ASYNCHRONOUS-SERVER-CALL-POINT-REF does not resolve"
773
+ run.points.append(rec)
774
+ return
775
+ ap = call # the result point inherits port/operation from its call point
776
+ port_ref = _ref_ending(ap, "PORT-REF", "PORT-PROTOTYPE-REF")
777
+ tgt = _ref_ending(ap, "TARGET-DATA-PROTOTYPE-REF", "TARGET-REQUIRED-OPERATION-REF", "TARGET-MODE-GROUP-REF",
778
+ "TARGET-TRIGGER-REF", "TARGET-DATA-ELEMENT-REF", "TARGET-PROVIDED-OPERATION-REF")
779
+ rec.update({"port": (port_ref or "").rsplit("/", 1)[-1] or None, "target": tgt})
780
+ run.points.append(rec)
781
+ port = next((p for p in m.ports if p["path"] == port_ref), None)
782
+ if port is None:
783
+ rec["unresolved"] = f"port {port_ref} is not a port of {m.name}"
784
+ return
785
+ el = next((e for e in port["elements"] if e["path"] == tgt), None)
786
+ if el is None:
787
+ rec["unresolved"] = f"{tgt} is not an element of {port['interface']}"
788
+ return
789
+ pn, en = port["name"], el["name"]
790
+ if coll in ("DATA-READ-ACCESSS", "DATA-WRITE-ACCESSS"):
791
+ self.per_runnable_sr(run.name, port, el)
792
+ kinds = ["IRead", "IReadRef", "IStatus"] if coll == "DATA-READ-ACCESSS" else ["IWrite", "IWriteRef", "IInvalidate"]
793
+ for k in kinds:
794
+ grant(f"Rte_{k}_{run.name}_{pn}_{en}")
795
+ return
796
+ kinds = {"DATA-SEND-POINTS": ["Write", "Send", "Invalidate", "Feedback"],
797
+ "DATA-RECEIVE-POINT-BY-ARGUMENTS": ["Read", "Receive", "IsUpdated"],
798
+ "DATA-RECEIVE-POINT-BY-VALUES": ["DRead", "IsUpdated"],
799
+ "SERVER-CALL-POINTS": ["Call"], "ASYNCHRONOUS-SERVER-CALL-RESULT-POINTS": ["Result"],
800
+ "MODE-ACCESS-POINTS": ["Mode"], "MODE-SWITCH-POINTS": ["Switch", "SwitchAck"],
801
+ "PARAMETER-ACCESSS": ["Prm"], "EXTERNAL-TRIGGERING-POINTS": ["Trigger"]}[coll]
802
+ if coll == "SERVER-CALL-POINTS" and ap.tag == "ASYNCHRONOUS-SERVER-CALL-POINT":
803
+ op = ix.get(tgt)
804
+ call = m.apis.get(f"Rte_Call_{pn}_{en}")
805
+ if call is not None and op is not None: # async: Call carries IN/INOUT, Result the OUT/INOUT
806
+ call.params = self.inst + self.op_args(op, ("IN", "INOUT"))
807
+ call.condition = "asynchronous"
808
+ self.add(ApiEntry(f"Rte_Result_{pn}_{en}", "Result", "Std_ReturnType",
809
+ self.inst + self.op_args(op, ("INOUT", "OUT")), tgt, port=pn, port_path=port["path"],
810
+ element=en, condition="asynchronous"))
811
+ for k in kinds:
812
+ grant(f"Rte_{k}_{pn}_{en}")
813
+
814
+
815
+ # ---------------------------------------------------------------- entry points
816
+ def build_all(root: str, ix: Optional[ArIndex] = None) -> list:
817
+ ix = ix or ArIndex(root)
818
+ b = Builder(ix, Types(ix))
819
+ out = []
820
+ for n in ix.of_tag(*SWC_TAGS):
821
+ out.append(b.build(n))
822
+ # derivable per-runnable implicit S/R APIs (names include the runnable): add after runnables are known
823
+ for m in out:
824
+ b.m, b.inst = m, ([{"name": "self", "type": "Rte_Instance self", "dir": "IN"}] if m.multiple_instantiation else [])
825
+ for r in m.runnables:
826
+ for p in m.ports:
827
+ for el in p["elements"]:
828
+ b.per_runnable_sr(r.name, p, el)
829
+ for p in m.ports:
830
+ for el in p["elements"]:
831
+ el.pop("_flags", None)
832
+ return out
833
+
834
+
835
+ def find_swc(models: list, name: Optional[str]) -> Optional[SwcModel]:
836
+ if not name:
837
+ return None
838
+ for m in models:
839
+ if name in (m.name, m.path):
840
+ return m
841
+ low = name.lower()
842
+ hits = [m for m in models if m.name.lower() == low]
843
+ return hits[0] if len(hits) == 1 else None
844
+
845
+
846
+ RTE_DECL = re.compile(r"\b(Rte_\w+)\s*\(")
847
+
848
+
849
+ def header_entries(files: list) -> dict:
850
+ """Rte_* macros and function prototypes declared in the given headers -> {name: {form, text, file, line}}."""
851
+ from .audit import parse_defines, strip_comments
852
+ out: dict = {}
853
+ for name, (params, body, f, line) in parse_defines(files).items():
854
+ if name.startswith("Rte_") and params is not None:
855
+ out.setdefault(name, {"form": "macro", "text": f"#define {name}({', '.join(params)}) {body}".strip(),
856
+ "file": f, "line": line})
857
+ for f in files:
858
+ try:
859
+ raw = open(f, encoding="utf-8", errors="replace").read()
860
+ except OSError:
861
+ continue
862
+ from .csrc import _blank_preproc
863
+ text = _blank_preproc(strip_comments(raw))
864
+ for st in re.finditer(r"[^;{}#]*?\b(Rte_\w+)\s*\([^;{}]*\)\s*;", text):
865
+ name = st.group(1)
866
+ ls = text.rfind("\n", 0, st.start(1)) + 1
867
+ if text[ls:st.start(1)].lstrip().startswith("#") or name in out:
868
+ continue
869
+ decl = " ".join(text[st.start():st.end()].split())
870
+ if re.match(r"^(?:return\b|\(void\))", decl) or "=" in decl.split(name)[0]:
871
+ continue
872
+ out[name] = {"form": "function", "text": decl, "file": f, "line": text.count("\n", 0, st.start(1)) + 1}
873
+ return out
874
+
875
+
876
+ def find_header(root: str, swc: str, c_files: Optional[list] = None) -> Optional[str]:
877
+ want = f"rte_{swc}.h".lower()
878
+ files = c_files if c_files is not None else paths.walk(root, paths.is_c).files
879
+ hits = sorted(f for f in files if os.path.basename(f).lower() == want)
880
+ return hits[0] if hits else None
881
+
882
+
883
+ def match_header(m: SwcModel, header: Optional[str], rel=lambda f: f) -> dict:
884
+ """Optional second step: match each generated API to its declaration/macro in Rte_<Swc>.h."""
885
+ if not header:
886
+ return {"header": None, "note": f"no Rte_{m.name}.h in the project; nothing to match"}
887
+ decl = header_entries([header])
888
+ gen = {a.name for a in m.generated()}
889
+ matched = {}
890
+ for a in sorted(m.apis.values(), key=lambda x: x.name):
891
+ d = decl.get(a.name)
892
+ if d:
893
+ matched[a.name] = {"form": d["form"], "text": d["text"], "at": f"{rel(d['file'])}:{d['line']}"}
894
+ missing = sorted(gen - set(decl))
895
+ extra = []
896
+ for name in sorted(set(decl) - gen):
897
+ e = m.apis.get(name)
898
+ extra.append({"name": name, "at": f"{rel(decl[name]['file'])}:{decl[name]['line']}",
899
+ "why": "derivable but no runnable has the access point" if e is not None
900
+ else explain_unknown(m, name)})
901
+ symbols = {}
902
+ try:
903
+ text = open(header, encoding="utf-8", errors="replace").read()
904
+ except OSError:
905
+ text = ""
906
+ for r in m.runnables:
907
+ mm = re.search(rf"^[^\n#]*\b{re.escape(r.symbol)}\s*\([^)]*\)\s*;", text, re.M)
908
+ symbols[r.name] = mm.group(0).strip() if mm else None
909
+ return {"header": rel(header), "matched": matched, "missing": missing, "extra": extra,
910
+ "runnablePrototypes": symbols,
911
+ "counts": {"generatedExpected": len(gen), "declared": len([n for n in decl if n in gen]),
912
+ "missing": len(missing), "extra": len(extra)}}
913
+
914
+
915
+ def rte_api(root: str, swc: Optional[str] = None, runnable: Optional[str] = None, header: bool = True) -> dict:
916
+ ix = ArIndex(root)
917
+ models = build_all(root, ix)
918
+ rel = lambda f: os.path.relpath(f, root) if not os.path.relpath(f, root).startswith("..") else f # noqa: E731
919
+ if swc:
920
+ m = find_swc(models, swc)
921
+ if m is None:
922
+ return {"error": f"no SW-COMPONENT-TYPE {swc!r}; known: {', '.join(x.name for x in models)}"}
923
+ models = [m]
924
+ if runnable and m.runnable(runnable) is None:
925
+ return {"error": f"{m.name} has no runnable {runnable!r}; runnables: {', '.join(r.name for r in m.runnables)}"}
926
+ cfiles = paths.walk(root, paths.is_c).files if header else []
927
+ out = []
928
+ for m in models:
929
+ d = m.to_dict(runnable)
930
+ if header:
931
+ d["header"] = match_header(m, find_header(root, m.name, cfiles), rel)
932
+ out.append(d)
933
+ return {"swcs": out, "summary": {"swcs": len(out), "runnables": sum(len(x["runnables"]) for x in out),
934
+ "derivable": sum(x["counts"]["derivable"] for x in out),
935
+ "generated": sum(x["counts"]["generated"] for x in out)}}