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.
Files changed (96) hide show
  1. wireforge/__init__.py +3 -0
  2. wireforge/_data/cmake/WireForgeConfig.cmake +29 -0
  3. wireforge/_data/cmake/WireForgeConfigVersion.cmake +15 -0
  4. wireforge/_data/cmake/WireForgeGenerate.cmake +64 -0
  5. wireforge/_data/runtime/LICENSE +21 -0
  6. wireforge/_data/runtime/include/wireforge/array_access.hpp +35 -0
  7. wireforge/_data/runtime/include/wireforge/bit_utils.hpp +226 -0
  8. wireforge/_data/runtime/include/wireforge/byte_io.hpp +387 -0
  9. wireforge/_data/runtime/include/wireforge/checksum.hpp +201 -0
  10. wireforge/_data/runtime/include/wireforge/codec_support.hpp +412 -0
  11. wireforge/_data/runtime/include/wireforge/config.hpp +41 -0
  12. wireforge/_data/runtime/include/wireforge/delegate.hpp +101 -0
  13. wireforge/_data/runtime/include/wireforge/dispatcher.hpp +339 -0
  14. wireforge/_data/runtime/include/wireforge/expr_support.hpp +38 -0
  15. wireforge/_data/runtime/include/wireforge/frame_decoder.hpp +262 -0
  16. wireforge/_data/runtime/include/wireforge/frame_layout.hpp +20 -0
  17. wireforge/_data/runtime/include/wireforge/link.hpp +223 -0
  18. wireforge/_data/runtime/include/wireforge/lock.hpp +146 -0
  19. wireforge/_data/runtime/include/wireforge/ring_buffer.hpp +226 -0
  20. wireforge/_data/runtime/include/wireforge/span.hpp +116 -0
  21. wireforge/_data/runtime/include/wireforge/static_vector.hpp +264 -0
  22. wireforge/_data/runtime/include/wireforge/status.hpp +75 -0
  23. wireforge/_data/runtime/include/wireforge/transport.hpp +95 -0
  24. wireforge/_data/runtime/include/wireforge/validation.hpp +153 -0
  25. wireforge/_data/runtime/include/wireforge/version.hpp +22 -0
  26. wireforge/_data/runtime/requirements.yaml +438 -0
  27. wireforge/_data/schema/deviations.schema.json +47 -0
  28. wireforge/_data/schema/fragment.schema.json +79 -0
  29. wireforge/_data/schema/llr_ids.schema.json +25 -0
  30. wireforge/_data/schema/project.schema.json +1240 -0
  31. wireforge/_data/schema/runtime_requirements.schema.json +34 -0
  32. wireforge/_data/ui/assets/index-6Pomu6U4.js +44 -0
  33. wireforge/_data/ui/assets/index-GVdDyUAG.css +1 -0
  34. wireforge/_data/ui/index.html +13 -0
  35. wireforge/bundle.py +177 -0
  36. wireforge/ci_check.py +155 -0
  37. wireforge/cli.py +617 -0
  38. wireforge/codec.py +897 -0
  39. wireforge/compat.py +536 -0
  40. wireforge/coverage_points.py +64 -0
  41. wireforge/cpp.py +273 -0
  42. wireforge/cpp_codec.py +440 -0
  43. wireforge/cpp_interface.py +874 -0
  44. wireforge/cpp_tests.py +1993 -0
  45. wireforge/differential.py +586 -0
  46. wireforge/docs_markdown.py +72 -0
  47. wireforge/expr.py +598 -0
  48. wireforge/findings.py +187 -0
  49. wireforge/fmt.py +351 -0
  50. wireforge/generate.py +644 -0
  51. wireforge/golden.py +253 -0
  52. wireforge/hlr_import.py +112 -0
  53. wireforge/impact.py +249 -0
  54. wireforge/importers.py +402 -0
  55. wireforge/issues.py +27 -0
  56. wireforge/layout.py +681 -0
  57. wireforge/lifecycle.py +1266 -0
  58. wireforge/lifecycle_results.py +205 -0
  59. wireforge/lifecycle_rules.py +78 -0
  60. wireforge/lifecycle_runtime.py +375 -0
  61. wireforge/llr.py +693 -0
  62. wireforge/llr_ids.py +104 -0
  63. wireforge/loader.py +422 -0
  64. wireforge/mcp_server.py +357 -0
  65. wireforge/model.py +322 -0
  66. wireforge/plain.py +15 -0
  67. wireforge/py.typed +0 -0
  68. wireforge/reqif.py +289 -0
  69. wireforge/resources.py +43 -0
  70. wireforge/results.py +377 -0
  71. wireforge/runtime_reqs.py +147 -0
  72. wireforge/schema.py +151 -0
  73. wireforge/server/__init__.py +1 -0
  74. wireforge/server/app.py +619 -0
  75. wireforge/server/oidc.py +99 -0
  76. wireforge/server/storage.py +146 -0
  77. wireforge/service.py +1354 -0
  78. wireforge/templates/command/project.yaml +38 -0
  79. wireforge/templates/raw/project.yaml +28 -0
  80. wireforge/templates/telemetry/project.yaml +51 -0
  81. wireforge/test_cases.py +475 -0
  82. wireforge/test_project.py +478 -0
  83. wireforge/trace.py +442 -0
  84. wireforge/validate.py +1241 -0
  85. wireforge/vcs.py +44 -0
  86. wireforge/vectors.py +222 -0
  87. wireforge/versioning.py +137 -0
  88. wireforge/wireshark.py +792 -0
  89. wireforge/wireshark_runtime.lua +725 -0
  90. wireforge-0.1.0.dist-info/METADATA +91 -0
  91. wireforge-0.1.0.dist-info/RECORD +96 -0
  92. wireforge-0.1.0.dist-info/WHEEL +5 -0
  93. wireforge-0.1.0.dist-info/entry_points.txt +2 -0
  94. wireforge-0.1.0.dist-info/licenses/LICENSE +98 -0
  95. wireforge-0.1.0.dist-info/licenses/runtime/LICENSE +21 -0
  96. 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())