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/generate.py
ADDED
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
"""C++17 code generator: data types, defaults, validation and wire sizes of a project.
|
|
2
|
+
|
|
3
|
+
Output (relative to the output directory):
|
|
4
|
+
- `<project>/user_prelude.hpp`: application headers, prelude lines and checks of user constants;
|
|
5
|
+
- `<project>/model.hpp`: defines, enums, structs and messages with `validate()`,
|
|
6
|
+
`min_wire_size()` and `max_wire_size()` overloads.
|
|
7
|
+
|
|
8
|
+
The output depends only on the model, never on the profile: profile differences are expressed with
|
|
9
|
+
`#if` blocks and overridable define constants, so one generated tree serves every profile.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import TYPE_CHECKING
|
|
20
|
+
|
|
21
|
+
from wireforge import bundle, test_project, versioning
|
|
22
|
+
from wireforge.cpp import Names, Writer, guard, plain_count, string_literal, wire_only
|
|
23
|
+
from wireforge.cpp_codec import CodecWriter, checksum_type
|
|
24
|
+
from wireforge.cpp_interface import InterfaceError, InterfaceHeader
|
|
25
|
+
from wireforge.issues import Issue
|
|
26
|
+
from wireforge.llr import requirement_notes
|
|
27
|
+
from wireforge.loader import load_project
|
|
28
|
+
from wireforge.model import BUILTIN_BITS, Field, Project, Scalar, Source, Struct, UserConstant
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from collections.abc import Iterable
|
|
32
|
+
|
|
33
|
+
BYTE_BITS = 8
|
|
34
|
+
RUNTIME_HEADERS = (
|
|
35
|
+
"wireforge/bit_utils.hpp",
|
|
36
|
+
"wireforge/byte_io.hpp",
|
|
37
|
+
"wireforge/checksum.hpp",
|
|
38
|
+
"wireforge/codec_support.hpp",
|
|
39
|
+
"wireforge/expr_support.hpp",
|
|
40
|
+
"wireforge/span.hpp",
|
|
41
|
+
"wireforge/static_vector.hpp",
|
|
42
|
+
"wireforge/status.hpp",
|
|
43
|
+
"wireforge/validation.hpp",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class Banner:
|
|
49
|
+
"""Identity of the generated files."""
|
|
50
|
+
|
|
51
|
+
project: str
|
|
52
|
+
version: str | None
|
|
53
|
+
content_hash: str
|
|
54
|
+
|
|
55
|
+
def lines(self) -> list[str]:
|
|
56
|
+
"""Doxygen paragraph describing the generator run."""
|
|
57
|
+
version = f" {self.version}" if self.version else ""
|
|
58
|
+
tool = (
|
|
59
|
+
f"WireForge {versioning.TOOL_VERSION} (file format "
|
|
60
|
+
f"{versioning.FORMAT_MAJOR}.{versioning.FORMAT_MINOR})"
|
|
61
|
+
)
|
|
62
|
+
return [
|
|
63
|
+
f" * Generated by {tool} from project {self.project}{version}.",
|
|
64
|
+
f" * Model hash: {self.content_hash}.",
|
|
65
|
+
" * Do not edit; regenerate instead.",
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def source_text(project: Project, source: Source) -> str:
|
|
70
|
+
"""Return `file:line` of a model element, relative to the project directory."""
|
|
71
|
+
root = Path(project.source.file).parent
|
|
72
|
+
relative = Path(os.path.relpath(source.file, root)).as_posix()
|
|
73
|
+
return f"{relative}:{source.line}" if source.line is not None else relative
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _namespace(project: Project) -> str:
|
|
77
|
+
return project.namespace or project.name
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _description(text: str | None, fallback: str) -> str:
|
|
81
|
+
return " ".join((text or fallback).split())
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _type_order(project: Project) -> list[Struct]:
|
|
85
|
+
"""Types sorted so that every type comes after the types it contains."""
|
|
86
|
+
ordered: list[Struct] = []
|
|
87
|
+
seen: set[str] = set()
|
|
88
|
+
|
|
89
|
+
def visit(name: str) -> None:
|
|
90
|
+
if name in seen:
|
|
91
|
+
return
|
|
92
|
+
seen.add(name)
|
|
93
|
+
for item in project.types[name].fields:
|
|
94
|
+
if item.type in project.types:
|
|
95
|
+
visit(item.type)
|
|
96
|
+
ordered.append(project.types[name])
|
|
97
|
+
|
|
98
|
+
for name in project.types:
|
|
99
|
+
visit(name)
|
|
100
|
+
return ordered
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class _ModelHeader:
|
|
104
|
+
"""Writes model.hpp."""
|
|
105
|
+
|
|
106
|
+
def __init__(self, project: Project, banner: Banner, notes: dict[str, list[str]]) -> None:
|
|
107
|
+
self.project = project
|
|
108
|
+
self.names = Names(project)
|
|
109
|
+
self.banner = banner
|
|
110
|
+
self.notes = notes
|
|
111
|
+
self.out = Writer()
|
|
112
|
+
self.codec = CodecWriter(project, self.out)
|
|
113
|
+
|
|
114
|
+
def render(self) -> str:
|
|
115
|
+
out = self.out
|
|
116
|
+
name = self.project.name
|
|
117
|
+
out.line("/**")
|
|
118
|
+
out.line(f" * @file {name}/model.hpp")
|
|
119
|
+
out.line(f" * @brief Data types of the {name} messages: defaults, validation, codec.")
|
|
120
|
+
out.line(" *")
|
|
121
|
+
for text in self.banner.lines():
|
|
122
|
+
out.line(text)
|
|
123
|
+
out.line(" */")
|
|
124
|
+
out.line(f"#ifndef {guard(name, 'MODEL_HPP')}")
|
|
125
|
+
out.line(f"#define {guard(name, 'MODEL_HPP')}")
|
|
126
|
+
out.line()
|
|
127
|
+
out.line(f'#include "{name}/user_prelude.hpp"')
|
|
128
|
+
out.line()
|
|
129
|
+
for header in RUNTIME_HEADERS:
|
|
130
|
+
out.line(f"#include <{header}>")
|
|
131
|
+
out.line()
|
|
132
|
+
custom = sorted({c.include for c in self.project.checksums.values() if c.include})
|
|
133
|
+
for header in custom:
|
|
134
|
+
out.line(f'#include "{header}"')
|
|
135
|
+
if custom:
|
|
136
|
+
out.line()
|
|
137
|
+
for header in ("array", "cstddef", "cstdint"):
|
|
138
|
+
out.line(f"#include <{header}>")
|
|
139
|
+
out.line()
|
|
140
|
+
out.line(f"/// @brief Generated code of project {name}.")
|
|
141
|
+
out.line(f"namespace {_namespace(self.project)} {{")
|
|
142
|
+
out.line()
|
|
143
|
+
self.defines()
|
|
144
|
+
self.checksums()
|
|
145
|
+
self.enums()
|
|
146
|
+
for item in _type_order(self.project):
|
|
147
|
+
self.struct(item, "Type")
|
|
148
|
+
for item in self.project.messages.values():
|
|
149
|
+
self.struct(item, "Message")
|
|
150
|
+
out.line(f"}} // namespace {_namespace(self.project)}")
|
|
151
|
+
out.line()
|
|
152
|
+
out.line(f"#endif // {guard(name, 'MODEL_HPP')}")
|
|
153
|
+
return out.text()
|
|
154
|
+
|
|
155
|
+
# --- defines and enums ----------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
def defines(self) -> None:
|
|
158
|
+
if not self.project.defines:
|
|
159
|
+
return
|
|
160
|
+
out = self.out
|
|
161
|
+
out.line("/// @brief Model defines; each can be overridden with -D<NAME>=<value>.")
|
|
162
|
+
out.line("namespace defines {")
|
|
163
|
+
out.line()
|
|
164
|
+
for define in self.project.defines.values():
|
|
165
|
+
out.line(f"#if defined({define.name})")
|
|
166
|
+
out.line(f"inline constexpr std::int64_t k{define.name} = {define.name};")
|
|
167
|
+
out.line("#else")
|
|
168
|
+
out.line(f"/// @brief Value of define {define.name} (default {define.value}).")
|
|
169
|
+
out.line(f"inline constexpr std::int64_t k{define.name} = {define.value};")
|
|
170
|
+
out.line("#endif")
|
|
171
|
+
out.line()
|
|
172
|
+
out.line("} // namespace defines")
|
|
173
|
+
out.line()
|
|
174
|
+
|
|
175
|
+
def note(self, owner: str) -> None:
|
|
176
|
+
"""Write the requirement trace of an element (nothing without a requirements catalog)."""
|
|
177
|
+
for text in self.notes.get(owner, ()):
|
|
178
|
+
self.out.line(text)
|
|
179
|
+
|
|
180
|
+
def checksums(self) -> None:
|
|
181
|
+
if not self.project.checksums:
|
|
182
|
+
return
|
|
183
|
+
out = self.out
|
|
184
|
+
out.line("/// @brief Checksum algorithms of the model.")
|
|
185
|
+
out.line("namespace checksums {")
|
|
186
|
+
out.line()
|
|
187
|
+
for checksum in self.project.checksums.values():
|
|
188
|
+
out.line(f"/// @brief Checksum {checksum.name} ({checksum.algo}).")
|
|
189
|
+
self.note(checksum.name)
|
|
190
|
+
out.line(f"using {checksum.name} = {checksum_type(checksum)};")
|
|
191
|
+
out.line(
|
|
192
|
+
f"static_assert(wireforge::is_checksum_v<{checksum.name}>, "
|
|
193
|
+
f'"{checksum.name} must satisfy the checksum contract");'
|
|
194
|
+
)
|
|
195
|
+
out.line()
|
|
196
|
+
out.line("} // namespace checksums")
|
|
197
|
+
out.line()
|
|
198
|
+
|
|
199
|
+
def enums(self) -> None:
|
|
200
|
+
out = self.out
|
|
201
|
+
for enum in self.project.enums.values():
|
|
202
|
+
underlying = self.names.scalar(enum.type)
|
|
203
|
+
out.line(f"/// @brief Enumeration {enum.name}.")
|
|
204
|
+
self.note(enum.name)
|
|
205
|
+
out.line(f"enum class {enum.name} : {underlying} {{")
|
|
206
|
+
out.indent()
|
|
207
|
+
signed = enum.type.startswith("i")
|
|
208
|
+
for member, value in enum.values.items():
|
|
209
|
+
literal = str(value) if signed or value < 0 else f"{value}U"
|
|
210
|
+
out.line(f"{member} = {literal}, ///< {member}")
|
|
211
|
+
out.dedent()
|
|
212
|
+
out.line("};")
|
|
213
|
+
out.line()
|
|
214
|
+
out.line(f"/// @brief true when @p value is a member of {enum.name}.")
|
|
215
|
+
out.line(f"[[nodiscard]] constexpr bool is_valid({enum.name} value) noexcept {{")
|
|
216
|
+
out.indent()
|
|
217
|
+
# A comparison chain, not a switch: a switch over every member needs a default label
|
|
218
|
+
# that compilers with -Wcovered-switch-default reject.
|
|
219
|
+
terms = [f"value == {enum.name}::{member}" for member in enum.values]
|
|
220
|
+
for position, term in enumerate(terms):
|
|
221
|
+
joiner = " ||" if position < len(terms) - 1 else ";"
|
|
222
|
+
prefix = "return " if position == 0 else " "
|
|
223
|
+
out.line(f"{prefix}{term}{joiner}")
|
|
224
|
+
out.dedent()
|
|
225
|
+
out.line("}")
|
|
226
|
+
out.line()
|
|
227
|
+
|
|
228
|
+
# --- structs --------------------------------------------------------------------------------
|
|
229
|
+
|
|
230
|
+
def conditional(self, item: Field, lines: Iterable[str]) -> None:
|
|
231
|
+
lines = list(lines)
|
|
232
|
+
if not lines:
|
|
233
|
+
return
|
|
234
|
+
if item.condition is not None:
|
|
235
|
+
self.out.line(f"#if {self.names.condition(item.condition)}")
|
|
236
|
+
for text in lines:
|
|
237
|
+
self.out.line(text)
|
|
238
|
+
if item.condition is not None:
|
|
239
|
+
self.out.line("#endif")
|
|
240
|
+
|
|
241
|
+
def struct(self, item: Struct, kind: str) -> None:
|
|
242
|
+
out = self.out
|
|
243
|
+
hidden = wire_only(item)
|
|
244
|
+
if item.condition is not None:
|
|
245
|
+
out.line(f"#if {self.names.condition(item.condition)}")
|
|
246
|
+
out.line(f"/// @brief {_description(item.description, f'{kind} {item.name}.')}")
|
|
247
|
+
out.line(
|
|
248
|
+
f"/// Defined in {source_text(self.project, item.source)} ({item.source.pointer})."
|
|
249
|
+
)
|
|
250
|
+
self.note(item.name)
|
|
251
|
+
out.line(f"struct {item.name} {{")
|
|
252
|
+
out.indent()
|
|
253
|
+
for field in item.fields:
|
|
254
|
+
if field.name not in hidden:
|
|
255
|
+
self.conditional(field, [self.member(field)])
|
|
256
|
+
out.dedent()
|
|
257
|
+
out.line("};")
|
|
258
|
+
out.line()
|
|
259
|
+
self.validate(item, hidden)
|
|
260
|
+
self.sizes(item)
|
|
261
|
+
self.codec.encode_struct(item)
|
|
262
|
+
self.codec.decode_struct(item)
|
|
263
|
+
if item.message is not None:
|
|
264
|
+
self.codec.message_api(item)
|
|
265
|
+
if item.condition is not None:
|
|
266
|
+
out.line("#endif")
|
|
267
|
+
out.line()
|
|
268
|
+
|
|
269
|
+
def member(self, item: Field) -> str:
|
|
270
|
+
names = self.names
|
|
271
|
+
doc = _description(item.description, item.name)
|
|
272
|
+
if item.units:
|
|
273
|
+
doc += f" [{item.units}]"
|
|
274
|
+
doc += f" ({source_text(self.project, item.source)})"
|
|
275
|
+
return f"{names.member_type(item)} {item.name}{self.initializer(item)}; ///< {doc}"
|
|
276
|
+
|
|
277
|
+
def initializer(self, item: Field) -> str:
|
|
278
|
+
default = item.default
|
|
279
|
+
dynamic = item.occurs is not None and (
|
|
280
|
+
item.occurs.count is not None or item.occurs.fill or item.type == "char"
|
|
281
|
+
)
|
|
282
|
+
if default is None:
|
|
283
|
+
return "" if dynamic else "{}"
|
|
284
|
+
if item.occurs is None:
|
|
285
|
+
single = default if not isinstance(default, list) else None
|
|
286
|
+
return "{}" if single is None else f"{{{self.names.literal(item, single)}}}"
|
|
287
|
+
return self.array_initializer(item, default, dynamic=dynamic)
|
|
288
|
+
|
|
289
|
+
def array_initializer(
|
|
290
|
+
self, item: Field, default: Scalar | list[Scalar], *, dynamic: bool
|
|
291
|
+
) -> str:
|
|
292
|
+
names = self.names
|
|
293
|
+
if item.type == "char" and isinstance(default, str):
|
|
294
|
+
return f"{{{string_literal(default)}}}"
|
|
295
|
+
if isinstance(default, list):
|
|
296
|
+
values = ", ".join(names.literal(item, value) for value in default)
|
|
297
|
+
return f"{{{values}}}" if dynamic else f"{{{{{values}}}}}"
|
|
298
|
+
if dynamic or item.occurs is None:
|
|
299
|
+
return "" # a dynamic array starts empty; the default applies to new elements
|
|
300
|
+
element = names.scalar(item.type)
|
|
301
|
+
capacity = names.capacity(item.occurs.max)
|
|
302
|
+
return f"{{wireforge::filled<{element}, {capacity}>({names.literal(item, default)})}}"
|
|
303
|
+
|
|
304
|
+
# --- validate -------------------------------------------------------------------------------
|
|
305
|
+
|
|
306
|
+
def value_checks(self, item: Field, subject: str, members: set[str]) -> list[str]:
|
|
307
|
+
"""Return the checks of one scalar value (`subject` is its C++ expression)."""
|
|
308
|
+
names = self.names
|
|
309
|
+
checks: list[str] = []
|
|
310
|
+
if item.type in self.project.enums:
|
|
311
|
+
checks.append(f"wireforge::check_true(is_valid({subject}))")
|
|
312
|
+
base = names.base(item.type)
|
|
313
|
+
if item.bits is not None and item.bits < BUILTIN_BITS[base] and base in BUILTIN_BITS:
|
|
314
|
+
value = subject
|
|
315
|
+
if item.type in self.project.enums:
|
|
316
|
+
value = f"static_cast<{names.scalar(base)}>({subject})"
|
|
317
|
+
if base != "bool":
|
|
318
|
+
checks.append(f"wireforge::check_bits({value}, {item.bits}U)")
|
|
319
|
+
checks += self.bound_checks(item, subject, members)
|
|
320
|
+
return checks
|
|
321
|
+
|
|
322
|
+
def bound_checks(self, item: Field, subject: str, members: set[str]) -> list[str]:
|
|
323
|
+
names = self.names
|
|
324
|
+
limits = names.type_limits(item)
|
|
325
|
+
low, high = item.min, item.max
|
|
326
|
+
if limits is not None and isinstance(low, int) and low <= limits[0]:
|
|
327
|
+
low = None
|
|
328
|
+
if limits is not None and isinstance(high, int) and high >= limits[1]:
|
|
329
|
+
high = None
|
|
330
|
+
if low is not None and high is not None:
|
|
331
|
+
bounds = f"{names.bound(item, low, members)}, {names.bound(item, high, members)}"
|
|
332
|
+
return [f"wireforge::check_range({subject}, {bounds})"]
|
|
333
|
+
if low is not None:
|
|
334
|
+
return [f"wireforge::check_min({subject}, {names.bound(item, low, members)})"]
|
|
335
|
+
if high is not None:
|
|
336
|
+
return [f"wireforge::check_max({subject}, {names.bound(item, high, members)})"]
|
|
337
|
+
return []
|
|
338
|
+
|
|
339
|
+
def field_checks(self, item: Field, owner: Struct, members: set[str]) -> list[str]:
|
|
340
|
+
subject = f"value.{item.name}"
|
|
341
|
+
if item.occurs is None:
|
|
342
|
+
if self.names.is_struct(item):
|
|
343
|
+
return [f"validate({subject})"]
|
|
344
|
+
return self.value_checks(item, subject, members)
|
|
345
|
+
checks = self.count_checks(item, owner, members)
|
|
346
|
+
if self.names.is_struct(item):
|
|
347
|
+
checks.append(f"wireforge::validate_each({subject})")
|
|
348
|
+
elif item.type != "char":
|
|
349
|
+
inner = self.value_checks(item, "element", members)
|
|
350
|
+
if inner:
|
|
351
|
+
element = self.names.scalar(item.type)
|
|
352
|
+
body = (
|
|
353
|
+
inner[0] if len(inner) == 1 else f"wireforge::first_error({', '.join(inner)})"
|
|
354
|
+
)
|
|
355
|
+
checks.append(
|
|
356
|
+
f"wireforge::check_each({subject}, [](const {element}& element) "
|
|
357
|
+
f"{{ return {body}; }})"
|
|
358
|
+
)
|
|
359
|
+
return checks
|
|
360
|
+
|
|
361
|
+
def count_checks(self, item: Field, owner: Struct, members: set[str]) -> list[str]:
|
|
362
|
+
occurs = item.occurs
|
|
363
|
+
if occurs is None or (occurs.count is None and not occurs.fill):
|
|
364
|
+
return []
|
|
365
|
+
names = self.names
|
|
366
|
+
size = f"value.{item.name}.size()"
|
|
367
|
+
checks = []
|
|
368
|
+
low = occurs.min or 0
|
|
369
|
+
if low != 0:
|
|
370
|
+
bounds = f"{names.capacity(low)}, {names.capacity(occurs.max)}"
|
|
371
|
+
checks.append(f"wireforge::check_count({size}, {bounds})")
|
|
372
|
+
counter = plain_count(item)
|
|
373
|
+
if counter is None and occurs.count is not None:
|
|
374
|
+
checks.append(
|
|
375
|
+
f"wireforge::check_count_equals({size}, {names.value(occurs.count, members)})"
|
|
376
|
+
)
|
|
377
|
+
elif counter is not None and counter not in members:
|
|
378
|
+
checks += self.counter_checks(item, owner, counter, size)
|
|
379
|
+
return checks
|
|
380
|
+
|
|
381
|
+
def counter_checks(self, item: Field, owner: Struct, counter: str, size: str) -> list[str]:
|
|
382
|
+
"""Range of a wire-only count field, and equal sizes of arrays sharing it."""
|
|
383
|
+
target = next(field for field in owner.fields if field.name == counter)
|
|
384
|
+
checks = self.bound_checks(target, size, set())
|
|
385
|
+
first = next(field for field in owner.fields if plain_count(field) == counter)
|
|
386
|
+
if first.name != item.name:
|
|
387
|
+
checks.append(
|
|
388
|
+
f"wireforge::check_count_equals({size}, "
|
|
389
|
+
f"static_cast<std::int64_t>(value.{first.name}.size()))"
|
|
390
|
+
)
|
|
391
|
+
return checks
|
|
392
|
+
|
|
393
|
+
def validate(self, item: Struct, hidden: set[str]) -> None:
|
|
394
|
+
out = self.out
|
|
395
|
+
members = {field.name for field in item.fields if field.name not in hidden}
|
|
396
|
+
out.line("/// @brief Checks ranges, enum values and element counts of @p value.")
|
|
397
|
+
signature = f"constexpr wireforge::Status validate(const {item.name}& value) noexcept"
|
|
398
|
+
out.line(f"[[nodiscard]] {signature} {{")
|
|
399
|
+
out.indent()
|
|
400
|
+
blocks = [
|
|
401
|
+
(field, self.field_checks(field, item, members))
|
|
402
|
+
for field in item.fields
|
|
403
|
+
if field.name not in hidden
|
|
404
|
+
]
|
|
405
|
+
if not any(checks for _, checks in blocks):
|
|
406
|
+
out.line("static_cast<void>(value);")
|
|
407
|
+
out.line("return wireforge::Status::Ok;")
|
|
408
|
+
else:
|
|
409
|
+
out.line("return wireforge::first_error(")
|
|
410
|
+
out.indent()
|
|
411
|
+
for field, checks in blocks:
|
|
412
|
+
self.conditional(field, [f"{check}," for check in checks] if checks else [])
|
|
413
|
+
out.line("wireforge::Status::Ok);")
|
|
414
|
+
out.dedent()
|
|
415
|
+
out.dedent()
|
|
416
|
+
out.line("}")
|
|
417
|
+
out.line()
|
|
418
|
+
|
|
419
|
+
# --- wire sizes -----------------------------------------------------------------------------
|
|
420
|
+
|
|
421
|
+
def group_bits(self, item: Field) -> int | None:
|
|
422
|
+
if item.occurs is not None:
|
|
423
|
+
return None
|
|
424
|
+
if item.bits is not None:
|
|
425
|
+
return item.bits
|
|
426
|
+
inner = self.project.types.get(item.type)
|
|
427
|
+
if inner is None or not inner.fields or any(f.bits is None for f in inner.fields):
|
|
428
|
+
return None
|
|
429
|
+
return sum(f.bits or 0 for f in inner.fields)
|
|
430
|
+
|
|
431
|
+
def element_size(self, item: Field, which: str) -> str:
|
|
432
|
+
if item.type in self.project.types or item.type in self.project.user_types:
|
|
433
|
+
cpp_name = self.names.scalar(item.type)
|
|
434
|
+
return f"{which}_wire_size(wireforge::type_tag<{cpp_name}>)"
|
|
435
|
+
return f"{BUILTIN_BITS[self.names.base(item.type)] // BYTE_BITS}U"
|
|
436
|
+
|
|
437
|
+
def field_size(self, item: Field, which: str) -> str:
|
|
438
|
+
element = self.element_size(item, which)
|
|
439
|
+
occurs = item.occurs
|
|
440
|
+
if occurs is None:
|
|
441
|
+
return element
|
|
442
|
+
dynamic = occurs.count is not None or occurs.fill
|
|
443
|
+
count = occurs.max if which == "max" or not dynamic else (occurs.min or 0)
|
|
444
|
+
if count == 0:
|
|
445
|
+
return "0U"
|
|
446
|
+
if isinstance(count, int):
|
|
447
|
+
return f"std::size_t{{{count}U}} * {element}"
|
|
448
|
+
return f"{self.names.capacity(count)} * {element}"
|
|
449
|
+
|
|
450
|
+
def size_lines(self, item: Struct, which: str) -> list[tuple[Field, str]]:
|
|
451
|
+
lines: list[tuple[Field, str]] = []
|
|
452
|
+
group: list[Field] = []
|
|
453
|
+
for field in [*item.fields, None]:
|
|
454
|
+
bits = None if field is None else self.group_bits(field)
|
|
455
|
+
if bits is not None and field is not None:
|
|
456
|
+
group.append(field)
|
|
457
|
+
continue
|
|
458
|
+
if group:
|
|
459
|
+
total = sum(self.group_bits(member) or 0 for member in group)
|
|
460
|
+
lines.append((group[0], f"{(total + BYTE_BITS - 1) // BYTE_BITS}U"))
|
|
461
|
+
group = []
|
|
462
|
+
if field is not None:
|
|
463
|
+
lines.append((field, self.field_size(field, which)))
|
|
464
|
+
return lines
|
|
465
|
+
|
|
466
|
+
def sizes(self, item: Struct) -> None:
|
|
467
|
+
out = self.out
|
|
468
|
+
for which, text in (("min", "Smallest"), ("max", "Largest")):
|
|
469
|
+
out.line(f"/// @brief {text} encoded size of {item.name} in bytes.")
|
|
470
|
+
out.line(
|
|
471
|
+
f"[[nodiscard]] constexpr std::size_t {which}_wire_size("
|
|
472
|
+
f"wireforge::TypeTag<{item.name}> /*tag*/) noexcept {{"
|
|
473
|
+
)
|
|
474
|
+
out.indent()
|
|
475
|
+
out.line("std::size_t size = 0U;")
|
|
476
|
+
for field, size in self.size_lines(item, which):
|
|
477
|
+
if size != "0U":
|
|
478
|
+
self.conditional(field, [f"size += {size};"])
|
|
479
|
+
out.line("return size;")
|
|
480
|
+
out.dedent()
|
|
481
|
+
out.line("}")
|
|
482
|
+
out.line()
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def _user_prelude(project: Project, banner: Banner) -> str:
|
|
486
|
+
out = Writer()
|
|
487
|
+
name = project.name
|
|
488
|
+
names = Names(project)
|
|
489
|
+
out.line("/**")
|
|
490
|
+
out.line(f" * @file {name}/user_prelude.hpp")
|
|
491
|
+
out.line(f" * @brief Application headers and constants used by the {name} generated code.")
|
|
492
|
+
out.line(" *")
|
|
493
|
+
for text in banner.lines():
|
|
494
|
+
out.line(text)
|
|
495
|
+
out.line(" */")
|
|
496
|
+
out.line(f"#ifndef {guard(name, 'USER_PRELUDE_HPP')}")
|
|
497
|
+
out.line(f"#define {guard(name, 'USER_PRELUDE_HPP')}")
|
|
498
|
+
out.line()
|
|
499
|
+
out.line("#include <cstdint>")
|
|
500
|
+
if project.user_types:
|
|
501
|
+
out.line("#include <wireforge/validation.hpp>")
|
|
502
|
+
for header in project.headers:
|
|
503
|
+
include = f'"{header.path}"' if header.style == "quote" else f"<{header.path}>"
|
|
504
|
+
if header.condition is not None:
|
|
505
|
+
out.line(f"#if {names.condition(header.condition)}")
|
|
506
|
+
out.line(f"#include {include}")
|
|
507
|
+
if header.condition is not None:
|
|
508
|
+
out.line("#endif")
|
|
509
|
+
_user_type_support(out, project, names)
|
|
510
|
+
for constant in project.constants.values():
|
|
511
|
+
_constant_checks(out, constant)
|
|
512
|
+
out.line()
|
|
513
|
+
out.line(f"#endif // {guard(name, 'USER_PRELUDE_HPP')}")
|
|
514
|
+
return out.text()
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def _user_type_support(out: Writer, project: Project, names: Names) -> None:
|
|
518
|
+
"""Each user type's own header include, plus its generated (fixed) size free functions."""
|
|
519
|
+
for user_type in project.user_types.values():
|
|
520
|
+
if user_type.include is not None:
|
|
521
|
+
quoted = user_type.style == "quote"
|
|
522
|
+
include = f'"{user_type.include}"' if quoted else f"<{user_type.include}>"
|
|
523
|
+
out.line(f"#include {include}")
|
|
524
|
+
if not project.user_types:
|
|
525
|
+
return
|
|
526
|
+
out.line()
|
|
527
|
+
out.line("namespace wireforge {")
|
|
528
|
+
for user_type in project.user_types.values():
|
|
529
|
+
cpp_name = names.scalar(user_type.name)
|
|
530
|
+
for which, text in (("max", "Largest"), ("min", "Smallest")):
|
|
531
|
+
out.line(f"/// @brief {text} encoded size of {cpp_name} in bytes (fixed).")
|
|
532
|
+
out.line(
|
|
533
|
+
f"constexpr std::size_t {which}_wire_size(TypeTag<{cpp_name}> /*tag*/) "
|
|
534
|
+
f"noexcept {{ return {user_type.size_bytes}U; }}"
|
|
535
|
+
)
|
|
536
|
+
out.line("} // namespace wireforge")
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def _constant_checks(out: Writer, constant: UserConstant) -> None:
|
|
540
|
+
"""static_assert that the application constant matches the model (per profile)."""
|
|
541
|
+
branches = [
|
|
542
|
+
(f"defined(WIREFORGE_PROFILE_{profile})", value)
|
|
543
|
+
for profile, value in constant.profile_values.items()
|
|
544
|
+
]
|
|
545
|
+
if not branches and constant.value is None:
|
|
546
|
+
return
|
|
547
|
+
out.line()
|
|
548
|
+
for index, (condition, value) in enumerate(branches):
|
|
549
|
+
out.line(f"#{'if' if index == 0 else 'elif'} {condition}")
|
|
550
|
+
out.line(_constant_assert(constant.cpp, value))
|
|
551
|
+
if constant.value is not None:
|
|
552
|
+
if branches:
|
|
553
|
+
out.line("#else")
|
|
554
|
+
out.line(_constant_assert(constant.cpp, constant.value))
|
|
555
|
+
if branches:
|
|
556
|
+
out.line("#endif")
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def _constant_assert(cpp: str, value: int) -> str:
|
|
560
|
+
return (
|
|
561
|
+
f"static_assert(static_cast<std::int64_t>({cpp}) == {value}, "
|
|
562
|
+
f'"{cpp} differs from the value in the WireForge model");'
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def render(project: Project, content_hash: str) -> dict[str, str]:
|
|
567
|
+
"""Return the generated files of a valid project: relative path -> content."""
|
|
568
|
+
banner = Banner(project.name, project.version, content_hash)
|
|
569
|
+
notes = requirement_notes(project)
|
|
570
|
+
files = {
|
|
571
|
+
f"{project.name}/user_prelude.hpp": _user_prelude(project, banner),
|
|
572
|
+
f"{project.name}/model.hpp": _ModelHeader(project, banner, notes).render(),
|
|
573
|
+
}
|
|
574
|
+
for interface in project.interfaces.values():
|
|
575
|
+
relative = f"{project.name}/interfaces/{interface.name}.hpp"
|
|
576
|
+
files[relative] = InterfaceHeader(
|
|
577
|
+
project, interface, banner, notes.get(interface.name, [])
|
|
578
|
+
).render()
|
|
579
|
+
return files
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def generate(
|
|
583
|
+
location: Path, output: Path, tests: test_project.TestOptions | None = None
|
|
584
|
+
) -> list[Issue]:
|
|
585
|
+
"""Validate the project at `location` and write its generated files below `output`.
|
|
586
|
+
|
|
587
|
+
With `tests`, the GoogleTest project `<output>/<project>/generated_tests/` is written as well.
|
|
588
|
+
"""
|
|
589
|
+
try:
|
|
590
|
+
content_hash = bundle.build_bundle(location)["bundle"]["content_hash"]
|
|
591
|
+
except bundle.BundleError as error:
|
|
592
|
+
return error.issues or [Issue(str(location), None, "", str(error))]
|
|
593
|
+
result = load_project(location)
|
|
594
|
+
if result.project is None:
|
|
595
|
+
return result.issues
|
|
596
|
+
try:
|
|
597
|
+
files = render(result.project, content_hash)
|
|
598
|
+
except InterfaceError as error:
|
|
599
|
+
return [Issue(str(location), None, "/interfaces", str(error))]
|
|
600
|
+
for relative, text in files.items():
|
|
601
|
+
target = output / relative
|
|
602
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
603
|
+
if not target.exists() or target.read_text(encoding="utf-8") != text:
|
|
604
|
+
target.write_text(text, encoding="utf-8")
|
|
605
|
+
if tests is None:
|
|
606
|
+
return []
|
|
607
|
+
return test_project.write_test_project(result.project, location, output, tests)
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def main(argv: list[str] | None = None) -> int:
|
|
611
|
+
"""Command line: python -m wireforge.generate PROJECT -o OUTPUT [--tests ...]."""
|
|
612
|
+
parser = argparse.ArgumentParser(prog="python -m wireforge.generate")
|
|
613
|
+
parser.add_argument("project", type=Path)
|
|
614
|
+
parser.add_argument("-o", "--output", type=Path, required=True)
|
|
615
|
+
parser.add_argument(
|
|
616
|
+
"--tests", action="store_true", help="also write the GoogleTest project generated_tests/"
|
|
617
|
+
)
|
|
618
|
+
parser.add_argument("--plugins", type=Path, help="Python file defining checksum PLUGINS")
|
|
619
|
+
parser.add_argument(
|
|
620
|
+
"--runtime",
|
|
621
|
+
type=Path,
|
|
622
|
+
default=test_project.RUNTIME_INCLUDE,
|
|
623
|
+
help="WireForge runtime include directory used by the test project",
|
|
624
|
+
)
|
|
625
|
+
parser.add_argument(
|
|
626
|
+
"--coverage-threshold",
|
|
627
|
+
type=int,
|
|
628
|
+
default=test_project.DEFAULT_COVERAGE,
|
|
629
|
+
help="minimum line coverage of the test project's coverage target (percent)",
|
|
630
|
+
)
|
|
631
|
+
args = parser.parse_args(argv)
|
|
632
|
+
options = (
|
|
633
|
+
test_project.TestOptions(args.plugins, args.runtime, args.coverage_threshold)
|
|
634
|
+
if args.tests
|
|
635
|
+
else None
|
|
636
|
+
)
|
|
637
|
+
issues = generate(args.project, args.output, options)
|
|
638
|
+
for issue in issues:
|
|
639
|
+
sys.stdout.write(f"{issue}\n")
|
|
640
|
+
return 1 if issues else 0
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
if __name__ == "__main__":
|
|
644
|
+
sys.exit(main())
|