wireforge 0.1.0__py3-none-any.whl
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.
- wireforge/__init__.py +3 -0
- wireforge/_data/cmake/WireForgeConfig.cmake +29 -0
- wireforge/_data/cmake/WireForgeConfigVersion.cmake +15 -0
- wireforge/_data/cmake/WireForgeGenerate.cmake +64 -0
- wireforge/_data/runtime/LICENSE +21 -0
- wireforge/_data/runtime/include/wireforge/array_access.hpp +35 -0
- wireforge/_data/runtime/include/wireforge/bit_utils.hpp +226 -0
- wireforge/_data/runtime/include/wireforge/byte_io.hpp +387 -0
- wireforge/_data/runtime/include/wireforge/checksum.hpp +201 -0
- wireforge/_data/runtime/include/wireforge/codec_support.hpp +412 -0
- wireforge/_data/runtime/include/wireforge/config.hpp +41 -0
- wireforge/_data/runtime/include/wireforge/delegate.hpp +101 -0
- wireforge/_data/runtime/include/wireforge/dispatcher.hpp +339 -0
- wireforge/_data/runtime/include/wireforge/expr_support.hpp +38 -0
- wireforge/_data/runtime/include/wireforge/frame_decoder.hpp +262 -0
- wireforge/_data/runtime/include/wireforge/frame_layout.hpp +20 -0
- wireforge/_data/runtime/include/wireforge/link.hpp +223 -0
- wireforge/_data/runtime/include/wireforge/lock.hpp +146 -0
- wireforge/_data/runtime/include/wireforge/ring_buffer.hpp +226 -0
- wireforge/_data/runtime/include/wireforge/span.hpp +116 -0
- wireforge/_data/runtime/include/wireforge/static_vector.hpp +264 -0
- wireforge/_data/runtime/include/wireforge/status.hpp +75 -0
- wireforge/_data/runtime/include/wireforge/transport.hpp +95 -0
- wireforge/_data/runtime/include/wireforge/validation.hpp +153 -0
- wireforge/_data/runtime/include/wireforge/version.hpp +22 -0
- wireforge/_data/runtime/requirements.yaml +438 -0
- wireforge/_data/schema/deviations.schema.json +47 -0
- wireforge/_data/schema/fragment.schema.json +79 -0
- wireforge/_data/schema/llr_ids.schema.json +25 -0
- wireforge/_data/schema/project.schema.json +1240 -0
- wireforge/_data/schema/runtime_requirements.schema.json +34 -0
- wireforge/_data/ui/assets/index-6Pomu6U4.js +44 -0
- wireforge/_data/ui/assets/index-GVdDyUAG.css +1 -0
- wireforge/_data/ui/index.html +13 -0
- wireforge/bundle.py +177 -0
- wireforge/ci_check.py +155 -0
- wireforge/cli.py +617 -0
- wireforge/codec.py +897 -0
- wireforge/compat.py +536 -0
- wireforge/coverage_points.py +64 -0
- wireforge/cpp.py +273 -0
- wireforge/cpp_codec.py +440 -0
- wireforge/cpp_interface.py +874 -0
- wireforge/cpp_tests.py +1993 -0
- wireforge/differential.py +586 -0
- wireforge/docs_markdown.py +72 -0
- wireforge/expr.py +598 -0
- wireforge/findings.py +187 -0
- wireforge/fmt.py +351 -0
- wireforge/generate.py +644 -0
- wireforge/golden.py +253 -0
- wireforge/hlr_import.py +112 -0
- wireforge/impact.py +249 -0
- wireforge/importers.py +402 -0
- wireforge/issues.py +27 -0
- wireforge/layout.py +681 -0
- wireforge/lifecycle.py +1266 -0
- wireforge/lifecycle_results.py +205 -0
- wireforge/lifecycle_rules.py +78 -0
- wireforge/lifecycle_runtime.py +375 -0
- wireforge/llr.py +693 -0
- wireforge/llr_ids.py +104 -0
- wireforge/loader.py +422 -0
- wireforge/mcp_server.py +357 -0
- wireforge/model.py +322 -0
- wireforge/plain.py +15 -0
- wireforge/py.typed +0 -0
- wireforge/reqif.py +289 -0
- wireforge/resources.py +43 -0
- wireforge/results.py +377 -0
- wireforge/runtime_reqs.py +147 -0
- wireforge/schema.py +151 -0
- wireforge/server/__init__.py +1 -0
- wireforge/server/app.py +619 -0
- wireforge/server/oidc.py +99 -0
- wireforge/server/storage.py +146 -0
- wireforge/service.py +1354 -0
- wireforge/templates/command/project.yaml +38 -0
- wireforge/templates/raw/project.yaml +28 -0
- wireforge/templates/telemetry/project.yaml +51 -0
- wireforge/test_cases.py +475 -0
- wireforge/test_project.py +478 -0
- wireforge/trace.py +442 -0
- wireforge/validate.py +1241 -0
- wireforge/vcs.py +44 -0
- wireforge/vectors.py +222 -0
- wireforge/versioning.py +137 -0
- wireforge/wireshark.py +792 -0
- wireforge/wireshark_runtime.lua +725 -0
- wireforge-0.1.0.dist-info/METADATA +91 -0
- wireforge-0.1.0.dist-info/RECORD +96 -0
- wireforge-0.1.0.dist-info/WHEEL +5 -0
- wireforge-0.1.0.dist-info/entry_points.txt +2 -0
- wireforge-0.1.0.dist-info/licenses/LICENSE +98 -0
- wireforge-0.1.0.dist-info/licenses/runtime/LICENSE +21 -0
- wireforge-0.1.0.dist-info/top_level.txt +1 -0
wireforge/validate.py
ADDED
|
@@ -0,0 +1,1241 @@
|
|
|
1
|
+
"""Semantic validation of a loaded project.
|
|
2
|
+
|
|
3
|
+
Every issue is tagged with its rule id (`[SR-n]`). Duplicates across files (SR-1) are reported by
|
|
4
|
+
the loader and version stamps (SR-19) by the structural stage. Expressions are evaluated once per
|
|
5
|
+
variant: each profile, or the plain defines when the project has no profiles. `reqs` lists are
|
|
6
|
+
checked against the `requirements` catalog (SR-21).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import dataclasses
|
|
12
|
+
import re
|
|
13
|
+
import struct
|
|
14
|
+
import sys
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import TYPE_CHECKING
|
|
18
|
+
|
|
19
|
+
from wireforge import expr
|
|
20
|
+
from wireforge.cpp import wire_only
|
|
21
|
+
from wireforge.expr import ExprError, Scope
|
|
22
|
+
from wireforge.loader import load_project
|
|
23
|
+
from wireforge.model import (
|
|
24
|
+
BUILTIN_BITS,
|
|
25
|
+
FLOAT_TYPES,
|
|
26
|
+
INTEGER_TYPES,
|
|
27
|
+
SIGNED_TYPES,
|
|
28
|
+
UNSIGNED_TYPES,
|
|
29
|
+
Checksum,
|
|
30
|
+
Field,
|
|
31
|
+
Interface,
|
|
32
|
+
Project,
|
|
33
|
+
Scalar,
|
|
34
|
+
Source,
|
|
35
|
+
Struct,
|
|
36
|
+
Value,
|
|
37
|
+
Vector,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
if TYPE_CHECKING:
|
|
41
|
+
from collections.abc import Iterable
|
|
42
|
+
|
|
43
|
+
from wireforge.issues import Issue
|
|
44
|
+
|
|
45
|
+
BYTE_BITS = 8
|
|
46
|
+
MAX_GROUP_BITS = 64
|
|
47
|
+
MAX_CHECKSUM_BYTES = 8
|
|
48
|
+
FLOAT32_MAX = struct.unpack(">f", bytes.fromhex("7f7fffff"))[0]
|
|
49
|
+
CHECK_INPUT = b"123456789"
|
|
50
|
+
ROLE_TYPES_UNSIGNED = frozenset({"message_id", "payload_length", "sequence", "protocol_version"})
|
|
51
|
+
SINGLE_ROLES = ("message_id", "payload_length")
|
|
52
|
+
|
|
53
|
+
CPP_KEYWORDS = frozenset(
|
|
54
|
+
{
|
|
55
|
+
"alignas",
|
|
56
|
+
"alignof",
|
|
57
|
+
"and",
|
|
58
|
+
"and_eq",
|
|
59
|
+
"asm",
|
|
60
|
+
"auto",
|
|
61
|
+
"bitand",
|
|
62
|
+
"bitor",
|
|
63
|
+
"bool",
|
|
64
|
+
"break",
|
|
65
|
+
"case",
|
|
66
|
+
"catch",
|
|
67
|
+
"char",
|
|
68
|
+
"char8_t",
|
|
69
|
+
"char16_t",
|
|
70
|
+
"char32_t",
|
|
71
|
+
"class",
|
|
72
|
+
"compl",
|
|
73
|
+
"concept",
|
|
74
|
+
"const",
|
|
75
|
+
"consteval",
|
|
76
|
+
"constexpr",
|
|
77
|
+
"constinit",
|
|
78
|
+
"const_cast",
|
|
79
|
+
"continue",
|
|
80
|
+
"co_await",
|
|
81
|
+
"co_return",
|
|
82
|
+
"co_yield",
|
|
83
|
+
"decltype",
|
|
84
|
+
"default",
|
|
85
|
+
"delete",
|
|
86
|
+
"do",
|
|
87
|
+
"double",
|
|
88
|
+
"dynamic_cast",
|
|
89
|
+
"else",
|
|
90
|
+
"enum",
|
|
91
|
+
"explicit",
|
|
92
|
+
"export",
|
|
93
|
+
"extern",
|
|
94
|
+
"false",
|
|
95
|
+
"float",
|
|
96
|
+
"for",
|
|
97
|
+
"friend",
|
|
98
|
+
"goto",
|
|
99
|
+
"if",
|
|
100
|
+
"inline",
|
|
101
|
+
"int",
|
|
102
|
+
"long",
|
|
103
|
+
"mutable",
|
|
104
|
+
"namespace",
|
|
105
|
+
"new",
|
|
106
|
+
"noexcept",
|
|
107
|
+
"not",
|
|
108
|
+
"not_eq",
|
|
109
|
+
"nullptr",
|
|
110
|
+
"operator",
|
|
111
|
+
"or",
|
|
112
|
+
"or_eq",
|
|
113
|
+
"private",
|
|
114
|
+
"protected",
|
|
115
|
+
"public",
|
|
116
|
+
"register",
|
|
117
|
+
"reinterpret_cast",
|
|
118
|
+
"requires",
|
|
119
|
+
"return",
|
|
120
|
+
"short",
|
|
121
|
+
"signed",
|
|
122
|
+
"sizeof",
|
|
123
|
+
"static",
|
|
124
|
+
"static_assert",
|
|
125
|
+
"static_cast",
|
|
126
|
+
"struct",
|
|
127
|
+
"switch",
|
|
128
|
+
"template",
|
|
129
|
+
"this",
|
|
130
|
+
"thread_local",
|
|
131
|
+
"throw",
|
|
132
|
+
"true",
|
|
133
|
+
"try",
|
|
134
|
+
"typedef",
|
|
135
|
+
"typeid",
|
|
136
|
+
"typename",
|
|
137
|
+
"union",
|
|
138
|
+
"unsigned",
|
|
139
|
+
"using",
|
|
140
|
+
"virtual",
|
|
141
|
+
"void",
|
|
142
|
+
"volatile",
|
|
143
|
+
"wchar_t",
|
|
144
|
+
"while",
|
|
145
|
+
"xor",
|
|
146
|
+
"xor_eq",
|
|
147
|
+
}
|
|
148
|
+
)
|
|
149
|
+
_RESERVED_IDENTIFIER = re.compile(r"^_[A-Z]|__")
|
|
150
|
+
|
|
151
|
+
# Kinds of value expressions and what they may reference.
|
|
152
|
+
COUNT = "count" # occurs.count: earlier fields allowed, may be dynamic
|
|
153
|
+
LAYOUT = "layout" # capacities and frame sizes: static, no opaque constants
|
|
154
|
+
RANGE = "range" # field min/max: static, opaque constants allowed (not evaluated)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@dataclass(frozen=True)
|
|
158
|
+
class Variant:
|
|
159
|
+
"""One configuration to evaluate: a profile, or the plain defines (name "")."""
|
|
160
|
+
|
|
161
|
+
name: str
|
|
162
|
+
scope: Scope
|
|
163
|
+
opaque: frozenset[str]
|
|
164
|
+
|
|
165
|
+
def label(self) -> str:
|
|
166
|
+
"""Suffix for messages: where the problem occurs."""
|
|
167
|
+
return f" in profile {self.name!r}" if self.name else ""
|
|
168
|
+
|
|
169
|
+
@property
|
|
170
|
+
def rule(self) -> str:
|
|
171
|
+
"""Evaluation failures are SR-16 inside profiles, SR-5 otherwise."""
|
|
172
|
+
return "SR-16" if self.name else "SR-5"
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@dataclass(frozen=True)
|
|
176
|
+
class _At:
|
|
177
|
+
"""Location of a reported problem: an element plus an optional sub-pointer."""
|
|
178
|
+
|
|
179
|
+
source: Source
|
|
180
|
+
suffix: str = ""
|
|
181
|
+
|
|
182
|
+
def below(self, suffix: str) -> _At:
|
|
183
|
+
return _At(self.source, self.suffix + suffix)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@dataclass(frozen=True)
|
|
187
|
+
class _Context:
|
|
188
|
+
"""The struct (type, message, header or trailer) whose fields are being checked."""
|
|
189
|
+
|
|
190
|
+
owner: str
|
|
191
|
+
fields: tuple[Field, ...]
|
|
192
|
+
is_message: bool = False
|
|
193
|
+
frame_part: bool = False
|
|
194
|
+
|
|
195
|
+
def index_of(self, name: str) -> int | None:
|
|
196
|
+
for index, item in enumerate(self.fields):
|
|
197
|
+
if item.name == name:
|
|
198
|
+
return index
|
|
199
|
+
return None
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _crc(checksum: Checksum, data: bytes) -> int:
|
|
203
|
+
"""Rocksoft-model CRC of `data` with the parameters of `checksum`."""
|
|
204
|
+
width = checksum.width or 0
|
|
205
|
+
top = 1 << (width - 1)
|
|
206
|
+
mask = (1 << width) - 1
|
|
207
|
+
register = checksum.init
|
|
208
|
+
for byte in data:
|
|
209
|
+
value = int(f"{byte:08b}"[::-1], 2) if checksum.refin else byte
|
|
210
|
+
for bit in range(7, -1, -1):
|
|
211
|
+
feedback = bool(register & top) ^ bool((value >> bit) & 1)
|
|
212
|
+
register = ((register << 1) & mask) ^ ((checksum.poly or 0) if feedback else 0)
|
|
213
|
+
if checksum.refout:
|
|
214
|
+
register = int(f"{register:0{width}b}"[::-1], 2)
|
|
215
|
+
return register ^ checksum.xorout
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _checksum_bits(checksum: Checksum) -> int:
|
|
219
|
+
if checksum.algo == "crc32":
|
|
220
|
+
return 32
|
|
221
|
+
if checksum.algo == "crc":
|
|
222
|
+
return checksum.width or 0
|
|
223
|
+
return (checksum.size_bytes or 0) * BYTE_BITS
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _int_bounds(type_name: str, bits: int) -> tuple[int, int]:
|
|
227
|
+
if type_name in SIGNED_TYPES:
|
|
228
|
+
return -(1 << (bits - 1)), (1 << (bits - 1)) - 1
|
|
229
|
+
return 0, (1 << bits) - 1
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _is_relative_path(path: str) -> bool:
|
|
233
|
+
parts = re.split(r"[\\/]", path)
|
|
234
|
+
absolute = path.startswith(("/", "\\")) or re.match(r"^[A-Za-z]:", path) is not None
|
|
235
|
+
return not absolute and ".." not in parts
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
class Validator:
|
|
239
|
+
"""Checks one project; `run()` returns every semantic issue."""
|
|
240
|
+
|
|
241
|
+
def __init__(self, project: Project) -> None:
|
|
242
|
+
"""Prepare variants and lookup tables."""
|
|
243
|
+
self.project = project
|
|
244
|
+
self.issues: list[Issue] = []
|
|
245
|
+
self.members = {
|
|
246
|
+
(enum.name, member): value
|
|
247
|
+
for enum in project.enums.values()
|
|
248
|
+
for member, value in enum.values.items()
|
|
249
|
+
}
|
|
250
|
+
self.variants = self._variants()
|
|
251
|
+
self.cyclic: set[str] = set()
|
|
252
|
+
self._current: _Context | None = None # struct whose fields are being checked
|
|
253
|
+
self._bits_cache: dict[tuple[str, str, bool], int | None] = {}
|
|
254
|
+
|
|
255
|
+
# --- infrastructure -------------------------------------------------------------------------
|
|
256
|
+
|
|
257
|
+
def report(self, at: Source | _At, rule: str, message: str, suffix: str = "") -> None:
|
|
258
|
+
"""Record one error at an element (plus optional sub-pointer)."""
|
|
259
|
+
where = at if isinstance(at, _At) else _At(at)
|
|
260
|
+
self.issues.append(where.source.issue(rule, message, where.suffix + suffix))
|
|
261
|
+
|
|
262
|
+
def _variants(self) -> list[Variant]:
|
|
263
|
+
defines = {name: define.value for name, define in self.project.defines.items()}
|
|
264
|
+
if not self.project.profiles:
|
|
265
|
+
return [self._variant("", defines, None)]
|
|
266
|
+
return [
|
|
267
|
+
self._variant(name, {**defines, **profile.defines}, name)
|
|
268
|
+
for name, profile in self.project.profiles.items()
|
|
269
|
+
]
|
|
270
|
+
|
|
271
|
+
def _variant(self, name: str, defines: dict[str, int], profile: str | None) -> Variant:
|
|
272
|
+
values = dict(defines)
|
|
273
|
+
opaque = set()
|
|
274
|
+
for constant in self.project.constants.values():
|
|
275
|
+
value = constant.profile_values.get(profile or "", constant.value)
|
|
276
|
+
if value is None:
|
|
277
|
+
opaque.add(constant.name)
|
|
278
|
+
else:
|
|
279
|
+
values[constant.name] = value
|
|
280
|
+
scope = Scope(values=values, members=self.members, defined=frozenset(values))
|
|
281
|
+
return Variant(name, scope, frozenset(opaque))
|
|
282
|
+
|
|
283
|
+
def run(self) -> list[Issue]:
|
|
284
|
+
"""Check every rule and return the issues found."""
|
|
285
|
+
self._names()
|
|
286
|
+
self._enums()
|
|
287
|
+
self._resolve_types()
|
|
288
|
+
self._constants()
|
|
289
|
+
self._checksums()
|
|
290
|
+
self._user_types()
|
|
291
|
+
for item in self.project.types.values():
|
|
292
|
+
self._struct(_Context(f"type {item.name}", item.fields), item)
|
|
293
|
+
for item in self.project.messages.values():
|
|
294
|
+
self._struct(_Context(f"message {item.name}", item.fields, is_message=True), item)
|
|
295
|
+
for interface in self.project.interfaces.values():
|
|
296
|
+
self._interface(interface)
|
|
297
|
+
self._user_includes()
|
|
298
|
+
self._requirement_refs()
|
|
299
|
+
self._vectors()
|
|
300
|
+
return self.issues
|
|
301
|
+
|
|
302
|
+
# --- requirement references -------------------------------------------------------------------
|
|
303
|
+
|
|
304
|
+
def _requirement_refs(self) -> None:
|
|
305
|
+
"""SR-21: every `reqs` entry names a requirement of the catalog."""
|
|
306
|
+
project = self.project
|
|
307
|
+
for owner, source, reqs, suffix in self._requirement_users():
|
|
308
|
+
for index, rid in enumerate(reqs):
|
|
309
|
+
if rid not in project.requirements:
|
|
310
|
+
self.report(
|
|
311
|
+
source,
|
|
312
|
+
"SR-21",
|
|
313
|
+
f"{owner}: unknown requirement {rid!r}",
|
|
314
|
+
f"{suffix}/reqs/{index}",
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
def _requirement_users(self) -> Iterable[tuple[str, Source, tuple[str, ...], str]]:
|
|
318
|
+
"""Every element that can carry `reqs`: (description, source, ids, pointer suffix)."""
|
|
319
|
+
project = self.project
|
|
320
|
+
for enum in project.enums.values():
|
|
321
|
+
yield f"enum {enum.name}", enum.source, enum.reqs, ""
|
|
322
|
+
for checksum in project.checksums.values():
|
|
323
|
+
yield f"checksum {checksum.name}", checksum.source, checksum.reqs, ""
|
|
324
|
+
for user_type in project.user_types.values():
|
|
325
|
+
yield f"user type {user_type.name}", user_type.source, user_type.reqs, ""
|
|
326
|
+
for kind, structs in (("type", project.types), ("message", project.messages)):
|
|
327
|
+
for struct_ in structs.values():
|
|
328
|
+
yield f"{kind} {struct_.name}", struct_.source, struct_.reqs, ""
|
|
329
|
+
for owner, fields in self._all_field_lists():
|
|
330
|
+
for item in fields:
|
|
331
|
+
yield f"{owner}: field {item.name}", item.source, item.reqs, ""
|
|
332
|
+
for vector in project.vectors.values():
|
|
333
|
+
yield f"vector {vector.name}", vector.source, vector.reqs, ""
|
|
334
|
+
yield from self._interface_requirement_users()
|
|
335
|
+
|
|
336
|
+
def _interface_requirement_users(self) -> Iterable[tuple[str, Source, tuple[str, ...], str]]:
|
|
337
|
+
for interface in self.project.interfaces.values():
|
|
338
|
+
yield f"interface {interface.name}", interface.source, interface.reqs, ""
|
|
339
|
+
yield (
|
|
340
|
+
f"interface {interface.name}",
|
|
341
|
+
interface.source,
|
|
342
|
+
interface.framing.reqs,
|
|
343
|
+
"/framing",
|
|
344
|
+
)
|
|
345
|
+
for entry in interface.messages:
|
|
346
|
+
yield (
|
|
347
|
+
f"interface {interface.name}: message {entry.name}",
|
|
348
|
+
entry.source,
|
|
349
|
+
entry.reqs,
|
|
350
|
+
"",
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
# --- requirement test vectors -----------------------------------------------------------------
|
|
354
|
+
|
|
355
|
+
def _vectors(self) -> None:
|
|
356
|
+
"""SR-22: what a vector says about itself is consistent (the codec check is `vectors`)."""
|
|
357
|
+
for vector in self.project.vectors.values():
|
|
358
|
+
for problem, suffix in self._vector_problems(vector):
|
|
359
|
+
self.report(vector.source, "SR-22", f"vector {vector.name}: {problem}", suffix)
|
|
360
|
+
|
|
361
|
+
def _vector_problems(self, vector: Vector) -> Iterable[tuple[str, str]]:
|
|
362
|
+
project = self.project
|
|
363
|
+
if vector.data is None:
|
|
364
|
+
yield "bytes is not hexadecimal", "/bytes"
|
|
365
|
+
message = project.messages.get(vector.message)
|
|
366
|
+
if message is None:
|
|
367
|
+
yield f"unknown message {vector.message!r}", "/message"
|
|
368
|
+
if vector.profile is not None and vector.profile not in project.profiles:
|
|
369
|
+
yield f"unknown profile {vector.profile!r}", "/profile"
|
|
370
|
+
if vector.interface is not None:
|
|
371
|
+
interface = project.interfaces.get(vector.interface)
|
|
372
|
+
if interface is None:
|
|
373
|
+
yield f"unknown interface {vector.interface!r}", "/interface"
|
|
374
|
+
elif message is not None and vector.message not in {m.name for m in interface.messages}:
|
|
375
|
+
yield f"message {vector.message} is not on interface {vector.interface}", "/message"
|
|
376
|
+
if vector.direction != "decode" and vector.fields is None:
|
|
377
|
+
yield f"direction {vector.direction} needs expect.fields", "/direction"
|
|
378
|
+
if message is not None and vector.fields is not None:
|
|
379
|
+
yield from self._vector_fields(vector, message)
|
|
380
|
+
|
|
381
|
+
def _vector_fields(self, vector: Vector, message: Struct) -> Iterable[tuple[str, str]]:
|
|
382
|
+
fields = vector.fields or {}
|
|
383
|
+
wire_only_names = wire_only(message)
|
|
384
|
+
names = {item.name for item in message.fields}
|
|
385
|
+
for name in fields:
|
|
386
|
+
if name not in names:
|
|
387
|
+
yield f"expect.fields names unknown field {name!r}", f"/expect/fields/{name}"
|
|
388
|
+
elif name in wire_only_names:
|
|
389
|
+
yield f"field {name!r} exists only on the wire", f"/expect/fields/{name}"
|
|
390
|
+
if vector.direction != "decode":
|
|
391
|
+
missing = [
|
|
392
|
+
item.name
|
|
393
|
+
for item in message.fields
|
|
394
|
+
if item.name not in wire_only_names
|
|
395
|
+
and item.condition is None
|
|
396
|
+
and item.name not in fields
|
|
397
|
+
]
|
|
398
|
+
if missing:
|
|
399
|
+
yield f"encoding needs every field, missing {', '.join(missing)}", "/expect/fields"
|
|
400
|
+
|
|
401
|
+
# --- names ------------------------------------------------------------------------------------
|
|
402
|
+
|
|
403
|
+
def _check_identifier(self, name: str, source: Source, what: str) -> None:
|
|
404
|
+
if name in CPP_KEYWORDS:
|
|
405
|
+
self.report(source, "SR-2", f"{what} {name!r} is a C++ keyword")
|
|
406
|
+
elif _RESERVED_IDENTIFIER.search(name):
|
|
407
|
+
self.report(source, "SR-2", f"{what} {name!r} is a reserved C++ identifier")
|
|
408
|
+
|
|
409
|
+
def _names(self) -> None:
|
|
410
|
+
project = self.project
|
|
411
|
+
generated: dict[str, str] = {}
|
|
412
|
+
sections: list[tuple[str, Iterable[tuple[str, Source]]]] = [
|
|
413
|
+
("define", ((n, d.source) for n, d in project.defines.items())),
|
|
414
|
+
("enum", ((n, e.source) for n, e in project.enums.items())),
|
|
415
|
+
("type", ((n, t.source) for n, t in project.types.items())),
|
|
416
|
+
("user type", ((n, t.source) for n, t in project.user_types.items())),
|
|
417
|
+
("message", ((n, m.source) for n, m in project.messages.items())),
|
|
418
|
+
("interface", ((n, i.source) for n, i in project.interfaces.items())),
|
|
419
|
+
("checksum", ((n, c.source) for n, c in project.checksums.items())),
|
|
420
|
+
("constant", ((n, c.source) for n, c in project.constants.items())),
|
|
421
|
+
]
|
|
422
|
+
for what, entries in sections:
|
|
423
|
+
for name, source in entries:
|
|
424
|
+
self._check_identifier(name, source, what)
|
|
425
|
+
if what in {"constant", "checksum"}:
|
|
426
|
+
continue
|
|
427
|
+
if name in generated:
|
|
428
|
+
message = f"{what} {name!r} collides with {generated[name]} {name!r} in C++"
|
|
429
|
+
self.report(source, "SR-2", message)
|
|
430
|
+
generated.setdefault(name, what)
|
|
431
|
+
for enum in project.enums.values():
|
|
432
|
+
for member in enum.values:
|
|
433
|
+
self._check_identifier(member, enum.source, f"member of enum {enum.name}")
|
|
434
|
+
|
|
435
|
+
# --- enums ------------------------------------------------------------------------------------
|
|
436
|
+
|
|
437
|
+
def _enums(self) -> None:
|
|
438
|
+
for enum in self.project.enums.values():
|
|
439
|
+
low, high = _int_bounds(enum.type, BUILTIN_BITS[enum.type])
|
|
440
|
+
seen: dict[int, str] = {}
|
|
441
|
+
for member, value in enum.values.items():
|
|
442
|
+
if not low <= value <= high:
|
|
443
|
+
message = f"enum {enum.name}.{member} = {value} does not fit {enum.type}"
|
|
444
|
+
self.report(enum.source, "SR-15", message, f"/values/{member}")
|
|
445
|
+
if value in seen:
|
|
446
|
+
message = f"enum {enum.name}: {member} and {seen[value]} share value {value}"
|
|
447
|
+
self.report(enum.source, "SR-15", message, f"/values/{member}")
|
|
448
|
+
seen.setdefault(value, member)
|
|
449
|
+
|
|
450
|
+
# --- types ------------------------------------------------------------------------------------
|
|
451
|
+
|
|
452
|
+
def _all_field_lists(self) -> Iterable[tuple[str, tuple[Field, ...]]]:
|
|
453
|
+
for item in self.project.types.values():
|
|
454
|
+
yield f"type {item.name}", item.fields
|
|
455
|
+
for item in self.project.messages.values():
|
|
456
|
+
yield f"message {item.name}", item.fields
|
|
457
|
+
for interface in self.project.interfaces.values():
|
|
458
|
+
yield f"header of interface {interface.name}", interface.header or ()
|
|
459
|
+
yield f"trailer of interface {interface.name}", interface.trailer
|
|
460
|
+
|
|
461
|
+
def _resolve_types(self) -> None:
|
|
462
|
+
known = (
|
|
463
|
+
set(BUILTIN_BITS)
|
|
464
|
+
| set(self.project.enums)
|
|
465
|
+
| set(self.project.types)
|
|
466
|
+
| set(self.project.user_types)
|
|
467
|
+
)
|
|
468
|
+
for owner, fields in self._all_field_lists():
|
|
469
|
+
for item in fields:
|
|
470
|
+
if item.type in known:
|
|
471
|
+
continue
|
|
472
|
+
if item.type in self.project.messages:
|
|
473
|
+
message = f"{owner}: field {item.name} uses message {item.type} as a type; "
|
|
474
|
+
message += "move the shared fields to `types`"
|
|
475
|
+
else:
|
|
476
|
+
message = f"{owner}: field {item.name} has unknown type {item.type!r}"
|
|
477
|
+
self.report(item.source, "SR-3", message, "/type")
|
|
478
|
+
self._find_cycles()
|
|
479
|
+
|
|
480
|
+
def _find_cycles(self) -> None:
|
|
481
|
+
done: set[str] = set()
|
|
482
|
+
for name in self.project.types:
|
|
483
|
+
self._visit(name, [], done)
|
|
484
|
+
|
|
485
|
+
def _visit(self, name: str, path: list[str], done: set[str]) -> None:
|
|
486
|
+
if name in path:
|
|
487
|
+
cycle = [*path[path.index(name) :], name]
|
|
488
|
+
self.cyclic.update(cycle)
|
|
489
|
+
message = f"type {name} contains itself: {' -> '.join(cycle)}"
|
|
490
|
+
self.report(self.project.types[name].source, "SR-3", message)
|
|
491
|
+
return
|
|
492
|
+
if name in done:
|
|
493
|
+
return
|
|
494
|
+
for inner in self.project.types[name].fields:
|
|
495
|
+
if inner.type in self.project.types:
|
|
496
|
+
self._visit(inner.type, [*path, name], done)
|
|
497
|
+
done.add(name)
|
|
498
|
+
|
|
499
|
+
# --- user constants ---------------------------------------------------------------------------
|
|
500
|
+
|
|
501
|
+
def _constants(self) -> None:
|
|
502
|
+
field_names = {item.name for _, fields in self._all_field_lists() for item in fields}
|
|
503
|
+
profile_defines = {name for p in self.project.profiles.values() for name in p.defines}
|
|
504
|
+
for constant in self.project.constants.values():
|
|
505
|
+
source = constant.source
|
|
506
|
+
if constant.name in self.project.defines or constant.name in profile_defines:
|
|
507
|
+
self.report(source, "SR-18", f"constant {constant.name} collides with a define")
|
|
508
|
+
if constant.name in field_names:
|
|
509
|
+
message = f"constant {constant.name} collides with a field of the same name"
|
|
510
|
+
self.report(source, "SR-18", message)
|
|
511
|
+
for profile in constant.profile_values:
|
|
512
|
+
if profile not in self.project.profiles:
|
|
513
|
+
message = f"constant {constant.name}: unknown profile {profile!r}"
|
|
514
|
+
self.report(source, "SR-18", message, f"/profile_values/{profile}")
|
|
515
|
+
given = set(constant.profile_values)
|
|
516
|
+
partial = given and given != set(self.project.profiles)
|
|
517
|
+
if constant.value is None and partial:
|
|
518
|
+
message = (
|
|
519
|
+
f"constant {constant.name} has no value and values for only some profiles; "
|
|
520
|
+
"give `value` or a value for every profile"
|
|
521
|
+
)
|
|
522
|
+
self.report(source, "SR-18", message)
|
|
523
|
+
|
|
524
|
+
# --- checksum declarations --------------------------------------------------------------------
|
|
525
|
+
|
|
526
|
+
def _checksums(self) -> None:
|
|
527
|
+
for checksum in self.project.checksums.values():
|
|
528
|
+
if checksum.algo == "crc":
|
|
529
|
+
self._crc_parameters(checksum)
|
|
530
|
+
if checksum.algo == "custom":
|
|
531
|
+
self._custom_checksum(checksum)
|
|
532
|
+
|
|
533
|
+
def _crc_parameters(self, checksum: Checksum) -> None:
|
|
534
|
+
width = checksum.width or 0
|
|
535
|
+
limit = 1 << width
|
|
536
|
+
for key in ("poly", "init", "xorout"):
|
|
537
|
+
value = getattr(checksum, key)
|
|
538
|
+
if value is not None and value >= limit:
|
|
539
|
+
message = f"checksum {checksum.name}: {key} {value:#x} does not fit width {width}"
|
|
540
|
+
self.report(checksum.source, "SR-12", message, f"/{key}")
|
|
541
|
+
if checksum.check is not None and checksum.poly is not None:
|
|
542
|
+
actual = _crc(checksum, CHECK_INPUT)
|
|
543
|
+
if actual != checksum.check:
|
|
544
|
+
message = (
|
|
545
|
+
f"checksum {checksum.name}: check value {checksum.check:#x} does not match "
|
|
546
|
+
f"the computed {actual:#x} for '123456789'"
|
|
547
|
+
)
|
|
548
|
+
self.report(checksum.source, "SR-12", message, "/check")
|
|
549
|
+
|
|
550
|
+
def _custom_checksum(self, checksum: Checksum) -> None:
|
|
551
|
+
if (checksum.size_bytes or 0) > MAX_CHECKSUM_BYTES:
|
|
552
|
+
message = f"checksum {checksum.name}: size_bytes must be 1..8 (runtime contract)"
|
|
553
|
+
self.report(checksum.source, "SR-12", message, "/size_bytes")
|
|
554
|
+
if checksum.include is not None and not _is_relative_path(checksum.include):
|
|
555
|
+
message = f"checksum {checksum.name}: include {checksum.include!r} must be relative"
|
|
556
|
+
self.report(checksum.source, "SR-20", message + " without '..'", "/include")
|
|
557
|
+
|
|
558
|
+
# --- user types -------------------------------------------------------------------------------
|
|
559
|
+
|
|
560
|
+
def _user_types(self) -> None:
|
|
561
|
+
for item in self.project.user_types.values():
|
|
562
|
+
if item.size_bytes <= 0:
|
|
563
|
+
self.report(
|
|
564
|
+
item.source,
|
|
565
|
+
"SR-6",
|
|
566
|
+
f"user type {item.name}: size_bytes must be positive",
|
|
567
|
+
"/size_bytes",
|
|
568
|
+
)
|
|
569
|
+
if item.include is not None and not _is_relative_path(item.include):
|
|
570
|
+
message = f"user type {item.name}: include {item.include!r} must be relative"
|
|
571
|
+
self.report(item.source, "SR-20", message + " without '..'", "/include")
|
|
572
|
+
|
|
573
|
+
# --- expressions ------------------------------------------------------------------------------
|
|
574
|
+
|
|
575
|
+
def _parse(self, raw: Value, at: _At) -> expr.Node | None:
|
|
576
|
+
if isinstance(raw, float):
|
|
577
|
+
self.report(at, "SR-5", f"invalid expression {raw!r}: not an integer")
|
|
578
|
+
return None
|
|
579
|
+
try:
|
|
580
|
+
return expr.as_expression(raw)
|
|
581
|
+
except ExprError as error:
|
|
582
|
+
self.report(at, "SR-5", f"invalid expression {raw!r}: {error}")
|
|
583
|
+
return None
|
|
584
|
+
|
|
585
|
+
def _profile_define(self, name: str) -> bool:
|
|
586
|
+
return any(name in profile.defines for profile in self.project.profiles.values())
|
|
587
|
+
|
|
588
|
+
def _constant_use(self, name: str, at: _At, kind: str) -> None:
|
|
589
|
+
if kind == LAYOUT and any(name in variant.opaque for variant in self.variants):
|
|
590
|
+
message = (
|
|
591
|
+
f"constant {name} has no value (opaque) and may only be used in min/max, "
|
|
592
|
+
"not in sizes or counts"
|
|
593
|
+
)
|
|
594
|
+
self.report(at, "SR-18", message)
|
|
595
|
+
|
|
596
|
+
def _names_resolve(self, node: expr.Node, at: _At, kind: str, context: _Context | None) -> bool:
|
|
597
|
+
"""Check every name; returns False when the expression refers to fields (not static)."""
|
|
598
|
+
static = True
|
|
599
|
+
for name in sorted(expr.referenced_names(node)):
|
|
600
|
+
if context is not None and context.index_of(name) is not None:
|
|
601
|
+
static = False
|
|
602
|
+
if kind != COUNT:
|
|
603
|
+
self.report(
|
|
604
|
+
at, "SR-5", f"field {name!r} may only be referenced by occurs.count"
|
|
605
|
+
)
|
|
606
|
+
elif name in self.project.constants:
|
|
607
|
+
self._constant_use(name, at, kind)
|
|
608
|
+
elif name not in self.project.defines and not self._profile_define(name):
|
|
609
|
+
self.report(at, "SR-5", f"unknown name {name!r}")
|
|
610
|
+
for enum, member in sorted(expr.referenced_members(node)):
|
|
611
|
+
if (enum, member) not in self.members:
|
|
612
|
+
self.report(at, "SR-5", f"unknown enum member {enum}.{member}")
|
|
613
|
+
for type_name in sorted(expr.call_arguments(node, "sizeof")):
|
|
614
|
+
known_sizeof = type_name in self.project.types or type_name in BUILTIN_BITS
|
|
615
|
+
known_sizeof = known_sizeof or type_name in self.project.user_types
|
|
616
|
+
if not known_sizeof:
|
|
617
|
+
self.report(at, "SR-5", f"sizeof: unknown type {type_name!r}")
|
|
618
|
+
return static
|
|
619
|
+
|
|
620
|
+
def _scope_with_sizes(self, node: expr.Node, variant: Variant) -> Scope | None:
|
|
621
|
+
sizes = {}
|
|
622
|
+
for type_name in expr.call_arguments(node, "sizeof"):
|
|
623
|
+
bits = self._type_bits(type_name, variant, maximum=False)
|
|
624
|
+
if bits is None or bits % BYTE_BITS:
|
|
625
|
+
return None
|
|
626
|
+
sizes[type_name] = bits // BYTE_BITS
|
|
627
|
+
return dataclasses.replace(variant.scope, sizes=sizes) if sizes else variant.scope
|
|
628
|
+
|
|
629
|
+
def _evaluate(
|
|
630
|
+
self, raw: Value, at: _At, kind: str, context: _Context | None = None
|
|
631
|
+
) -> dict[str, int] | None:
|
|
632
|
+
"""Values per variant, or None when the expression is invalid or not static."""
|
|
633
|
+
node = self._parse(raw, at)
|
|
634
|
+
if node is None or not self._names_resolve(node, at, kind, context):
|
|
635
|
+
return None
|
|
636
|
+
names = expr.referenced_names(node)
|
|
637
|
+
results: dict[str, int] = {}
|
|
638
|
+
for variant in self.variants:
|
|
639
|
+
if names & variant.opaque:
|
|
640
|
+
continue
|
|
641
|
+
scope = self._scope_with_sizes(node, variant)
|
|
642
|
+
if scope is None:
|
|
643
|
+
self.report(at, "SR-5", "sizeof() needs a fixed-size type" + variant.label())
|
|
644
|
+
return None
|
|
645
|
+
try:
|
|
646
|
+
results[variant.name] = expr.evaluate(node, scope)
|
|
647
|
+
except ExprError as error:
|
|
648
|
+
self.report(at, variant.rule, f"cannot evaluate {raw!r}{variant.label()}: {error}")
|
|
649
|
+
return None
|
|
650
|
+
return results
|
|
651
|
+
|
|
652
|
+
def static_value(self, raw: Value, variant: Variant) -> int | None:
|
|
653
|
+
"""Silent evaluation of a layout expression (problems are reported elsewhere)."""
|
|
654
|
+
try:
|
|
655
|
+
node = expr.as_expression(raw) if not isinstance(raw, float) else None
|
|
656
|
+
scope = None if node is None else self._scope_with_sizes(node, variant)
|
|
657
|
+
return None if node is None or scope is None else expr.evaluate(node, scope)
|
|
658
|
+
except ExprError:
|
|
659
|
+
return None
|
|
660
|
+
|
|
661
|
+
def _condition(self, raw: str | None, at: _At, context: _Context | None = None) -> None:
|
|
662
|
+
if raw is None:
|
|
663
|
+
return
|
|
664
|
+
try:
|
|
665
|
+
node = expr.parse_condition(raw)
|
|
666
|
+
except ExprError as error:
|
|
667
|
+
self.report(at, "SR-5", f"invalid if: condition {raw!r}: {error}")
|
|
668
|
+
return
|
|
669
|
+
for name in sorted(expr.referenced_names(node)):
|
|
670
|
+
if context is not None and context.index_of(name) is not None:
|
|
671
|
+
self.report(at, "SR-5", f"if: conditions must not reference fields ({name!r})")
|
|
672
|
+
elif name in self.project.constants:
|
|
673
|
+
message = f"constant {name} is a C++ symbol, not a macro; it cannot appear in if:"
|
|
674
|
+
self.report(at, "SR-18", message)
|
|
675
|
+
elif name not in self.project.defines:
|
|
676
|
+
message = f"if: may name only defines; use defined({name}) for profile macros"
|
|
677
|
+
self.report(at, "SR-5", message)
|
|
678
|
+
|
|
679
|
+
# --- sizes ----------------------------------------------------------------------------------
|
|
680
|
+
|
|
681
|
+
def _type_bits(self, type_name: str, variant: Variant, *, maximum: bool) -> int | None:
|
|
682
|
+
"""Wire size in bits of one element; with maximum=True dynamic arrays count at capacity."""
|
|
683
|
+
if type_name in BUILTIN_BITS:
|
|
684
|
+
return BUILTIN_BITS[type_name]
|
|
685
|
+
if type_name in self.project.enums:
|
|
686
|
+
return BUILTIN_BITS[self.project.enums[type_name].type]
|
|
687
|
+
if type_name in self.project.user_types:
|
|
688
|
+
return self.project.user_types[type_name].size_bytes * BYTE_BITS
|
|
689
|
+
item = self.project.types.get(type_name)
|
|
690
|
+
if item is None or type_name in self.cyclic:
|
|
691
|
+
return None
|
|
692
|
+
key = (type_name, variant.name, maximum)
|
|
693
|
+
if key not in self._bits_cache:
|
|
694
|
+
self._bits_cache[key] = self._fields_bits(item.fields, variant, maximum=maximum)
|
|
695
|
+
return self._bits_cache[key]
|
|
696
|
+
|
|
697
|
+
def present(self, item: Field, variant: Variant) -> bool:
|
|
698
|
+
"""Return whether the field's `if:` condition holds in `variant` (true when unknown)."""
|
|
699
|
+
if item.condition is None:
|
|
700
|
+
return True
|
|
701
|
+
try:
|
|
702
|
+
return expr.evaluate(expr.parse_condition(item.condition), variant.scope) != 0
|
|
703
|
+
except ExprError:
|
|
704
|
+
return True
|
|
705
|
+
|
|
706
|
+
def _fields_bits(
|
|
707
|
+
self, fields: tuple[Field, ...], variant: Variant, *, maximum: bool
|
|
708
|
+
) -> int | None:
|
|
709
|
+
total = 0
|
|
710
|
+
for item in fields:
|
|
711
|
+
if not self.present(item, variant):
|
|
712
|
+
continue
|
|
713
|
+
bits = self._field_bits(item, variant, maximum=maximum)
|
|
714
|
+
if bits is None:
|
|
715
|
+
return None
|
|
716
|
+
total += bits
|
|
717
|
+
return total
|
|
718
|
+
|
|
719
|
+
def _field_bits(self, item: Field, variant: Variant, *, maximum: bool) -> int | None:
|
|
720
|
+
element = item.bits or self._type_bits(item.type, variant, maximum=maximum)
|
|
721
|
+
if element is None or item.occurs is None:
|
|
722
|
+
return element
|
|
723
|
+
dynamic = item.occurs.count is not None or item.occurs.fill
|
|
724
|
+
if dynamic and not maximum:
|
|
725
|
+
return None
|
|
726
|
+
count = self.static_value(item.occurs.max, variant)
|
|
727
|
+
return None if count is None else element * count
|
|
728
|
+
|
|
729
|
+
# --- structs --------------------------------------------------------------------------------
|
|
730
|
+
|
|
731
|
+
def _struct(self, context: _Context, item: Struct | None = None) -> None:
|
|
732
|
+
if item is not None:
|
|
733
|
+
self._condition(item.condition, _At(item.source, "/if"))
|
|
734
|
+
seen: set[str] = set()
|
|
735
|
+
self._current = context
|
|
736
|
+
for index, field in enumerate(context.fields):
|
|
737
|
+
at = _At(field.source)
|
|
738
|
+
self._check_identifier(field.name, field.source, f"{context.owner}: field")
|
|
739
|
+
if field.name in seen:
|
|
740
|
+
self.report(at, "SR-2", f"{context.owner}: duplicate field {field.name!r}")
|
|
741
|
+
seen.add(field.name)
|
|
742
|
+
self._field_bits_rules(field, at)
|
|
743
|
+
self._field_occurs(field, index, context)
|
|
744
|
+
self._field_role(field, context)
|
|
745
|
+
self._field_checksum(field, index, context)
|
|
746
|
+
self._field_values(field, at)
|
|
747
|
+
self._condition(field.condition, at.below("/if"), context)
|
|
748
|
+
pure_bits_type = item is not None and item.message is None and self._pure_bits(item)
|
|
749
|
+
self._bit_groups(context, check_total=not pure_bits_type)
|
|
750
|
+
|
|
751
|
+
def _pure_bits(self, item: Struct) -> bool:
|
|
752
|
+
return bool(item.fields) and all(field.bits is not None for field in item.fields)
|
|
753
|
+
|
|
754
|
+
def _field_bits_rules(self, field: Field, at: _At) -> None:
|
|
755
|
+
if field.bits is None:
|
|
756
|
+
return
|
|
757
|
+
base = (
|
|
758
|
+
self.project.enums[field.type].type if field.type in self.project.enums else field.type
|
|
759
|
+
)
|
|
760
|
+
if base not in INTEGER_TYPES and base != "bool":
|
|
761
|
+
message = f"bits is only allowed on integer, bool and enum fields ({field.name})"
|
|
762
|
+
self.report(at, "SR-10", message, "/bits")
|
|
763
|
+
elif field.bits > BUILTIN_BITS[base]:
|
|
764
|
+
message = f"field {field.name}: bits {field.bits} exceeds the width of {base}"
|
|
765
|
+
self.report(at, "SR-10", message, "/bits")
|
|
766
|
+
if field.occurs is not None:
|
|
767
|
+
self.report(at, "SR-11", f"field {field.name}: occurs cannot be combined with bits")
|
|
768
|
+
|
|
769
|
+
def _group_bits(self, field: Field) -> int | None:
|
|
770
|
+
"""Bits contributed to a bit group, or None when the field is byte-oriented."""
|
|
771
|
+
if field.bits is not None:
|
|
772
|
+
return field.bits if field.occurs is None else None
|
|
773
|
+
item = self.project.types.get(field.type)
|
|
774
|
+
if item is None or field.occurs is not None or not self._pure_bits(item):
|
|
775
|
+
return None
|
|
776
|
+
return sum(inner.bits or 0 for inner in item.fields)
|
|
777
|
+
|
|
778
|
+
def _bit_groups(self, context: _Context, *, check_total: bool) -> None:
|
|
779
|
+
group: list[Field] = []
|
|
780
|
+
for field in [*context.fields, None]:
|
|
781
|
+
bits = None if field is None else self._group_bits(field)
|
|
782
|
+
if bits is not None and field is not None:
|
|
783
|
+
group.append(field)
|
|
784
|
+
continue
|
|
785
|
+
if group:
|
|
786
|
+
self._close_group(context, group, check_total=check_total)
|
|
787
|
+
group = []
|
|
788
|
+
|
|
789
|
+
def _close_group(self, context: _Context, group: list[Field], *, check_total: bool) -> None:
|
|
790
|
+
total = sum(self._group_bits(field) or 0 for field in group)
|
|
791
|
+
first = group[0]
|
|
792
|
+
if check_total and total % BYTE_BITS:
|
|
793
|
+
message = (
|
|
794
|
+
f"{context.owner}: bit group starting at {first.name} totals {total} bits; "
|
|
795
|
+
"add reserved fields up to a multiple of 8"
|
|
796
|
+
)
|
|
797
|
+
self.report(first.source, "SR-10", message)
|
|
798
|
+
if total > MAX_GROUP_BITS:
|
|
799
|
+
message = f"{context.owner}: bit group starting at {first.name} exceeds 64 bits"
|
|
800
|
+
self.report(first.source, "SR-10", message)
|
|
801
|
+
for field in group:
|
|
802
|
+
if field.condition is not None:
|
|
803
|
+
message = (
|
|
804
|
+
f"{context.owner}: conditional field {field.name} inside a bit group would "
|
|
805
|
+
"change the bit layout per profile; make the whole group conditional"
|
|
806
|
+
)
|
|
807
|
+
self.report(field.source, "SR-17", message, "/if")
|
|
808
|
+
|
|
809
|
+
def _field_occurs(self, field: Field, index: int, context: _Context) -> None:
|
|
810
|
+
occurs = field.occurs
|
|
811
|
+
if occurs is None:
|
|
812
|
+
return
|
|
813
|
+
at = _At(field.source, "/occurs")
|
|
814
|
+
maxima = self._evaluate(occurs.max, at.below("/max"), LAYOUT) or {}
|
|
815
|
+
minima = {} if occurs.min is None else self._evaluate(occurs.min, at.below("/min"), LAYOUT)
|
|
816
|
+
for variant in self.variants:
|
|
817
|
+
high = maxima.get(variant.name)
|
|
818
|
+
low = (minima or {}).get(variant.name, 0)
|
|
819
|
+
if high is not None and high <= 0:
|
|
820
|
+
message = f"field {field.name}: occurs.max must be positive{variant.label()}"
|
|
821
|
+
self.report(at, "SR-8", message, "/max")
|
|
822
|
+
if high is not None and not 0 <= low <= high:
|
|
823
|
+
message = f"field {field.name}: occurs.min must lie in 0..max{variant.label()}"
|
|
824
|
+
self.report(at, "SR-8", message, "/min")
|
|
825
|
+
if occurs.count is not None:
|
|
826
|
+
self._evaluate(occurs.count, at.below("/count"), COUNT, context)
|
|
827
|
+
self._count_references(field, index, context, at.below("/count"))
|
|
828
|
+
if occurs.fill:
|
|
829
|
+
self._fill(field, index, context, at)
|
|
830
|
+
|
|
831
|
+
def _count_references(self, field: Field, index: int, context: _Context, at: _At) -> None:
|
|
832
|
+
count = field.occurs.count if field.occurs is not None else None
|
|
833
|
+
try:
|
|
834
|
+
node = None if count is None or isinstance(count, float) else expr.as_expression(count)
|
|
835
|
+
except ExprError:
|
|
836
|
+
node = None # already reported by _evaluate
|
|
837
|
+
for name in sorted(expr.referenced_names(node) if node is not None else ()):
|
|
838
|
+
position = context.index_of(name)
|
|
839
|
+
if position is None:
|
|
840
|
+
continue
|
|
841
|
+
target = context.fields[position]
|
|
842
|
+
base = self.project.enums.get(target.type)
|
|
843
|
+
scalar = target.type in INTEGER_TYPES or target.type == "bool" or base is not None
|
|
844
|
+
if position >= index:
|
|
845
|
+
message = f"count field {name!r} must precede {field.name}"
|
|
846
|
+
self.report(at, "SR-7", message)
|
|
847
|
+
elif not scalar or target.occurs is not None:
|
|
848
|
+
message = f"count field {name!r} must be a single integer or bool"
|
|
849
|
+
self.report(at, "SR-7", message)
|
|
850
|
+
|
|
851
|
+
def _fill(self, field: Field, index: int, context: _Context, at: _At) -> None:
|
|
852
|
+
if not context.is_message or index != len(context.fields) - 1:
|
|
853
|
+
message = f"field {field.name}: fill is only allowed on the last field of a message"
|
|
854
|
+
self.report(at, "SR-8", message, "/fill")
|
|
855
|
+
return
|
|
856
|
+
for variant in self.variants:
|
|
857
|
+
element = field.bits or self._type_bits(field.type, variant, maximum=False)
|
|
858
|
+
if element is None:
|
|
859
|
+
message = f"field {field.name}: fill needs a constant element size{variant.label()}"
|
|
860
|
+
self.report(at, "SR-8", message, "/fill")
|
|
861
|
+
return
|
|
862
|
+
|
|
863
|
+
def _field_role(self, field: Field, context: _Context) -> None:
|
|
864
|
+
if field.role is not None and not context.frame_part:
|
|
865
|
+
message = f"{context.owner}: role {field.role!r} is only allowed in headers/trailers"
|
|
866
|
+
self.report(field.source, "SR-13", message, "/role")
|
|
867
|
+
|
|
868
|
+
# --- checksum fields --------------------------------------------------------------------------
|
|
869
|
+
|
|
870
|
+
def _field_checksum(self, field: Field, index: int, context: _Context) -> None:
|
|
871
|
+
use = field.checksum
|
|
872
|
+
if use is None:
|
|
873
|
+
return
|
|
874
|
+
at = _At(field.source, "/checksum")
|
|
875
|
+
algo = self.project.checksums.get(use.use)
|
|
876
|
+
width = field.bits or BUILTIN_BITS.get(field.type, 0)
|
|
877
|
+
if algo is None:
|
|
878
|
+
self.report(at, "SR-4", f"field {field.name}: unknown checksum {use.use!r}", "/use")
|
|
879
|
+
elif field.type not in UNSIGNED_TYPES or field.occurs is not None:
|
|
880
|
+
message = f"checksum field {field.name} must be a single unsigned integer"
|
|
881
|
+
self.report(at, "SR-12", message)
|
|
882
|
+
elif width != _checksum_bits(algo):
|
|
883
|
+
message = (
|
|
884
|
+
f"checksum field {field.name} has {width} bits but {algo.name} produces "
|
|
885
|
+
f"{_checksum_bits(algo)}"
|
|
886
|
+
)
|
|
887
|
+
self.report(at, "SR-12", message)
|
|
888
|
+
if use.scope in {"payload", "header_and_payload"} and not context.frame_part:
|
|
889
|
+
message = f"covers scope {use.scope!r} is only allowed in headers/trailers"
|
|
890
|
+
self.report(at, "SR-12", message, "/covers/scope")
|
|
891
|
+
if use.first is not None and use.last is not None:
|
|
892
|
+
self._covers_range(field, index, context, at.below("/covers"))
|
|
893
|
+
|
|
894
|
+
def _covers_range(self, field: Field, index: int, context: _Context, at: _At) -> None:
|
|
895
|
+
use = field.checksum
|
|
896
|
+
if use is None or use.first is None or use.last is None:
|
|
897
|
+
return
|
|
898
|
+
first = context.index_of(use.first)
|
|
899
|
+
last = context.index_of(use.last)
|
|
900
|
+
for name, position in ((use.first, first), (use.last, last)):
|
|
901
|
+
if position is None:
|
|
902
|
+
self.report(at, "SR-4", f"covers names unknown field {name!r}")
|
|
903
|
+
if first is not None and last is not None and not first <= last < index:
|
|
904
|
+
message = (
|
|
905
|
+
f"covers {use.first}..{use.last} must be ordered and precede the checksum "
|
|
906
|
+
f"field {field.name}"
|
|
907
|
+
)
|
|
908
|
+
self.report(at, "SR-12", message)
|
|
909
|
+
|
|
910
|
+
# --- values and ranges ------------------------------------------------------------------------
|
|
911
|
+
|
|
912
|
+
def _value_kind(self, field: Field) -> str:
|
|
913
|
+
if field.type in self.project.enums:
|
|
914
|
+
return "enum"
|
|
915
|
+
if field.type in FLOAT_TYPES:
|
|
916
|
+
return "float"
|
|
917
|
+
if field.type in INTEGER_TYPES:
|
|
918
|
+
return "int"
|
|
919
|
+
if field.type in {"bool", "char"}:
|
|
920
|
+
return field.type
|
|
921
|
+
return "struct"
|
|
922
|
+
|
|
923
|
+
def _type_range(self, field: Field, kind: str) -> tuple[float, float]:
|
|
924
|
+
if kind == "float":
|
|
925
|
+
limit = FLOAT32_MAX if field.type == "f32" else sys.float_info.max
|
|
926
|
+
return -limit, limit
|
|
927
|
+
if kind in {"bool", "char"}:
|
|
928
|
+
return 0, 1 if kind == "bool" else 255
|
|
929
|
+
base = self.project.enums[field.type].type if kind == "enum" else field.type
|
|
930
|
+
return _int_bounds(base, field.bits or BUILTIN_BITS[base])
|
|
931
|
+
|
|
932
|
+
def _field_values(self, field: Field, at: _At) -> None:
|
|
933
|
+
kind = self._value_kind(field)
|
|
934
|
+
given = [
|
|
935
|
+
key for key in ("min", "max", "default", "const") if getattr(field, key) is not None
|
|
936
|
+
]
|
|
937
|
+
if kind == "struct":
|
|
938
|
+
for key in given:
|
|
939
|
+
self.report(at, "SR-6", f"field {field.name}: {key} needs a scalar type", f"/{key}")
|
|
940
|
+
return
|
|
941
|
+
self._check_enum_start(field, kind, at)
|
|
942
|
+
type_range = self._type_range(field, kind)
|
|
943
|
+
lows = self._bound(field, "min", kind, at)
|
|
944
|
+
highs = self._bound(field, "max", kind, at)
|
|
945
|
+
self._check_bounds(field, at, (lows, highs), type_range)
|
|
946
|
+
limits = (lows, highs, type_range)
|
|
947
|
+
if field.default is not None:
|
|
948
|
+
self._check_default(field, at.below("/default"), kind, limits)
|
|
949
|
+
if field.const is not None:
|
|
950
|
+
if field.occurs is not None:
|
|
951
|
+
self.report(at, "SR-6", f"field {field.name}: const cannot have occurs", "/const")
|
|
952
|
+
self._check_scalar(field, field.const, at.below("/const"), kind, limits)
|
|
953
|
+
|
|
954
|
+
def _check_enum_start(self, field: Field, kind: str, at: _At) -> None:
|
|
955
|
+
"""SR-23: a field starts as zero, so an enum field without a default needs a member 0."""
|
|
956
|
+
dynamic = field.occurs is not None and (field.occurs.count is not None or field.occurs.fill)
|
|
957
|
+
if kind != "enum" or field.default is not None or field.const is not None or dynamic:
|
|
958
|
+
return
|
|
959
|
+
if 0 not in self.project.enums[field.type].values.values():
|
|
960
|
+
message = (
|
|
961
|
+
f"field {field.name}: enum {field.type} has no member with value 0 and the field "
|
|
962
|
+
"has no default, so init() would start it with a value that is not a member"
|
|
963
|
+
)
|
|
964
|
+
self.report(at, "SR-23", message)
|
|
965
|
+
|
|
966
|
+
def _bound(self, field: Field, key: str, kind: str, at: _At) -> dict[str, float]:
|
|
967
|
+
raw = getattr(field, key)
|
|
968
|
+
if raw is None:
|
|
969
|
+
return {}
|
|
970
|
+
if isinstance(raw, float) and kind != "float":
|
|
971
|
+
message = f"field {field.name}: {key} {raw} must be an integer for type {field.type}"
|
|
972
|
+
self.report(at, "SR-6", message, f"/{key}")
|
|
973
|
+
return {}
|
|
974
|
+
if isinstance(raw, (int, float)):
|
|
975
|
+
return {variant.name: raw for variant in self.variants}
|
|
976
|
+
return dict(self._evaluate(raw, at.below(f"/{key}"), RANGE, self._current) or {})
|
|
977
|
+
|
|
978
|
+
def _check_bounds(
|
|
979
|
+
self,
|
|
980
|
+
field: Field,
|
|
981
|
+
at: _At,
|
|
982
|
+
bounds: tuple[dict[str, float], dict[str, float]],
|
|
983
|
+
type_range: tuple[float, float],
|
|
984
|
+
) -> None:
|
|
985
|
+
lows, highs = bounds
|
|
986
|
+
for variant in self.variants:
|
|
987
|
+
low = lows.get(variant.name)
|
|
988
|
+
high = highs.get(variant.name)
|
|
989
|
+
if low is not None and high is not None and low > high:
|
|
990
|
+
message = f"field {field.name}: min {low} > max {high}{variant.label()}"
|
|
991
|
+
self.report(at, "SR-6", message, "/min")
|
|
992
|
+
for key, value in (("min", low), ("max", high)):
|
|
993
|
+
if value is not None and not type_range[0] <= value <= type_range[1]:
|
|
994
|
+
message = (
|
|
995
|
+
f"field {field.name}: {key} {value} is outside what {field.type}"
|
|
996
|
+
f"{f' with {field.bits} bits' if field.bits else ''} can hold"
|
|
997
|
+
)
|
|
998
|
+
self.report(at, "SR-6", message, f"/{key}")
|
|
999
|
+
|
|
1000
|
+
def _check_default(
|
|
1001
|
+
self,
|
|
1002
|
+
field: Field,
|
|
1003
|
+
at: _At,
|
|
1004
|
+
kind: str,
|
|
1005
|
+
limits: tuple[dict[str, float], dict[str, float], tuple[float, float]],
|
|
1006
|
+
) -> None:
|
|
1007
|
+
default = field.default
|
|
1008
|
+
text = isinstance(default, str) and kind == "char" and field.occurs is not None
|
|
1009
|
+
if not isinstance(default, list) and not text:
|
|
1010
|
+
self._check_scalar(field, default, at, kind, limits)
|
|
1011
|
+
return
|
|
1012
|
+
if field.occurs is None:
|
|
1013
|
+
self.report(at, "SR-6", f"field {field.name}: a list default needs occurs")
|
|
1014
|
+
return
|
|
1015
|
+
elements: list[Scalar] = list(default) if isinstance(default, (list, str)) else []
|
|
1016
|
+
for variant in self.variants:
|
|
1017
|
+
capacity = self.static_value(field.occurs.max, variant)
|
|
1018
|
+
if capacity is not None and len(elements) > capacity:
|
|
1019
|
+
message = (
|
|
1020
|
+
f"field {field.name}: default has {len(elements)} elements, "
|
|
1021
|
+
f"max is {capacity}{variant.label()}"
|
|
1022
|
+
)
|
|
1023
|
+
self.report(at, "SR-6", message)
|
|
1024
|
+
for position, element in enumerate(elements):
|
|
1025
|
+
self._check_scalar(field, element, at.below(f"/{position}"), kind, limits)
|
|
1026
|
+
|
|
1027
|
+
def _scalar_number(
|
|
1028
|
+
self, field: Field, value: Scalar | list[Scalar] | None, kind: str, at: _At
|
|
1029
|
+
) -> float | None:
|
|
1030
|
+
number: float | None = None
|
|
1031
|
+
if kind == "enum" and isinstance(value, str):
|
|
1032
|
+
number = self.project.enums[field.type].values.get(value)
|
|
1033
|
+
if number is None:
|
|
1034
|
+
self.report(
|
|
1035
|
+
at, "SR-4", f"field {field.name}: {value!r} is not a member of {field.type}"
|
|
1036
|
+
)
|
|
1037
|
+
return None
|
|
1038
|
+
elif kind == "enum" and isinstance(value, int) and not isinstance(value, bool):
|
|
1039
|
+
if value in self.project.enums[field.type].values.values():
|
|
1040
|
+
number = value
|
|
1041
|
+
elif kind == "bool" and isinstance(value, bool):
|
|
1042
|
+
number = int(value)
|
|
1043
|
+
elif kind == "char" and isinstance(value, str) and len(value) == 1:
|
|
1044
|
+
number = ord(value)
|
|
1045
|
+
elif (kind in {"char", "int"} and isinstance(value, int)) or (
|
|
1046
|
+
kind == "float" and isinstance(value, (int, float))
|
|
1047
|
+
):
|
|
1048
|
+
number = None if isinstance(value, bool) else value
|
|
1049
|
+
if number is None:
|
|
1050
|
+
self.report(
|
|
1051
|
+
at, "SR-6", f"field {field.name}: {value!r} is not a valid {field.type} value"
|
|
1052
|
+
)
|
|
1053
|
+
return number
|
|
1054
|
+
|
|
1055
|
+
def _check_scalar(
|
|
1056
|
+
self,
|
|
1057
|
+
field: Field,
|
|
1058
|
+
value: Scalar | list[Scalar] | None,
|
|
1059
|
+
at: _At,
|
|
1060
|
+
kind: str,
|
|
1061
|
+
limits: tuple[dict[str, float], dict[str, float], tuple[float, float]],
|
|
1062
|
+
) -> None:
|
|
1063
|
+
number = self._scalar_number(field, value, kind, at)
|
|
1064
|
+
if number is None:
|
|
1065
|
+
return
|
|
1066
|
+
lows, highs, type_range = limits
|
|
1067
|
+
if not type_range[0] <= number <= type_range[1]:
|
|
1068
|
+
message = f"field {field.name}: {value!r} does not fit {field.type}"
|
|
1069
|
+
self.report(at, "SR-6", message)
|
|
1070
|
+
return
|
|
1071
|
+
for variant in self.variants:
|
|
1072
|
+
low = lows.get(variant.name, type_range[0])
|
|
1073
|
+
high = highs.get(variant.name, type_range[1])
|
|
1074
|
+
if not low <= number <= high:
|
|
1075
|
+
message = (
|
|
1076
|
+
f"field {field.name}: {value!r} is outside [{low}, {high}]{variant.label()}"
|
|
1077
|
+
)
|
|
1078
|
+
self.report(at, "SR-6", message)
|
|
1079
|
+
return
|
|
1080
|
+
|
|
1081
|
+
# --- interfaces -------------------------------------------------------------------------------
|
|
1082
|
+
|
|
1083
|
+
def _interface(self, interface: Interface) -> None:
|
|
1084
|
+
if interface.header is not None:
|
|
1085
|
+
owner = f"header of interface {interface.name}"
|
|
1086
|
+
self._struct(_Context(owner, interface.header, frame_part=True))
|
|
1087
|
+
self._header_roles(interface)
|
|
1088
|
+
if interface.trailer:
|
|
1089
|
+
owner = f"trailer of interface {interface.name}"
|
|
1090
|
+
self._struct(_Context(owner, interface.trailer, frame_part=True))
|
|
1091
|
+
self._framing(interface)
|
|
1092
|
+
self._interface_messages(interface)
|
|
1093
|
+
self._frame_sizes(interface)
|
|
1094
|
+
|
|
1095
|
+
def _header_roles(self, interface: Interface) -> None:
|
|
1096
|
+
header = interface.header or ()
|
|
1097
|
+
for role in SINGLE_ROLES:
|
|
1098
|
+
holders = [field for field in header if field.role == role]
|
|
1099
|
+
for extra in holders[1:]:
|
|
1100
|
+
message = f"interface {interface.name}: role {role} appears more than once"
|
|
1101
|
+
self.report(extra.source, "SR-13", message, "/role")
|
|
1102
|
+
for field in header:
|
|
1103
|
+
if field.role == "sync" and field.const is None:
|
|
1104
|
+
message = f"interface {interface.name}: sync field {field.name} needs a const value"
|
|
1105
|
+
self.report(field.source, "SR-13", message)
|
|
1106
|
+
if field.role in ROLE_TYPES_UNSIGNED and field.type not in UNSIGNED_TYPES:
|
|
1107
|
+
message = f"interface {interface.name}: {field.role} field must be unsigned"
|
|
1108
|
+
self.report(field.source, "SR-13", message, "/type")
|
|
1109
|
+
|
|
1110
|
+
def _framing(self, interface: Interface) -> None:
|
|
1111
|
+
at = _At(interface.source)
|
|
1112
|
+
roles = {field.role for field in interface.header or ()}
|
|
1113
|
+
needed = {"sync_length": {"sync", "payload_length"}, "length_prefixed": {"payload_length"}}
|
|
1114
|
+
missing = needed.get(interface.framing.kind, set()) - roles
|
|
1115
|
+
if missing:
|
|
1116
|
+
message = (
|
|
1117
|
+
f"interface {interface.name}: framing {interface.framing.kind} needs header "
|
|
1118
|
+
f"fields with role {', '.join(sorted(missing))}"
|
|
1119
|
+
)
|
|
1120
|
+
self.report(at, "SR-14", message, "/framing/kind")
|
|
1121
|
+
if len(interface.messages) > 1 and "message_id" not in roles:
|
|
1122
|
+
message = (
|
|
1123
|
+
f"interface {interface.name} carries {len(interface.messages)} messages and "
|
|
1124
|
+
"needs a header field with role message_id (raw interfaces carry at most one)"
|
|
1125
|
+
)
|
|
1126
|
+
self.report(at, "SR-14", message, "/messages")
|
|
1127
|
+
sizes = [("framing/frame_size", interface.framing.frame_size)]
|
|
1128
|
+
sizes.append(("framing/max_frame_size", interface.framing.max_frame_size))
|
|
1129
|
+
sizes += [(f"buffers/{key}", value) for key, value in interface.buffers.items()]
|
|
1130
|
+
for path, raw in sizes:
|
|
1131
|
+
if raw is None:
|
|
1132
|
+
continue
|
|
1133
|
+
for name, value in (self._evaluate(raw, at.below(f"/{path}"), LAYOUT) or {}).items():
|
|
1134
|
+
if value <= 0:
|
|
1135
|
+
label = f" in profile {name!r}" if name else ""
|
|
1136
|
+
self.report(at, "SR-16", f"{path} must be positive{label}", f"/{path}")
|
|
1137
|
+
|
|
1138
|
+
def _message_ids(self, interface: Interface) -> list[tuple[int, str, Source]]:
|
|
1139
|
+
ids = []
|
|
1140
|
+
for entry in interface.messages:
|
|
1141
|
+
message = self.project.messages.get(entry.name)
|
|
1142
|
+
if message is None:
|
|
1143
|
+
continue
|
|
1144
|
+
declared = message.message.id if message.message is not None else None
|
|
1145
|
+
identifier = entry.id if entry.id is not None else declared
|
|
1146
|
+
if identifier is None:
|
|
1147
|
+
text = f"message {entry.name} has no id on interface {interface.name}"
|
|
1148
|
+
self.report(entry.source, "SR-9", text)
|
|
1149
|
+
else:
|
|
1150
|
+
ids.append((identifier, entry.name, entry.source))
|
|
1151
|
+
return ids
|
|
1152
|
+
|
|
1153
|
+
def _interface_messages(self, interface: Interface) -> None:
|
|
1154
|
+
seen: set[str] = set()
|
|
1155
|
+
for entry in interface.messages:
|
|
1156
|
+
if entry.name not in self.project.messages:
|
|
1157
|
+
self.report(entry.source, "SR-4", f"unknown message {entry.name!r}")
|
|
1158
|
+
if entry.name in seen:
|
|
1159
|
+
self.report(entry.source, "SR-9", f"message {entry.name} is listed twice")
|
|
1160
|
+
seen.add(entry.name)
|
|
1161
|
+
self._condition(entry.condition, _At(entry.source, "/if"))
|
|
1162
|
+
id_field = next((f for f in interface.header or () if f.role == "message_id"), None)
|
|
1163
|
+
if id_field is None:
|
|
1164
|
+
return
|
|
1165
|
+
width = id_field.bits or BUILTIN_BITS.get(id_field.type, 0)
|
|
1166
|
+
owners: dict[int, str] = {}
|
|
1167
|
+
for identifier, name, source in self._message_ids(interface):
|
|
1168
|
+
if identifier >= 1 << width:
|
|
1169
|
+
self.report(
|
|
1170
|
+
source, "SR-9", f"id {identifier:#x} of {name} does not fit {width} bits"
|
|
1171
|
+
)
|
|
1172
|
+
if identifier in owners:
|
|
1173
|
+
text = f"id {identifier:#x} of {name} is already used by {owners[identifier]}"
|
|
1174
|
+
self.report(source, "SR-9", text)
|
|
1175
|
+
owners.setdefault(identifier, name)
|
|
1176
|
+
|
|
1177
|
+
def _frame_sizes(self, interface: Interface) -> None:
|
|
1178
|
+
framing = interface.framing
|
|
1179
|
+
limit_raw = framing.frame_size if framing.kind == "fixed" else framing.max_frame_size
|
|
1180
|
+
if limit_raw is None:
|
|
1181
|
+
return
|
|
1182
|
+
for variant in self.variants:
|
|
1183
|
+
limit = self.static_value(limit_raw, variant)
|
|
1184
|
+
frame = self._fields_bits(
|
|
1185
|
+
(*(interface.header or ()), *interface.trailer), variant, maximum=True
|
|
1186
|
+
)
|
|
1187
|
+
if limit is None or frame is None:
|
|
1188
|
+
continue
|
|
1189
|
+
for entry in interface.messages:
|
|
1190
|
+
message = self.project.messages.get(entry.name)
|
|
1191
|
+
payload = (
|
|
1192
|
+
None
|
|
1193
|
+
if message is None
|
|
1194
|
+
else self._fields_bits(message.fields, variant, maximum=True)
|
|
1195
|
+
)
|
|
1196
|
+
if payload is not None and (frame + payload) // BYTE_BITS > limit:
|
|
1197
|
+
text = (
|
|
1198
|
+
f"interface {interface.name}: message {entry.name} needs up to "
|
|
1199
|
+
f"{(frame + payload) // BYTE_BITS} bytes per frame, more than {limit}"
|
|
1200
|
+
f"{variant.label()}"
|
|
1201
|
+
)
|
|
1202
|
+
self.report(entry.source, "SR-16", text)
|
|
1203
|
+
|
|
1204
|
+
# --- user includes ----------------------------------------------------------------------------
|
|
1205
|
+
|
|
1206
|
+
def _user_includes(self) -> None:
|
|
1207
|
+
for directory, source in self.project.include_dirs:
|
|
1208
|
+
if not _is_relative_path(directory):
|
|
1209
|
+
text = f"include directory {directory!r} must be relative without '..'"
|
|
1210
|
+
self.report(source, "SR-20", text)
|
|
1211
|
+
for header in self.project.headers:
|
|
1212
|
+
if not _is_relative_path(header.path):
|
|
1213
|
+
text = f"header {header.path!r} must be relative without '..'"
|
|
1214
|
+
self.report(header.source, "SR-20", text, "/path")
|
|
1215
|
+
self._condition(header.condition, _At(header.source, "/if"))
|
|
1216
|
+
|
|
1217
|
+
|
|
1218
|
+
def validate_project(location: Path) -> list[Issue]:
|
|
1219
|
+
"""Load and validate a project: structural issues first, semantic issues after."""
|
|
1220
|
+
result = load_project(location)
|
|
1221
|
+
if result.project is None:
|
|
1222
|
+
return result.issues
|
|
1223
|
+
return result.issues + Validator(result.project).run()
|
|
1224
|
+
|
|
1225
|
+
|
|
1226
|
+
def main(argv: list[str] | None = None) -> int:
|
|
1227
|
+
"""Validate project directories and print one line per issue."""
|
|
1228
|
+
paths = [Path(arg) for arg in (sys.argv[1:] if argv is None else argv)]
|
|
1229
|
+
failures = 0
|
|
1230
|
+
for path in paths:
|
|
1231
|
+
issues = validate_project(path)
|
|
1232
|
+
failures += len([issue for issue in issues if issue.severity == "error"])
|
|
1233
|
+
for issue in issues:
|
|
1234
|
+
sys.stdout.write(f"{issue}\n")
|
|
1235
|
+
if not issues:
|
|
1236
|
+
sys.stdout.write(f"{path}: OK\n")
|
|
1237
|
+
return 1 if failures else 0
|
|
1238
|
+
|
|
1239
|
+
|
|
1240
|
+
if __name__ == "__main__":
|
|
1241
|
+
sys.exit(main())
|