@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
@@ -1,4 +1,5 @@
1
- """CLI: python -m ecuc_engine [--root DIR] <info|read|edit|validate|diff> ... --json
1
+ """CLI: python -m ecuc_engine [--root DIR] <info|read|edit|validate|diff|audit|codegen|codegen-diff|rte-api|verify-rte|
2
+ build-context> ... --json
2
3
 
3
4
  Always prints one JSON object on stdout. {"ok": false, "error": ...} on failure (exit code 0 for
4
5
  handled errors, so callers parse stdout rather than the status).
@@ -49,16 +50,68 @@ def main(argv=None) -> int:
49
50
  v = sub.add_parser("validate")
50
51
  v.add_argument("--file", action="append")
51
52
  v.add_argument("--module")
53
+ v.add_argument("--check-generated", dest="check_generated", action="store_true", default=None,
54
+ help="regenerate in a scratch copy and report stale generated files (H2L-GEN-001)")
55
+ v.add_argument("--no-check-generated", dest="check_generated", action="store_false")
56
+ v.add_argument("--no-code", dest="code", action="store_false", help="skip the C-usage completeness check")
52
57
  v.add_argument("--max", type=int, default=0, help="cap the number of diagnostics returned (0 = all)")
53
58
  au = sub.add_parser("audit")
54
59
  au.add_argument("paths", nargs="*", help="C files/dirs to scan (default: audit.paths in .hydron/autosar.json, else Source/)")
55
60
  au.add_argument("--headers", action="append", help="generated header dirs for symbols and RTE macros (default Output/)")
61
+ asg = sub.add_parser("add-signal")
62
+ asg.add_argument("--name", required=True)
63
+ asg.add_argument("--ipdu", required=True)
64
+ asg.add_argument("--bit-size", dest="bit_size", type=int)
65
+ asg.add_argument("--type", dest="signal_type", default="UINT8")
66
+ asg.add_argument("--bit-position", dest="bit_position", type=int)
67
+ asg.add_argument("--endianness", default="LITTLE_ENDIAN")
68
+ asg.add_argument("--init", default="0")
69
+ asg.add_argument("--transfer-property", dest="transfer_property")
70
+ asg.add_argument("--dry-run", action="store_true")
71
+ ae = sub.add_parser("arxml-edit")
72
+ ae.add_argument("--ops", required=True)
73
+ ae.add_argument("--dry-run", action="store_true")
74
+ fd = sub.add_parser("find")
75
+ fd.add_argument("--module")
76
+ fd.add_argument("--container-def", dest="container_def")
77
+ fd.add_argument("--where", help='"Param=Value, Other>3" or a JSON object')
78
+ fd.add_argument("--ref-to", dest="ref_to")
79
+ fd.add_argument("--referenced-by", dest="referenced_by")
80
+ fd.add_argument("--limit", type=int, default=50)
81
+ cx = sub.add_parser("crosscheck")
82
+ cx.add_argument("module")
56
83
  so = sub.add_parser("signal-owners")
57
84
  so.add_argument("paths", nargs="+")
58
85
  d = sub.add_parser("diff")
59
86
  d.add_argument("a")
60
87
  d.add_argument("b")
61
- for p in (r, e, v, d, au, so): # allow --json after the subcommand too
88
+ cg = sub.add_parser("codegen", help="deterministic config code generation")
89
+ cg.add_argument("--module", action="append", help="module to generate (repeatable; default: all registered ones configured)")
90
+ cg.add_argument("--out", help="output dir (default: .hydron/codegen/out scratch; 'project' = codegen.outDir, else Output)")
91
+ cg.add_argument("--no-write-back", action="store_true", help="do not write assigned handle IDs into the ECUC ARXML")
92
+ cg.add_argument("--binding", help="static-code binding name ('none' = generic tables only)")
93
+ cd = sub.add_parser("codegen-diff", help="semantic diff of generated C against a reference dir")
94
+ cd.add_argument("reference")
95
+ cd.add_argument("generated", nargs="?", help="default: the codegen scratch dir")
96
+ cd.add_argument("--module", action="append")
97
+ cd.add_argument("--max", type=int, default=50, help="max element mismatches listed per object")
98
+ ra = sub.add_parser("rte-api", help="expected RTE API per SW-C / runnable (SWS_RTE naming and signatures)")
99
+ ra.add_argument("--swc")
100
+ ra.add_argument("--runnable")
101
+ ra.add_argument("--no-header", action="store_true", help="skip matching against Rte_<Swc>.h")
102
+ vr = sub.add_parser("verify-rte", help="classify Rte_* calls in C per runnable: OK / NOT-ALLOWED / INVENTED")
103
+ vr.add_argument("paths", nargs="*", help="C files/dirs/globs (default: all in-house C)")
104
+ vr.add_argument("--diff-against", dest="diff_against", help="only calls on lines changed since this git ref")
105
+ vr.add_argument("--swc")
106
+ vr.add_argument("--runnable")
107
+ vr.add_argument("--no-check-ops", dest="check_ops", action="store_false")
108
+ bc = sub.add_parser("build-context", help="deterministic context for implementing/changing a runnable")
109
+ bc.add_argument("--swc", required=True)
110
+ bc.add_argument("--runnable")
111
+ bc.add_argument("--task", required=True, help="task text, @file, or - for stdin")
112
+ bc.add_argument("--budget-tokens", dest="budget_tokens", type=int, default=24000)
113
+ 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
62
115
  p.add_argument("--json", action="store_true", help=argparse.SUPPRESS)
63
116
  a = ap.parse_args(argv)
64
117
  t0 = time.time()
@@ -74,7 +127,7 @@ def main(argv=None) -> int:
74
127
 
75
128
 
76
129
  def run(a) -> dict:
77
- if a.cmd == "edit":
130
+ if a.cmd in ("edit", "arxml-edit"):
78
131
  try:
79
132
  a.ops = _ops(a.ops)
80
133
  except (ValueError, OSError) as ex:
@@ -87,8 +140,64 @@ def run(a) -> dict:
87
140
  return diff(a.a, a.b, rel, root)
88
141
  except FileNotFoundError as ex:
89
142
  return {"error": str(ex)}
143
+ if a.cmd == "codegen-diff":
144
+ from .codegen import SCRATCH
145
+ from .codegen.cdiff import diff_dirs
146
+ root = os.path.abspath(a.root)
147
+ ab = lambda p: p if os.path.isabs(p) else os.path.join(root, p) # noqa: E731
148
+ ref, gen = ab(a.reference), ab(a.generated or SCRATCH)
149
+ for p in (ref, gen):
150
+ if not os.path.exists(p):
151
+ return {"error": f"{p} does not exist" + (" (run codegen first)" if p == gen else "")}
152
+ res = diff_dirs(ref, gen, modules=a.module, max_items=a.max)
153
+ res.update({"reference": ref, "generated": gen})
154
+ return res
155
+ if a.cmd == "rte-api":
156
+ from .rte_api import rte_api
157
+ return rte_api(os.path.abspath(a.root), swc=a.swc, runnable=a.runnable, header=not a.no_header)
158
+ if a.cmd == "verify-rte":
159
+ from .verify_rte import verify
160
+ return verify(os.path.abspath(a.root), files=a.paths or None, diff_against=a.diff_against, swc=a.swc,
161
+ runnable=a.runnable, check_ops=a.check_ops)
162
+ if a.cmd == "build-context":
163
+ from .context import build_context
164
+ task = a.task
165
+ if task == "-":
166
+ task = sys.stdin.read()
167
+ elif task.startswith("@"):
168
+ with open(task[1:]) as fh:
169
+ task = fh.read()
170
+ return build_context(os.path.abspath(a.root), a.swc, task, runnable=a.runnable,
171
+ budget_tokens=a.budget_tokens, history=a.history)
90
172
  from .model import Project
91
- project = Project(a.root, config_files=a.config, def_files=a.defs, load_input=a.cmd in ("validate", "edit"))
173
+ 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
+ if a.cmd == "add-signal":
175
+ from .add_signal import AddSignalError, add_signal
176
+ try:
177
+ return add_signal(project, a.name, a.ipdu, bit_size=a.bit_size, signal_type=a.signal_type,
178
+ bit_position=a.bit_position, endianness=a.endianness, init=a.init,
179
+ transfer_property=a.transfer_property, dry_run=a.dry_run)
180
+ except AddSignalError as ex:
181
+ return {"error": str(ex), "written": False}
182
+ if a.cmd == "arxml-edit":
183
+ from .arxml_edit import arxml_edit
184
+ return arxml_edit(project, a.ops, dry_run=a.dry_run)
185
+ if a.cmd == "find":
186
+ from .find import find
187
+ where = a.where
188
+ if where and where.strip().startswith("{"):
189
+ where = json.loads(where)
190
+ return find(project, module=a.module, container_def=a.container_def, where=where, ref_to=a.ref_to,
191
+ referenced_by=a.referenced_by, limit=a.limit)
192
+ if a.cmd == "crosscheck":
193
+ from .find import crosscheck
194
+ return crosscheck(project, a.module)
195
+ if a.cmd == "codegen":
196
+ from .codegen import generate
197
+ res = generate(project, modules=a.module, out_dir=a.out, write_back=not a.no_write_back, binding=a.binding)
198
+ if project.warnings:
199
+ res["warnings"] = project.warnings
200
+ return res
92
201
  if a.cmd == "signal-owners":
93
202
  from .audit import signal_owners
94
203
  return {"owners": signal_owners(project, a.paths)}
@@ -115,7 +224,7 @@ def run(a) -> dict:
115
224
  return res
116
225
  if a.cmd == "validate":
117
226
  from .validate import validate
118
- res = validate(project, files=a.file, module=a.module)
227
+ res = validate(project, files=a.file, module=a.module, check_generated=a.check_generated, code=a.code)
119
228
  if a.max and len(res["diagnostics"]) > a.max:
120
229
  res["truncated"] = len(res["diagnostics"]) - a.max
121
230
  res["diagnostics"] = res["diagnostics"][: a.max]
@@ -0,0 +1,185 @@
1
+ """Deterministic ECUC -> C configuration code generation.
2
+
3
+ Same ECUC ARXML + same param defs -> byte-identical C. No LLM is involved: every line comes from
4
+ the param defs (generic emitter), a module generator (codegen/modules/, only for what the defs
5
+ cannot express), or a static-code binding (codegen/bindings/*.json, the vendor struct layout).
6
+
7
+ Entry point: generate(project, modules=None, out_dir=None, write_back=True).
8
+ Project options (.hydron/autosar.json, key "codegen"):
9
+ {"outDir": "Output", # the project's generated-code folder (used when out_dir="project")
10
+ "binding": "sip-standin", # force a static-code binding ("none" = generic tables only)
11
+ "mip": {"Eep": "Eep_21_LDExt"}, # module implementation prefixes (vendorId/vendorApiInfix)
12
+ "memmap": {"pragmas": {"CODE": {"start": "#pragma ...", "stop": "#pragma ..."}}},
13
+ "mcal": {"silicon": "tc4d", "pinMap": "mcal/pins.json"}}
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import hashlib
18
+ import json
19
+ import os
20
+ import re
21
+ import time
22
+ from typing import Optional
23
+
24
+ from .. import paths as P
25
+ from ..model import PROJECT_CONFIG, Project
26
+ from . import binding as B
27
+ from . import handles as H
28
+ from . import schm_memmap as S
29
+ from .emit import Ctx
30
+ from .model import CgModel
31
+ from .registry import generator_for, registered
32
+
33
+ SCRATCH = os.path.join(".hydron", "codegen", "out")
34
+
35
+
36
+ def project_options(root: str) -> dict:
37
+ f = os.path.join(root, PROJECT_CONFIG)
38
+ if not os.path.isfile(f):
39
+ return {}
40
+ try:
41
+ with open(f) as fh:
42
+ return json.load(fh).get("codegen", {}) or {}
43
+ except (OSError, ValueError):
44
+ return {}
45
+
46
+
47
+ def _scan_c(root: str, exclude: list) -> tuple:
48
+ """(headers basename->path, source files) of the project's static/in-house C, excluding generator output."""
49
+ from ..audit import is_generated
50
+ ex = [os.path.realpath(e) + os.sep for e in exclude if e]
51
+ w = P.walk(root, lambda n: n.endswith((".h", ".c")))
52
+ headers: dict = {}
53
+ sources: list = []
54
+ for f in w.files:
55
+ rf = os.path.realpath(f)
56
+ if any(rf.startswith(e) for e in ex):
57
+ continue
58
+ if f.endswith(".h"):
59
+ headers.setdefault(os.path.basename(f), f)
60
+ if not is_generated(f):
61
+ sources.append(f)
62
+ return headers, sources
63
+
64
+
65
+ def resolve_out(root: str, out_dir: Optional[str], opts: dict) -> str:
66
+ if not out_dir:
67
+ return os.path.join(root, SCRATCH)
68
+ if out_dir == "project":
69
+ out_dir = opts.get("outDir", "Output")
70
+ return out_dir if os.path.isabs(out_dir) else os.path.join(root, out_dir)
71
+
72
+
73
+ def generate(project: Project, modules: Optional[list] = None, out_dir: Optional[str] = None,
74
+ write_back: bool = True, binding: Optional[str] = None) -> dict:
75
+ t0 = time.time()
76
+ root = project.root
77
+ opts = project_options(root)
78
+ out = resolve_out(root, out_dir, opts)
79
+ binding = binding or opts.get("binding")
80
+ timings: dict = {}
81
+
82
+ cg = CgModel(project, opts.get("mip"))
83
+ configured = sorted(cg.modules)
84
+ if modules:
85
+ want = []
86
+ for m in modules:
87
+ hit = [x for x in configured if x.lower() == m.lower()]
88
+ if not hit:
89
+ return {"ok": False, "error": f"module {m!r} is not configured (configured: {', '.join(configured)})"}
90
+ want.append(hit[0])
91
+ else:
92
+ want = [m for m in registered() if m in cg.modules]
93
+ if not project.defs:
94
+ return {"ok": False, "error": "code generation needs the ECUC param defs (paramDefs in .hydron/autosar.json "
95
+ "or $AUTOSAR_PARAMDEFS): config classes and types come from them"}
96
+ timings["load_ms"] = int((time.time() - t0) * 1000)
97
+
98
+ # 1. handle IDs (TPS_ECUC_02107): assign, write back (or patch in memory only)
99
+ t1 = time.time()
100
+ pl = H.plan(cg, want)
101
+ if pl["errors"]:
102
+ return {"ok": False, "error": "handle-ID conflicts; fix the configuration first", "handleErrors": pl["errors"]}
103
+ wb = H.apply(project, pl["assign"], write=write_back)
104
+ if pl["assign"]:
105
+ cg = CgModel(project, opts.get("mip"))
106
+ timings["handles_ms"] = int((time.time() - t1) * 1000)
107
+
108
+ # 2. emit
109
+ t2 = time.time()
110
+ proj_out = resolve_out(root, "project", opts)
111
+ headers, sources = _scan_c(root, [out, proj_out, os.path.join(root, ".hydron")])
112
+ ctx = Ctx(cg=cg, headers=headers, bindings=B.load_all(root), binding_name=binding, release=cg.release(),
113
+ sources=sources)
114
+ ctx.options = opts
115
+ ctx.generating = set(want)
116
+ ctx.available = set(headers) | (set(os.listdir(proj_out)) if os.path.isdir(proj_out) else set())
117
+ files: dict = {}
118
+ report: dict = {}
119
+ memmaps = []
120
+ notes = list(pl["notes"])
121
+ for name in want:
122
+ mc = cg.module(name)
123
+ gen = generator_for(name)(ctx, mc)
124
+ mo = gen.run()
125
+ rendered = mo.render()
126
+ n, text = S.schm_header(ctx, mo)
127
+ rendered[n] = text
128
+ mm = f"{mo.mip}_MemMap.h"
129
+ if mm in headers:
130
+ notes.append(f"{name}: project provides {P_rel(root, headers[mm])}; {mm} not generated")
131
+ else:
132
+ n, text, sections = S.module_memmap(ctx, mo, (opts.get("memmap") or {}).get("pragmas", {}))
133
+ rendered[n] = text
134
+ memmaps.append((mo.mip, sections))
135
+ files.update(rendered)
136
+ report[name] = {
137
+ "variant": mc.variant,
138
+ "binding": (mo.binding or {}).get("_binding"),
139
+ "files": sorted(rendered),
140
+ "symbols": len(mo.symbols),
141
+ "notes": mo.notes,
142
+ }
143
+ if memmaps and "MemMap.h" not in headers:
144
+ files["MemMap.h"] = S.aggregate_memmap(ctx, memmaps)
145
+ elif memmaps:
146
+ notes.append(f"project MemMap.h ({P_rel(root, headers['MemMap.h'])}) kept; generated code includes "
147
+ "<Mip>_MemMap.h (SWS_MemMap_00032)")
148
+ timings["emit_ms"] = int((time.time() - t2) * 1000)
149
+
150
+ # 3. write
151
+ os.makedirs(out, exist_ok=True)
152
+ manifest = []
153
+ for name in sorted(files):
154
+ data = files[name].encode("utf-8")
155
+ path = os.path.join(out, name)
156
+ with open(path, "wb") as fh:
157
+ fh.write(data)
158
+ manifest.append({"file": name, "bytes": len(data), "sha256": hashlib.sha256(data).hexdigest()[:16]})
159
+ std = _std_types_release(headers)
160
+ if std and std[:2] != ctx.release[:2]:
161
+ notes.append(f"Std_Types.h in the project is AUTOSAR {std[0]}.{std[1]}.x but the configuration is "
162
+ f"{ctx.release[0]}.{ctx.release[1]}.x (schema); the release check against <Mip>.h may fail")
163
+ timings["total_ms"] = int((time.time() - t0) * 1000)
164
+ return {"ok": True, "outDir": out, "modules": report, "files": manifest,
165
+ "handles": {"assigned": pl["assign"], "writtenBack": wb["written"], "changes": wb["changes"]},
166
+ "notes": notes, "timings": timings, "release": ".".join(map(str, ctx.release))}
167
+
168
+
169
+ def P_rel(root: str, f: str) -> str:
170
+ r = os.path.relpath(f, root)
171
+ return f if r.startswith("..") else r
172
+
173
+
174
+ def _std_types_release(headers: dict) -> Optional[tuple]:
175
+ f = headers.get("Std_Types.h")
176
+ if not f:
177
+ return None
178
+ try:
179
+ with open(f, errors="replace") as fh:
180
+ t = fh.read()
181
+ except OSError:
182
+ return None
183
+ m1 = re.search(r"STD_TYPES_AR_RELEASE_MAJOR_VERSION\s+\(?(\d+)", t)
184
+ m2 = re.search(r"STD_TYPES_AR_RELEASE_MINOR_VERSION\s+\(?(\d+)", t)
185
+ return (int(m1.group(1)), int(m2.group(1))) if m1 and m2 else None
@@ -0,0 +1,210 @@
1
+ """Static-code bindings: the data contract between generated tables and a BSW implementation.
2
+
3
+ The ECUC param defs say WHAT is configured; they do not say how a given implementation's static
4
+ code lays out its configuration structs (`NvM_BlockDescriptorType`, `PduR_RoutingPathType`, ...).
5
+ That layout belongs to the BSW vendor (SIP). A binding is a JSON file that maps those structs,
6
+ field by field, onto ECUC values with a small path language, so no layout is hard-coded in Python.
7
+
8
+ Bundled bindings live in `codegen/bindings/*.json`; a project adds or overrides them in
9
+ `.hydron/codegen/bindings/*.json`. A module binding is used when its `detect` header is found in
10
+ the project (or when the binding is named explicitly).
11
+
12
+ Path language (a list of steps, evaluated left to right from the row container):
13
+ param:<P> value of parameter/reference P in the current container (terminal)
14
+ sub:<Def> first sub-container of def <Def>
15
+ ref:<R> follow ECUC reference R to its target container
16
+ back:<Def>/<R> the container of def <Def> (any module) whose reference R points here
17
+ peer:<R>[@<Module>] follow R to target T, then the container that references T from another
18
+ module (or from <Module>): how a module finds its neighbour's PDU handle
19
+ (TPS_ECUC 3.4.1.3, via EcucPduCollection/Pdu)
20
+ sym TPS_ECUC_02108 symbol of the current container (terminal)
21
+ 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.
23
+ """
24
+ from __future__ import annotations
25
+
26
+ import glob
27
+ import json
28
+ import os
29
+ import re
30
+ from typing import Optional
31
+
32
+ from ..values import parse_int
33
+ from . import cwriter as W
34
+ from .model import CgModel, Container, ModuleCfg
35
+
36
+ HERE = os.path.dirname(__file__)
37
+
38
+
39
+ class BindingError(Exception):
40
+ pass
41
+
42
+
43
+ def load_all(root: str) -> dict:
44
+ """name -> binding dict; project bindings override bundled ones of the same name."""
45
+ out: dict = {}
46
+ for f in sorted(glob.glob(os.path.join(HERE, "bindings", "*.json"))) + sorted(
47
+ glob.glob(os.path.join(root, ".hydron", "codegen", "bindings", "*.json"))):
48
+ with open(f) as fh:
49
+ b = json.load(fh)
50
+ b["_file"] = f
51
+ out[b["name"]] = b
52
+ return out
53
+
54
+
55
+ def select(bindings: dict, module: str, headers: dict, explicit: Optional[str]) -> Optional[dict]:
56
+ """The module binding to use: the explicitly named binding, else the first whose detect header matches."""
57
+ if explicit == "none":
58
+ return None
59
+ names = [explicit] if explicit else sorted(bindings)
60
+ for n in names:
61
+ b = bindings.get(n)
62
+ if b is None:
63
+ raise BindingError(f"unknown binding {n!r} (known: {', '.join(sorted(bindings)) or 'none'})")
64
+ mb = b.get("modules", {}).get(module)
65
+ if mb is None:
66
+ continue
67
+ det = mb.get("detect")
68
+ if explicit or det is None or _detect(det, headers):
69
+ return dict(mb, _binding=n)
70
+ return None
71
+
72
+
73
+ def _detect(det: dict, headers: dict) -> bool:
74
+ path = headers.get(det["header"])
75
+ if not path:
76
+ return False
77
+ try:
78
+ with open(path, errors="replace") as f:
79
+ text = f.read()
80
+ except OSError:
81
+ return False
82
+ return all(t in text for t in ([det["contains"]] if isinstance(det.get("contains"), str) else det.get("contains", [])))
83
+
84
+
85
+ class Eval:
86
+ """Evaluates binding paths against one module; records touched params (config class) and used modules."""
87
+
88
+ def __init__(self, cg: CgModel, mc: ModuleCfg, ctx=None):
89
+ self.ctx = ctx
90
+ self.cg = cg
91
+ self.mc = mc
92
+ self.rank = 1
93
+ self.used_mips: set = set()
94
+
95
+ def steps(self, start: Container, steps) -> tuple:
96
+ """-> ("container", Container) | ("value", (raw, def, owner)) | ("token", str) | (None, None)"""
97
+ if isinstance(steps, str):
98
+ steps = [steps]
99
+ cur = start
100
+ for i, st in enumerate(steps):
101
+ op, _, arg = st.partition(":")
102
+ last = i == len(steps) - 1
103
+ if op == "param":
104
+ pv = cur.value(arg)
105
+ d = self.cg.param_def(cur, arg)
106
+ self._touch(cur, d)
107
+ if pv is not None and pv.is_ref:
108
+ if last:
109
+ return ("value", (pv.value, d, cur))
110
+ cur = self.cg.target(pv)
111
+ else:
112
+ return ("value", (pv.value if pv is not None else None, d, cur)) if last else (None, None)
113
+ elif op == "sub":
114
+ subs = cur.sub(arg)
115
+ cur = subs[0] if subs else None
116
+ elif op == "ref":
117
+ self._touch(cur, self.cg.param_def(cur, arg))
118
+ cur = self.cg.target(cur.value(arg))
119
+ elif op == "back":
120
+ dname, _, rname = arg.partition("/")
121
+ hits = [c for c in self.cg.referrers(cur, ref=rname) if c.def_name == dname]
122
+ cur = hits[0] if hits else None
123
+ if cur is not None:
124
+ self._touch(cur, self.cg.param_def(cur, rname))
125
+ elif op == "peer":
126
+ rname, _, want = arg.partition("@")
127
+ self._touch(cur, self.cg.param_def(cur, rname))
128
+ t = self.cg.target(cur.value(rname))
129
+ if t is None:
130
+ return (None, None)
131
+ 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)]
133
+ hits.sort(key=lambda c: (c.module.name, self.cg.by_path_index(c)))
134
+ cur = hits[0] if hits else None
135
+ elif op == "sym":
136
+ s = self.cg.symbol(cur)
137
+ if s and self.ctx is not None:
138
+ s = self.ctx.xsym(cur, self.mc, self.used_mips)
139
+ return ("token", s) if s else (None, None)
140
+ elif op == "module":
141
+ return ("token", cur.module.mip)
142
+ elif op == "name":
143
+ return ("token", cur.name)
144
+ elif op == "index":
145
+ return ("token", W.c_uint(self.cg.index_of(cur)))
146
+ else:
147
+ raise BindingError(f"unknown path step {st!r}")
148
+ if cur is None:
149
+ return (None, None)
150
+ return ("container", cur)
151
+
152
+ def _touch(self, c: Container, d):
153
+ if c.module is self.mc:
154
+ self.rank = max(self.rank, self.cg.eff_class(self.mc, d))
155
+
156
+ def field(self, row: Container, spec: dict) -> tuple:
157
+ """(C initialiser, comment) for one struct field."""
158
+ when = spec.get("when")
159
+ if when is not None:
160
+ kind, v = self.steps(row, when)
161
+ ok = kind == "value" and (v[0] or "").strip().lower() in ("true", "1")
162
+ if not ok:
163
+ return (spec.get("default", "0u"), "")
164
+ paths = spec["path"]
165
+ alts = paths if isinstance(paths, list) and paths and isinstance(paths[0], list) else [paths]
166
+ kind, v = None, None
167
+ for alt in alts: # alternatives: the first path that yields a value wins
168
+ kind, v = self.steps(row, alt)
169
+ if kind is not None and not (kind == "value" and v[0] is None):
170
+ break
171
+ default = spec.get("default")
172
+ fmt = spec.get("fmt")
173
+ if kind is None or (kind == "value" and v[0] is None):
174
+ if default is None:
175
+ raise BindingError(f"{row.path}: field {spec.get('name')} ({spec['path']}) has no value and no default")
176
+ return (default, "")
177
+ if kind == "container":
178
+ s = self.cg.symbol(v)
179
+ if s is None:
180
+ return (W.c_uint(self.cg.index_of(v)), v.name)
181
+ if self.ctx is not None:
182
+ s = self.ctx.xsym(v, self.mc, self.used_mips)
183
+ return (s, "")
184
+ if kind == "token":
185
+ if fmt:
186
+ return (fmt.replace("{MIP}", v.upper()).replace("{Mip}", v), "")
187
+ return (v, "")
188
+ raw, d, owner = v
189
+ if "enum" in spec and raw is not None:
190
+ m = spec["enum"]
191
+ if raw.strip() not in m:
192
+ raise BindingError(f"{row.path}: {spec['path']} literal {raw} not in the binding's enum map")
193
+ return (str(m[raw.strip()]), raw.strip())
194
+ return W.table_value(d, raw, hexa=fmt == "hex")
195
+
196
+ def macro(self, expr: str) -> str:
197
+ m = re.match(r"^(count|max):([\w]+)(?:/(\w+))?\s*([+-]\s*\d+)?$", expr.strip())
198
+ if not m:
199
+ raise BindingError(f"bad macro expression {expr!r} (count:<Def> | max:<Def>/<Param> [+N])")
200
+ op, dname, pname, off = m.groups()
201
+ insts = self.cg.instances(self.mc, dname)
202
+ if op == "count":
203
+ v = len(insts)
204
+ else:
205
+ vals = [parse_int(c.text(pname) or "") for c in insts]
206
+ vals = [x for x in vals if x is not None]
207
+ v = max(vals) if vals else 0
208
+ if off:
209
+ v += int(off.replace(" ", ""))
210
+ return W.c_uint(v)
@@ -0,0 +1,111 @@
1
+ {
2
+ "name": "sip-standin",
3
+ "description": "Struct layout of the fixture's SIP stand-in static code (fixtures/*/SIP/include). Every type, field and macro below is taken from those headers; the values come from the ECUC via the path language (see codegen/binding.py). Enumeration fields without an 'enum' map use the def literal order.",
4
+ "modules": {
5
+ "NvM": {
6
+ "detect": {"header": "NvM.h", "contains": "NvM_BlockDescriptorType"},
7
+ "sourceIncludes": ["NvM.h"],
8
+ "macros": [
9
+ {"name": "NVM_TOTAL_NUM_OF_NVRAM_BLOCKS", "value": "max:NvMBlockDescriptor/NvMNvramBlockIdentifier+1"}
10
+ ],
11
+ "tables": [
12
+ {
13
+ "name": "NvM_BlockDescriptorTable", "type": "NvM_BlockDescriptorType", "qualifier": "NVM_CONST",
14
+ "rows": "NvMBlockDescriptor", "size": "NVM_TOTAL_NUM_OF_NVRAM_BLOCKS", "designated": true,
15
+ "doc": "indexed by NvMNvramBlockIdentifier (0 and 1 are reserved, SWS_NvM_00478)",
16
+ "fields": [
17
+ {"name": "RamBlockDataAddress", "path": "param:NvMRamBlockDataAddress", "default": "NULL_PTR",
18
+ "extern": "VAR(uint8, NVM_APPL_DATA) {}[]"},
19
+ {"name": "NvBlockLength", "path": "param:NvMNvBlockLength"},
20
+ {"name": "FeeBlockNumber", "path": [["sub:NvMTargetBlockReference", "sub:NvMFeeRef", "ref:NvMNameOfFeeBlock", "param:FeeBlockNumber"],
21
+ ["sub:NvMTargetBlockReference", "sub:NvMEaRef", "ref:NvMNameOfEaBlock", "param:EaBlockNumber"]]},
22
+ {"name": "ManagementType", "path": "param:NvMBlockManagementType"},
23
+ {"name": "UseCrc", "path": "param:NvMBlockUseCrc"},
24
+ {"name": "SingleBlockCallback", "path": ["sub:NvMSingleBlockCallback", "param:NvMSingleBlockCallbackFnc"],
25
+ "default": "NULL_PTR",
26
+ "extern": "FUNC(Std_ReturnType, NVM_APPL_CODE) {}(NvM_BlockRequestType BlockRequest, NvM_RequestResultType JobResult)"}
27
+ ]
28
+ }
29
+ ]
30
+ },
31
+ "Dem": {
32
+ "detect": {"header": "Dem.h", "contains": "Dem_EventIdType"},
33
+ "genericTables": true,
34
+ "macros": [
35
+ {"name": "DEM_NUMBER_OF_EVENTS", "value": "max:DemEventParameter/DemEventId"}
36
+ ]
37
+ },
38
+ "PduR": {
39
+ "detect": {"header": "PduR.h", "contains": "PduR_RoutingPathType"},
40
+ "sourceIncludes": ["PduR.h"],
41
+ "macros": [
42
+ {"name": "PDUR_NUMBER_OF_SRC_PDUS", "value": "count:PduRSrcPdu"},
43
+ {"name": "PDUR_NUMBER_OF_DEST_PDUS", "value": "count:PduRDestPdu"}
44
+ ],
45
+ "tables": [
46
+ {
47
+ "name": "PduR_RoutingPaths", "type": "PduR_RoutingPathType", "qualifier": "PDUR_CONST", "static": true,
48
+ "rows": "PduRSrcPdu", "size": "PDUR_NUMBER_OF_SRC_PDUS",
49
+ "doc": "indexed by PduRSourcePduHandleId", "comment": "name",
50
+ "fields": [
51
+ {"name": "DestModule", "path": ["back:PduRRoutingPath/PduRSrcPduRRef", "ref:PduRDestPduRRef", "peer:PduRDestPduRef", "module"],
52
+ "fmt": "PDUR_DEST_{MIP}"},
53
+ {"name": "DestLowerPduId", "path": ["back:PduRRoutingPath/PduRSrcPduRRef", "ref:PduRDestPduRRef", "peer:PduRDestPduRef", "sym"]},
54
+ {"name": "DestPduRPduId", "path": ["back:PduRRoutingPath/PduRSrcPduRRef", "ref:PduRDestPduRRef", "sym"]}
55
+ ]
56
+ },
57
+ {
58
+ "name": "PduR_TxConfUpperPduId", "type": "PduIdType", "qualifier": "PDUR_CONST", "static": true, "scalar": true,
59
+ "rows": "PduRDestPdu", "size": "PDUR_NUMBER_OF_DEST_PDUS",
60
+ "doc": "indexed by PduRDestPduHandleId: the upper (Com) I-PDU to confirm when the source PDU asks for it (PduRSrcPduUpTxConf)",
61
+ "fields": [
62
+ {"name": "UpperPduId", "path": ["back:PduRRoutingPath/PduRDestPduRRef", "ref:PduRSrcPduRRef", "peer:PduRSrcPduRef@Com", "sym"],
63
+ "when": ["back:PduRRoutingPath/PduRDestPduRRef", "ref:PduRSrcPduRRef", "param:PduRSrcPduUpTxConf"],
64
+ "default": "COMSTACK_INVALID_PDUID"}
65
+ ]
66
+ }
67
+ ],
68
+ "objects": [
69
+ {"name": "PduR_Config", "type": "PduR_PBConfigType", "qualifier": "PDUR_CONST",
70
+ "init": ["PduR_RoutingPaths", "PduR_TxConfUpperPduId", "PDUR_NUMBER_OF_SRC_PDUS", "PDUR_NUMBER_OF_DEST_PDUS"]}
71
+ ]
72
+ },
73
+ "CanIf": {
74
+ "detect": {"header": "CanIf.h", "contains": "CanIf_TxPduConfigType"},
75
+ "sourceIncludes": ["CanIf.h"],
76
+ "macros": [
77
+ {"name": "CANIF_NUMBER_OF_TX_PDUS", "value": "count:CanIfTxPduCfg"},
78
+ {"name": "CANIF_NUMBER_OF_RX_PDUS", "value": "count:CanIfRxPduCfg"}
79
+ ],
80
+ "tables": [
81
+ {
82
+ "name": "CanIf_TxPduConfig", "type": "CanIf_TxPduConfigType", "qualifier": "CANIF_CONST", "static": true,
83
+ "rows": "CanIfTxPduCfg", "size": "CANIF_NUMBER_OF_TX_PDUS", "comment": "sym",
84
+ "doc": "indexed by CanIfTxPduId",
85
+ "fields": [
86
+ {"name": "CanId", "path": "param:CanIfTxPduCanId", "fmt": "hex"},
87
+ {"name": "Dlc", "path": ["ref:CanIfTxPduRef", "param:PduLength"]},
88
+ {"name": "Hth", "path": ["ref:CanIfTxPduBufferRef", "ref:CanIfBufferHthRef", "ref:CanIfHthIdSymRef", "sym"]},
89
+ {"name": "UpperPduId", "path": ["peer:CanIfTxPduRef@PduR", "sym"]}
90
+ ]
91
+ },
92
+ {
93
+ "name": "CanIf_RxPduConfig", "type": "CanIf_RxPduConfigType", "qualifier": "CANIF_CONST", "static": true,
94
+ "rows": "CanIfRxPduCfg", "size": "CANIF_NUMBER_OF_RX_PDUS", "comment": "sym",
95
+ "doc": "indexed by CanIfRxPduId",
96
+ "fields": [
97
+ {"name": "CanId", "path": "param:CanIfRxPduCanId", "fmt": "hex"},
98
+ {"name": "CanIdMask", "path": "param:CanIfRxPduCanIdMask", "fmt": "hex"},
99
+ {"name": "Dlc", "path": "param:CanIfRxPduDataLength"},
100
+ {"name": "Hrh", "path": ["ref:CanIfRxPduHrhIdRef", "ref:CanIfHrhIdSymRef", "sym"]},
101
+ {"name": "UpperPduId", "path": ["peer:CanIfRxPduRef@PduR", "sym"]}
102
+ ]
103
+ }
104
+ ],
105
+ "objects": [
106
+ {"name": "CanIf_Config", "type": "CanIf_ConfigType", "qualifier": "CANIF_CONST",
107
+ "init": ["CanIf_TxPduConfig", "CanIf_RxPduConfig", "CANIF_NUMBER_OF_TX_PDUS", "CANIF_NUMBER_OF_RX_PDUS"]}
108
+ ]
109
+ }
110
+ }
111
+ }