CIRCT 23.0.0git
Loading...
Searching...
No Matches
codegen.py
Go to the documentation of this file.
1# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
2# See https://llvm.org/LICENSE.txt for license information.
3# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
4"""Code generation from ESI manifests to source code.
5
6Uses a two-pass approach for C++: first collect and name all reachable types,
7then emit structs/aliases in a dependency-ordered sequence so headers are
8standalone and deterministic.
9"""
10
11# C++ header support included with the runtime, though it is intended to be
12# extensible for other languages.
13
14from typing import Dict, List, Set, TextIO, Tuple, Type, Optional
15from .accelerator import AcceleratorConnection, Context
16from .esiCppAccel import ModuleInfo
17from . import types
18from .types import (BundlePort as _BundlePort, FunctionPort as _FunctionPort,
19 CallbackPort as _CallbackPort, ToHostPort as _ToHostPort,
20 FromHostPort as _FromHostPort, MMIORegion as
21 _MMIORegionPort, MetricPort as _MetricPort)
22
23import sys
24import os
25import textwrap
26import argparse
27from dataclasses import dataclass, field as _dc_field
28from pathlib import Path
29
30_thisdir = Path(__file__).absolute().resolve().parent
31
32
33@dataclass
35 """All strings needed to emit one port slot (member + ctor + find) in Connected."""
36 struct_decls: List[str] = _dc_field(default_factory=list)
37 member_decl: str = ""
38 ctor_params: List[str] = _dc_field(default_factory=list)
39 init_entry: str = ""
40 find_code: str = ""
41 make_unique_args: List[str] = _dc_field(default_factory=list)
42 post_connect: str = ""
43 using_aliases: List[Tuple[str, str]] = _dc_field(default_factory=list)
44
45
47 """Base class for all generators."""
48
49 language: Optional[str] = None
50
51 def __init__(self, conn: AcceleratorConnection):
52 self.manifest = conn.manifest()
53
54 def generate(self, output_dir: Path, system_name: str):
55 raise NotImplementedError("Generator.generate() must be overridden")
56
57
59 """Generate C++ headers from an ESI manifest."""
60
61 language = "C++"
62
63 def __init__(self, conn: AcceleratorConnection):
64 super().__init__(conn)
65 self._conn = conn
66 self.type_planner = CppTypePlanner(self.manifest.type_table)
68
69 def get_consts_str(self, module_info: ModuleInfo) -> str:
70 """Get the C++ code for a constant in a module."""
71 const_strs: List[str] = [
72 f"static constexpr {self.type_emitter.type_identifier(const.type)} "
73 f"{name} = 0x{const.value:x};"
74 for name, const in module_info.constants.items()
75 ]
76 return "\n".join(const_strs)
77
78 # ---------------------------------------------------------------------------
79 # Port-emission helpers
80 # ---------------------------------------------------------------------------
81
82 @staticmethod
83 def _sanitize_id(name: str) -> str:
84 """Return a C++-safe identifier from an AppID name."""
85 result = []
86 for ch in name:
87 result.append(ch if (ch.isalnum() or ch == "_") else "_")
88 if not result:
89 return "_port"
90 if result[0].isdigit():
91 result.insert(0, "_")
92 return "".join(result)
93
94 def _build_module_instance_map(self) -> Dict[str, object]:
95 """Walk the live hierarchy and return {module_name: first Instance}."""
96 accel = self._conn.build_accelerator()
97 result: Dict[str, object] = {}
98 queue = list(accel.children.values())
99 while queue:
100 inst = queue.pop(0)
101 info = inst.cpp_hwmodule.info
102 if info is not None:
103 name = info.name
104 if name is not None and name not in result:
105 result[name] = inst
106 queue.extend(inst.children.values())
107 return result
108
109 def _cpp_member_type(self, port, alias_prefix: Optional[str] = None) -> str:
110 """Return the C++ member type string for a port (no member name).
111
112 For typed ports (function/callback/to-host/from-host channels) `alias_prefix`
113 is required; the returned template parameters are written using the alias
114 names (`<prefix>Args`, `<prefix>Result`, `<prefix>Data`) that should be
115 emitted at module-class scope via `_port_using_aliases`. For non-typed
116 ports (MMIO regions, telemetry metrics, plain bundles) `alias_prefix` is
117 ignored and the runtime reference/pointer type is returned directly.
118 """
119 if isinstance(port, _FunctionPort):
120 assert alias_prefix is not None, (
121 "alias_prefix is required for FunctionPort to avoid emitting "
122 "long mangled type names inline (which would also collide across "
123 "modules' `using` declarations)")
124 return (f"esi::TypedFunction<{alias_prefix}Args, "
125 f"{alias_prefix}Result>")
126 if isinstance(port, _CallbackPort):
127 assert alias_prefix is not None, (
128 "alias_prefix is required for CallbackPort")
129 return (f"esi::TypedCallback<{alias_prefix}Args, "
130 f"{alias_prefix}Result>")
131 if isinstance(port, _ToHostPort):
132 assert alias_prefix is not None, (
133 "alias_prefix is required for ToHostPort")
134 return f"esi::TypedReadPort<{alias_prefix}Data>"
135 if isinstance(port, _FromHostPort):
136 assert alias_prefix is not None, (
137 "alias_prefix is required for FromHostPort")
138 return f"esi::TypedWritePort<{alias_prefix}Data>"
139 if isinstance(port, _MMIORegionPort):
140 return "esi::services::MMIO::MMIORegion &"
141 if isinstance(port, _MetricPort):
142 return "esi::services::TelemetryService::Metric &"
143 return "esi::BundlePort &"
144
145 def _port_using_aliases(self, alias_prefix: str,
146 port) -> List[Tuple[str, str]]:
147 """Return (alias_name, type_id) pairs to emit as `using` declarations at
148 module-class scope for the typed-port template parameters."""
149 if isinstance(port, (_FunctionPort, _CallbackPort)):
150 arg = self.type_emitter.type_identifier(port.arg_window_type or
151 port.arg_type)
152 res = self.type_emitter.type_identifier(port.result_window_type or
153 port.result_type)
154 return [(f"{alias_prefix}Args", arg), (f"{alias_prefix}Result", res)]
155 if isinstance(port, (_ToHostPort, _FromHostPort)):
156 data = self.type_emitter.type_identifier(port.data_window_type or
157 port.data_type)
158 return [(f"{alias_prefix}Data", data)]
159 return []
160
162 port,
163 alias_prefix: Optional[str] = None) -> str:
164 """Return the storage type used inside an `IndexedPorts<T>` for `port`.
165
166 Typed ports use the same `TypedFunction<...>` / `TypedReadPort<...>` etc.
167 that `_cpp_member_type` produces. MMIO regions, telemetry metrics, and
168 plain bundle ports are stored as raw pointers because `std::map<int, T&>`
169 is ill-formed.
170 """
171 if isinstance(port, _MMIORegionPort):
172 return "esi::services::MMIO::MMIORegion *"
173 if isinstance(port, _MetricPort):
174 return "esi::services::TelemetryService::Metric *"
175 if isinstance(port,
176 (_FunctionPort, _CallbackPort, _ToHostPort, _FromHostPort)):
177 return self._cpp_member_type(port, alias_prefix=alias_prefix)
178 return "esi::BundlePort *"
179
180 def _cpp_ctor_param_type(self, port) -> str:
181 """Return the C++ constructor parameter type for a port."""
182 if isinstance(port, _FunctionPort):
183 return "esi::services::FuncService::Function *"
184 if isinstance(port, _CallbackPort):
185 return "esi::services::CallService::Callback *"
186 if isinstance(port, _ToHostPort):
187 return "esi::ReadChannelPort &"
188 if isinstance(port, _FromHostPort):
189 return "esi::WriteChannelPort &"
190 if isinstance(port, _MMIORegionPort):
191 return "esi::services::MMIO::MMIORegion &"
192 if isinstance(port, _MetricPort):
193 return "esi::services::TelemetryService::Metric &"
194 return "esi::BundlePort &"
195
196 @staticmethod
197 def _cpp_ctor_param_suffix(port) -> str:
198 """Return the parameter name suffix ('_chan', '_svc', or '_port')."""
199 if isinstance(port, (_ToHostPort, _FromHostPort)):
200 return "_chan"
201 if isinstance(port, (_MMIORegionPort, _MetricPort)):
202 return "_svc"
203 return "_port"
204
205 @staticmethod
206 def _appid_expr(appid) -> str:
207 """Return `esi::AppID(...)` expression for an AppID."""
208 name = appid.name
209 idx = appid.idx
210 if idx is None:
211 return f'esi::AppID("{name}")'
212 return f'esi::AppID("{name}", {idx})'
213
214 def _port_find_code(self, member_name: str, port, appid) -> str:
215 """Return the code snippet that resolves a scalar port in connect()."""
216 ae = self._appid_expr(appid)
217 if isinstance(port, _FunctionPort):
218 v = f"{member_name}_port"
219 return (
220 f"auto *{v} =\n"
221 f" esi::findPortAsOrThrow<esi::services::FuncService::Function>(\n"
222 f" rawModule, {ae});")
223 if isinstance(port, _CallbackPort):
224 v = f"{member_name}_port"
225 return (
226 f"auto *{v} =\n"
227 f" esi::findPortAsOrThrow<esi::services::CallService::Callback>(\n"
228 f" rawModule, {ae});")
229 if isinstance(port, _ToHostPort):
230 v = f"{member_name}_chan"
231 return (
232 f"auto &{v} =\n"
233 f" esi::findPortAsOrThrow<esi::services::ChannelService::ToHost>(\n"
234 f' rawModule, {ae})->getRawRead("data");')
235 if isinstance(port, _FromHostPort):
236 v = f"{member_name}_chan"
237 return (
238 f"auto &{v} =\n"
239 f" esi::findPortAsOrThrow<esi::services::ChannelService::FromHost>(\n"
240 f' rawModule, {ae})->getRawWrite("data");')
241 if isinstance(port, _MMIORegionPort):
242 v = f"{member_name}_svc"
243 return (f"auto &{v} =\n"
244 f" *esi::findPortAsOrThrow<esi::services::MMIO::MMIORegion>(\n"
245 f" rawModule, {ae});")
246 if isinstance(port, _MetricPort):
247 v = f"{member_name}_svc"
248 return (
249 f"auto &{v} =\n"
250 f" *esi::findPortAsOrThrow<esi::services::TelemetryService::Metric>(\n"
251 f" rawModule, {ae});")
252 # plain BundlePort fallback
253 v = f"{member_name}_port"
254 return f"auto &{v} = esi::findPortOrThrow(rawModule, {ae});"
255
256 @staticmethod
257 def _port_make_unique_arg(member_name: str, port) -> str:
258 """Return the argument expression for make_unique<Connected>(...)."""
259 if isinstance(port, (_ToHostPort, _FromHostPort)):
260 return f"{member_name}_chan"
261 if isinstance(port, (_MMIORegionPort, _MetricPort)):
262 return f"{member_name}_svc"
263 return f"{member_name}_port"
264
265 @staticmethod
266 def _port_is_connectable(port) -> bool:
267 """True if the generated connect() should call .connect() on this port.
268
269 CallbackPort.connect() requires a user-supplied callback — skip.
270 MMIORegion, BundlePort — no .connect() method."""
271 return isinstance(port,
272 (_FunctionPort, _ToHostPort, _FromHostPort, _MetricPort))
273
274 def _scalar_port_group(self, member_name: str, port, appid) -> _PortGroup:
275 """Build a _PortGroup for a single scalar (non-indexed) port."""
276 aliases = self._port_using_aliases(member_name, port)
277 alias_prefix = member_name if aliases else None
278 member_type = self._cpp_member_type(port, alias_prefix=alias_prefix)
279 is_ref = member_type.endswith(" &")
280 param_type = self._cpp_ctor_param_type(port)
281 param_suffix = self._cpp_ctor_param_suffix(port)
282 param_name = f"{member_name}{param_suffix}"
283
284 if is_ref:
285 member_decl = f"{member_type}{member_name};"
286 else:
287 member_decl = f"{member_type} {member_name};"
288
289 post = ""
290 if self._port_is_connectable(port):
291 post = f"connected->{member_name}.connect();"
292
293 return _PortGroup(
294 member_decl=member_decl,
295 ctor_params=[f"{param_type} {param_name}"],
296 init_entry=f"{member_name}({param_name})",
297 find_code=self._port_find_code(member_name, port, appid),
298 make_unique_args=[self._port_make_unique_arg(member_name, port)],
299 post_connect=post,
300 using_aliases=aliases,
301 )
302
303 def _indexed_ports_group(self, member_name: str, appid_name: str,
304 port_list) -> _PortGroup:
305 """Build a _PortGroup for a same-name, same-type indexed port array."""
306 # Derive the element type from the first port.
307 first_port = port_list[0][1]
308 aliases = self._port_using_aliases(member_name, first_port)
309 alias_prefix = member_name if aliases else None
310 elem_type = self._cpp_indexed_elem_type(first_port,
311 alias_prefix=alias_prefix)
312 indexed_type = f"esi::IndexedPorts<{elem_type}>"
313 map_var = f"{member_name}_backing"
314 map_type = f"std::map<int, {elem_type}>"
315 indexed_var = f"{member_name}_map"
316
317 # Build the find code: per-index resolve and try_emplace, then freeze
318 # into the IndexedPorts wrapper. The body of the loop differs by port
319 # kind: channel ports need an extra `getRawRead("data")` /
320 # `getRawWrite("data")` step, MMIO regions and metrics store raw
321 # pointers.
322 find_parts = [
323 f"{map_type} {map_var};",
324 f"for (uint32_t idx : esi::findPortIndices(rawModule, "
325 f"\"{appid_name}\")) {{",
326 ]
327 appid_expr = f'esi::AppID("{appid_name}", idx)'
328 if isinstance(first_port, _FunctionPort):
329 find_parts.append(
330 f" {map_var}.try_emplace(\n"
331 f" static_cast<int>(idx),\n"
332 f" esi::findPortAsOrThrow<esi::services::FuncService::Function>"
333 f"(\n"
334 f" rawModule, {appid_expr}));")
335 elif isinstance(first_port, _CallbackPort):
336 find_parts.append(
337 f" {map_var}.try_emplace(\n"
338 f" static_cast<int>(idx),\n"
339 f" esi::findPortAsOrThrow<esi::services::CallService::Callback>"
340 f"(\n"
341 f" rawModule, {appid_expr}));")
342 elif isinstance(first_port, _ToHostPort):
343 # TypedReadPort takes a ReadChannelPort&, not the service port. Resolve
344 # the service port first, then bind its underlying raw read channel.
345 find_parts.append(
346 f" auto *svc =\n"
347 f" esi::findPortAsOrThrow<esi::services::ChannelService::ToHost>"
348 f"(\n"
349 f" rawModule, {appid_expr});\n"
350 f" {map_var}.try_emplace(\n"
351 f" static_cast<int>(idx),\n"
352 f" svc->getRawRead(\"data\"));")
353 elif isinstance(first_port, _FromHostPort):
354 find_parts.append(
355 f" auto *svc =\n"
356 f" esi::findPortAsOrThrow<esi::services::ChannelService::"
357 f"FromHost>(\n"
358 f" rawModule, {appid_expr});\n"
359 f" {map_var}.try_emplace(\n"
360 f" static_cast<int>(idx),\n"
361 f" svc->getRawWrite(\"data\"));")
362 elif isinstance(first_port, _MMIORegionPort):
363 find_parts.append(
364 f" {map_var}.try_emplace(\n"
365 f" static_cast<int>(idx),\n"
366 f" esi::findPortAsOrThrow<esi::services::MMIO::MMIORegion>(\n"
367 f" rawModule, {appid_expr}));")
368 elif isinstance(first_port, _MetricPort):
369 find_parts.append(
370 f" {map_var}.try_emplace(\n"
371 f" static_cast<int>(idx),\n"
372 f" esi::findPortAsOrThrow<esi::services::TelemetryService::"
373 f"Metric>(\n"
374 f" rawModule, {appid_expr}));")
375 else:
376 # Plain BundlePort: any service port that doesn't match a standard
377 # specialization (e.g. a custom `@esi.ServiceDecl`-defined service).
378 find_parts.append(
379 f" {map_var}.try_emplace(\n"
380 f" static_cast<int>(idx),\n"
381 f" &esi::findPortOrThrow(rawModule, {appid_expr}));")
382 find_parts.append("}")
383 find_parts.append(f"{indexed_type} {indexed_var}(std::move({map_var}));")
384
385 post = ""
386 if self._port_is_connectable(first_port):
387 # IndexedPorts now exposes mutable iteration so `port.connect()` is fine.
388 post = (f"for (auto &[idx, port] : connected->{member_name})\n"
389 f" port.connect();")
390
391 return _PortGroup(
392 member_decl=f"{indexed_type} {member_name};",
393 ctor_params=[f"{indexed_type} {indexed_var}"],
394 init_entry=f"{member_name}(std::move({indexed_var}))",
395 find_code="\n".join(find_parts),
396 make_unique_args=[f"std::move({indexed_var})"],
397 post_connect=post,
398 using_aliases=aliases,
399 )
400
401 def _mixed_struct_group(self, member_name: str, appid_name: str,
402 port_list) -> _PortGroup:
403 """Build a _PortGroup for a same-name, mixed-type indexed port group."""
404 struct_name = f"{member_name}_ports"
405 sub_member_decls: List[str] = []
406 ctor_params: List[str] = []
407 init_args: List[str] = []
408 find_parts: List[str] = []
409 make_args: List[str] = []
410 post_parts: List[str] = []
411 using_aliases: List[Tuple[str, str]] = []
412
413 for appid, port in port_list:
414 idx = appid.idx if appid.idx is not None else 0
415 sub_name = f"_{idx}"
416 sub_alias_prefix = f"{member_name}_{idx}"
417 sub_aliases = self._port_using_aliases(sub_alias_prefix, port)
418 using_aliases.extend(sub_aliases)
419 alias_prefix = sub_alias_prefix if sub_aliases else None
420 member_type = self._cpp_member_type(port, alias_prefix=alias_prefix)
421 is_ref = member_type.endswith(" &")
422 if is_ref:
423 sub_member_decls.append(f"{member_type}{sub_name};")
424 else:
425 sub_member_decls.append(f"{member_type} {sub_name};")
426
427 param_type = self._cpp_ctor_param_type(port)
428 param_suffix = self._cpp_ctor_param_suffix(port)
429 param_name = f"{member_name}_{idx}{param_suffix}"
430 ctor_params.append(f"{param_type} {param_name}")
431 init_args.append(param_name)
432 find_parts.append(self._port_find_code(param_name, port, appid))
433 make_args.append(self._port_make_unique_arg(param_name, port))
434 if self._port_is_connectable(port):
435 post_parts.append(f"connected->{member_name}.{sub_name}.connect();")
436
437 struct_decl = (f"struct {struct_name} {{\n" +
438 "".join(f" {d}\n" for d in sub_member_decls) + " };")
439 init_entry = f"{member_name}{{{', '.join(init_args)}}}"
440
441 return _PortGroup(
442 struct_decls=[struct_decl],
443 member_decl=f"{struct_name} {member_name};",
444 ctor_params=ctor_params,
445 init_entry=init_entry,
446 find_code="\n".join(find_parts),
447 make_unique_args=make_args,
448 post_connect="\n".join(post_parts),
449 using_aliases=using_aliases,
450 )
451
452 def _collect_port_groups(self, ports: dict) -> List[_PortGroup]:
453 """Group `ports` (AppID → BundlePort) into _PortGroup list."""
454 # Group by AppID name, preserving sorted order.
455 groups_by_name: Dict[str, list] = {}
456 for appid, port in ports.items():
457 n = appid.name
458 if n not in groups_by_name:
459 groups_by_name[n] = []
460 groups_by_name[n].append((appid, port))
461
462 result: List[_PortGroup] = []
463 for appid_name, port_list in groups_by_name.items():
464 member_name = self._sanitize_id(appid_name)
465 # Sort by idx (None → -1 so scalar ports sort first).
466 port_list.sort(key=lambda x: x[0].idx if x[0].idx is not None else -1)
467
468 if len(port_list) == 1:
469 appid, port = port_list[0]
470 if appid.idx is None:
471 result.append(self._scalar_port_group(member_name, port, appid))
472 else:
473 result.append(
474 self._indexed_ports_group(member_name, appid_name, port_list))
475 continue
476
477 # Multiple ports with the same name.
478 all_indexed = all(a.idx is not None for a, _ in port_list)
479 if not all_indexed:
480 # Degenerate: mix of indexed and non-indexed with the same name.
481 # Emit as a mixed struct for safety.
482 result.append(
483 self._mixed_struct_group(member_name, appid_name, port_list))
484 continue
485
486 all_same_type = len({type(p) for _, p in port_list}) == 1
487 if all_same_type:
488 result.append(
489 self._indexed_ports_group(member_name, appid_name, port_list))
490 else:
491 result.append(
492 self._mixed_struct_group(member_name, appid_name, port_list))
493
494 return result
495
496 def _emit_module_class(self, name: str, system_name: str,
497 module_info: ModuleInfo, port_groups: List[_PortGroup],
498 out: TextIO) -> None:
499 """Emit the full module class to `out`."""
500 out.write(f"/// Generated header for {system_name} module {name}.\n"
501 "#pragma once\n"
502 '#include "types.h"\n'
503 '#include "esi/TypedPorts.h"\n'
504 "\n"
505 "#include <any>\n"
506 "#include <map>\n"
507 "#include <optional>\n"
508 "#include <string>\n"
509 "\n"
510 f"namespace {system_name} {{\n"
511 "\n")
512
513 # Module metadata as a Doxygen comment block above the class.
514 metadata_lines: List[str] = []
515 summary = getattr(module_info, "summary", None)
516 if summary:
517 for line in summary.splitlines():
518 metadata_lines.append(line)
519 for label, attr in (("Version", "version"), ("Repository", "repo"),
520 ("Commit", "commit_hash")):
521 val = getattr(module_info, attr, None)
522 if val:
523 metadata_lines.append(f"{label}: {val}")
524 if metadata_lines:
525 out.write("///\n")
526 for line in metadata_lines:
527 out.write(f"/// {line}\n" if line else "///\n")
528 out.write("///\n")
529
530 out.write(f"class {name} {{\n"
531 "public:\n")
532
533 consts = self.get_consts_str(module_info)
534 if consts:
535 out.write(" // Module constants.\n")
536 out.write(f" {consts}\n\n")
537
538 # Type aliases for typed-port template parameters, hoisted to module scope
539 # so the long mangled names don't appear inline as template arguments.
540 aliases = [a for grp in port_groups for a in grp.using_aliases]
541 if aliases:
542 for alias_name, alias_type in aliases:
543 out.write(f" using {alias_name} = {alias_type};\n")
544 out.write("\n")
545
546 if port_groups:
547 out.write(
548 " /// Holds the resolved, typed ports for this module instance.\n"
549 " /// Returned by `connect()`.\n"
550 " class Connected {\n public:\n")
551
552 # Struct declarations for mixed groups.
553 for grp in port_groups:
554 for decl in grp.struct_decls:
555 out.write(f" {decl}\n")
556 if any(grp.struct_decls for grp in port_groups):
557 out.write("\n")
558
559 # Member declarations.
560 for grp in port_groups:
561 out.write(f" {grp.member_decl}\n")
562 out.write("\n")
563
564 # Constructor.
565 all_params = [p for grp in port_groups for p in grp.ctor_params]
566 out.write(" Connected(\n")
567 for i, param in enumerate(all_params):
568 comma = "," if i < len(all_params) - 1 else ""
569 out.write(f" {param}{comma}\n")
570 out.write(" )\n : ")
571 inits = [grp.init_entry for grp in port_groups]
572 out.write(",\n ".join(inits))
573 out.write(" {}\n };\n\n")
574
575 # Outer constructor.
576 out.write(
577 f" {name}(esi::HWModule *rawModule) : rawModule(rawModule) {{}}\n\n")
578
579 # Module-metadata accessors. These read from the live HWModule's ModuleInfo
580 # so callers can verify that the connected accelerator is compatible with
581 # the build the software was generated against.
582 out.write(
583 " /// The connected module's name as reported by the manifest, or\n"
584 " /// std::nullopt if the module has no metadata.\n"
585 " std::optional<std::string> name() const {\n"
586 " auto info = rawModule->getInfo();\n"
587 " return info ? info->name : std::nullopt;\n"
588 " }\n"
589 " /// The connected module's summary string, if any.\n"
590 " std::optional<std::string> summary() const {\n"
591 " auto info = rawModule->getInfo();\n"
592 " return info ? info->summary : std::nullopt;\n"
593 " }\n"
594 " /// The connected module's version string, if any.\n"
595 " std::optional<std::string> version() const {\n"
596 " auto info = rawModule->getInfo();\n"
597 " return info ? info->version : std::nullopt;\n"
598 " }\n"
599 " /// The connected module's source repository, if any.\n"
600 " std::optional<std::string> repo() const {\n"
601 " auto info = rawModule->getInfo();\n"
602 " return info ? info->repo : std::nullopt;\n"
603 " }\n"
604 " /// The connected module's source commit hash, if any.\n"
605 " std::optional<std::string> commitHash() const {\n"
606 " auto info = rawModule->getInfo();\n"
607 " return info ? info->commitHash : std::nullopt;\n"
608 " }\n"
609 " /// Designer-specified constants for the connected module.\n"
610 " /// Returns an empty map if the module has no metadata.\n"
611 " std::map<std::string, esi::Constant> constants() const {\n"
612 " auto info = rawModule->getInfo();\n"
613 " return info ? info->constants\n"
614 " : std::map<std::string, esi::Constant>{};\n"
615 " }\n"
616 " /// Free-form designer-supplied metadata for the connected module.\n"
617 " /// Returns an empty map if the module has no metadata.\n"
618 " std::map<std::string, std::any> extra() const {\n"
619 " auto info = rawModule->getInfo();\n"
620 " return info ? info->extra : std::map<std::string, std::any>{};\n"
621 " }\n\n")
622
623 if port_groups:
624 out.write(" std::unique_ptr<Connected> connect() {\n")
625
626 # Find / resolve phase.
627 for grp in port_groups:
628 if grp.find_code:
629 for line in grp.find_code.splitlines():
630 out.write(f" {line}\n")
631 out.write("\n")
632
633 # Construct Connected.
634 all_args = [a for grp in port_groups for a in grp.make_unique_args]
635 out.write(" auto connected = std::make_unique<Connected>(\n")
636 for i, arg in enumerate(all_args):
637 comma = "," if i < len(all_args) - 1 else ""
638 out.write(f" {arg}{comma}\n")
639 out.write(" );\n\n")
640
641 # Post-construction connects.
642 for grp in port_groups:
643 if grp.post_connect:
644 for line in grp.post_connect.splitlines():
645 out.write(f" {line}\n")
646
647 out.write(" return connected;\n }\n\n")
648
649 out.write("private:\n esi::HWModule *rawModule;\n};\n\n")
650 out.write(f"}} // namespace {system_name}\n")
651
652 def write_modules(self, output_dir: Path, system_name: str):
653 """Write the C++ header. One for each module in the manifest."""
654 module_instances = self._build_module_instance_map()
655
656 for module_info in self.manifest.module_infos:
657 if module_info.name is None:
658 continue
659 name = module_info.name
660 instance = module_instances.get(name)
661 try:
662 if instance is not None:
663 port_groups = self._collect_port_groups(instance.ports)
664 else:
665 port_groups = []
666 except (NotImplementedError, ValueError) as e:
667 hdr_file = output_dir / f"{name}.h"
668 with open(hdr_file, "w", encoding="utf-8") as hdr:
669 hdr.write(f"// Skipped: {e}\n")
670 continue
671
672 hdr_file = output_dir / f"{name}.h"
673 with open(hdr_file, "w", encoding="utf-8") as hdr:
674 self._emit_module_class(name, system_name, module_info, port_groups,
675 hdr)
676
677 def generate(self, output_dir: Path, system_name: str):
678 self.type_emitter.write_header(output_dir, system_name)
679 self.write_modules(output_dir, system_name)
680
681
683 """Plan C++ type naming and ordering from an ESI manifest."""
684
685 def __init__(self, type_table) -> None:
686 """Initialize the generator with the manifest and target namespace."""
687 # Map manifest type ids to their preferred C++ names.
688 self.type_id_map: Dict[types.ESIType, str] = {}
689 # Track all names already taken to avoid collisions. True => alias-based.
690 self.used_names: Dict[str, bool] = {}
691 # Track alias base names to warn on collisions.
692 self.alias_base_names: Set[str] = set()
693 self.ordered_types: List[types.ESIType] = []
694 # Types the planner chose not to emit, paired with the reason. The
695 # emitter writes a `// Unsupported type ...` comment for each so the
696 # generated header explains the omission.
697 self.skipped_types: List[Tuple[types.ESIType, str]] = []
698 self.has_cycle = False
699 self._prepare_types(type_table)
700
701 def _prepare_types(self, type_table) -> None:
702 """Name the types and prepare for emission by registering all reachable
703 types and assigning."""
704 visited: Set[str] = set()
705 for t in type_table:
706 self._collect_aliases(t, visited)
707
708 visited = set()
709 for t in type_table:
710 self._collect_structs(t, visited)
711
712 visited = set()
713 for t in type_table:
714 self._collect_windows(t, visited)
715
717
718 def _sanitize_name(self, name: str) -> str:
719 """Create a C++-safe identifier from the manifest-provided name."""
720 name = name.replace("::", "_")
721 if name.startswith("@"):
722 name = name[1:]
723 sanitized = []
724 for ch in name:
725 if ch.isalnum() or ch == "_":
726 sanitized.append(ch)
727 else:
728 sanitized.append("_")
729 if not sanitized:
730 return "Type"
731 if sanitized[0].isdigit():
732 sanitized.insert(0, "_")
733 return "".join(sanitized)
734
735 def _reserve_name(self, base: str, is_alias: bool) -> str:
736 """Reserve a globally unique identifier using the sanitized base name."""
737 base = self._sanitize_name(base)
738 if is_alias and base in self.alias_base_names:
739 sys.stderr.write(
740 f"Warning: duplicate alias name '{base}' detected; disambiguating.\n")
741 if is_alias:
742 self.alias_base_names.add(base)
743 name = base
744 idx = 1
745 while name in self.used_names:
746 name = f"{base}_{idx}"
747 idx += 1
748 self.used_names[name] = is_alias
749 return name
750
751 def _auto_struct_name(self, struct_type: types.StructType) -> str:
752 """Derive a deterministic name for anonymous structs from their fields."""
753 parts = ["_struct"]
754 for field_name, field_type in struct_type.fields:
755 parts.append(field_name)
756 parts.append(self._sanitize_name(field_type.id))
757 return self._reserve_name("_".join(parts), is_alias=False)
758
759 def _auto_union_name(self, union_type: types.UnionType) -> str:
760 """Derive a deterministic name for anonymous unions from their fields."""
761 parts = ["_union"]
762 for field_name, field_type in union_type.fields:
763 parts.append(field_name)
764 parts.append(self._sanitize_name(field_type.id))
765 return self._reserve_name("_".join(parts), is_alias=False)
766
767 def _auto_window_name(self, window_type: types.WindowType) -> str:
768 """Derive a deterministic name for generated window helpers.
769
770 Two distinct windows can wrap the same `into` struct (e.g. serial and
771 parallel encodings of the same payload), so the helper name must be
772 derived from BOTH the inner type's name and the window's own name/id.
773 """
774 into_type = self._unwrap_aliases(window_type.into_type)
775 into_name = self.type_id_map.get(into_type)
776 window_part = (window_type.name
777 if window_type.name else self._sanitize_name(window_type.id))
778 if into_name:
779 base = f"{into_name}_{window_part}"
780 elif window_type.name:
781 base = window_part
782 else:
783 base = f"_window_{window_part}"
784 return self._reserve_name(base, is_alias=False)
785
786 def _unwrap_aliases(self, wrapped: types.ESIType) -> types.ESIType:
787 while isinstance(wrapped, types.TypeAlias):
788 wrapped = wrapped.inner_type
789 return wrapped
790
791 def _is_supported_window(self, current_type: types.ESIType) -> bool:
792 if not isinstance(current_type, types.WindowType):
793 return False
794 into_type = self._unwrap_aliases(current_type.into_type)
795 if not isinstance(into_type, types.StructType):
796 return False
797
798 # The generated window helper only supports struct-shaped payloads with a
799 # single logical list field to stream across multiple frames.
800 list_fields = []
801 for field_name, field_type in into_type.fields:
802 if isinstance(self._unwrap_aliases(field_type), types.ListType):
803 list_fields.append(field_name)
804 if len(list_fields) != 1:
805 return False
806
807 list_field_name = list_fields[0]
808 header_field = None
809 data_field = None
810 # That list must appear exactly once as a bulk-count field and exactly once
811 # as a single-item data field so the helper can synthesize header/data/footer.
812 for frame in current_type.frames:
813 for field in frame.fields:
814 if field.name != list_field_name:
815 continue
816 if field.bulk_count_width > 0:
817 if header_field is not None:
818 return False
819 header_field = field
820 elif field.num_items > 0:
821 if data_field is not None:
822 return False
823 data_field = field
824 return (header_field is not None and data_field is not None and
825 data_field.num_items == 1)
826
827 def _iter_type_children(self, t: types.ESIType) -> List[types.ESIType]:
828 """Return child types in a stable order for traversal."""
829 if isinstance(t, types.TypeAlias):
830 return [t.inner_type] if t.inner_type is not None else []
831 if isinstance(t, types.BundleType):
832 return [channel.type for channel in t.channels]
833 if isinstance(t, types.ChannelType):
834 return [t.inner]
835 if isinstance(t, types.StructType):
836 return [field_type for _, field_type in t.fields]
837 if isinstance(t, types.UnionType):
838 return [field_type for _, field_type in t.fields]
839 if isinstance(t, types.ListType):
840 return [t.element_type]
841 if isinstance(t, types.WindowType):
842 return [t.into_type]
843 if isinstance(t, types.ArrayType):
844 return [t.element_type]
845 return []
846
847 def _visit_types(self, t: types.ESIType, visited: Set[str], visit_fn) -> None:
848 """Traverse types with alphabetical child ordering in post-order."""
849 if not isinstance(t, types.ESIType):
850 raise TypeError(f"Expected ESIType, got {type(t)}")
851 tid = t.id
852 if tid in visited:
853 return
854 visited.add(tid)
855 children = sorted(self._iter_type_children(t), key=lambda child: child.id)
856 for child in children:
857 self._visit_types(child, visited, visit_fn)
858 visit_fn(t)
859
860 def _collect_aliases(self, t: types.ESIType, visited: Set[str]) -> None:
861 """Scan for aliases and reserve their names (recursive)."""
862
863 # Visit callback: reserve alias names and map aliases to identifiers.
864 def visit(alias_type: types.ESIType) -> None:
865 if not isinstance(alias_type, types.TypeAlias):
866 return
867 if alias_type not in self.type_id_map:
868 alias_name = self._reserve_name(alias_type.name, is_alias=True)
869 self.type_id_map[alias_type] = alias_name
870
871 self._visit_types(t, visited, visit)
872
873 def _collect_structs(self, t: types.ESIType, visited: Set[str]) -> None:
874 """Scan for structs/unions needing auto-names and reserve them."""
875
876 # Visit callback: assign auto-names to unnamed structs and unions.
877 def visit(current_type: types.ESIType) -> None:
878 if current_type in self.type_id_map:
879 return
880 if isinstance(current_type, types.StructType):
881 self.type_id_map[current_type] = self._auto_struct_name(current_type)
882 elif isinstance(current_type, types.UnionType):
883 self.type_id_map[current_type] = self._auto_union_name(current_type)
884
885 self._visit_types(t, visited, visit)
886
887 def _collect_windows(self, t: types.ESIType, visited: Set[str]) -> None:
888 """Scan for supported window types and reserve helper names."""
889
890 def visit(current_type: types.ESIType) -> None:
891 if not self._is_supported_window(current_type):
892 return
893 assert isinstance(current_type, types.WindowType)
894 if current_type in self.type_id_map:
895 return
896 self.type_id_map[current_type] = self._auto_window_name(current_type)
897
898 self._visit_types(t, visited, visit)
899
901 wrapped: types.ESIType) -> Set[types.ESIType]:
902 """Collect types that require top-level declarations for a given type."""
903 deps: Set[types.ESIType] = set()
904
905 # Visit callback: collect structs, unions, and non-struct aliases used by a
906 # type.
907 def visit(current: types.ESIType) -> None:
908 if isinstance(current, types.TypeAlias):
909 inner = current.inner_type
910 if inner is not None and (isinstance(
912 self._is_supported_window(inner)):
913 deps.add(inner)
914 else:
915 deps.add(current)
916 elif isinstance(current, (types.StructType, types.UnionType)):
917 deps.add(current)
918 elif self._is_supported_window(current):
919 deps.add(current)
920
921 self._visit_types(wrapped, set(), visit)
922 return deps
923
925 self, window_type: types.WindowType) -> Set[types.ESIType]:
926 """Collect only the declarations referenced by a generated window helper."""
927 deps: Set[types.ESIType] = set()
928 into_type = self._unwrap_aliases(window_type.into_type)
929 if not isinstance(into_type, types.StructType):
930 return deps
931
932 for _, field_type in into_type.fields:
933 unwrapped = self._unwrap_aliases(field_type)
934 if isinstance(unwrapped, types.ListType):
935 deps.update(self._collect_decls_from_type(unwrapped.element_type))
936 else:
937 deps.update(self._collect_decls_from_type(field_type))
938 return deps
939
940 def _contains_window(self, esi_type: types.ESIType) -> bool:
941 """Return True if `esi_type` is or transitively contains a WindowType.
942
943 Structs (and aliases/unions that reference them) which embed a window
944 cannot be emitted as C++ packed structs because the C++ window helper
945 is a variable-size multi-frame container. This helper is used to
946 exclude such types from the emission list entirely.
947 """
948 unwrapped = self._unwrap_aliases(esi_type)
949 if isinstance(unwrapped, types.WindowType):
950 return True
951 if isinstance(unwrapped, types.StructType):
952 return any(self._contains_window(ft) for _, ft in unwrapped.fields)
953 if isinstance(unwrapped, types.UnionType):
954 return any(self._contains_window(ft) for _, ft in unwrapped.fields)
955 if isinstance(unwrapped, types.ArrayType):
956 return self._contains_window(unwrapped.element_type)
957 return False
958
959 def _contains_unbounded(self, esi_type: types.ESIType) -> bool:
960 """Return True if `esi_type` is or transitively contains a type with
961 no bounded bit width (e.g. `!esi.any`, or a list that the window
962 helper can't wrap).
963
964 A struct can't be emitted as a fixed-size raw-bytes buffer if any
965 field's width is unbounded — there's no `std::array<uint8_t, N>` size
966 that would match the wire layout — so the planner excludes such
967 structs (and aliases/unions/arrays that reach one) from the emission
968 list, the same way `_contains_window` does for nested windows.
969 """
970 unwrapped = self._unwrap_aliases(esi_type)
971 try:
972 if unwrapped.bit_width < 0:
973 return True
974 except Exception:
975 return True
976 if isinstance(unwrapped, types.StructType):
977 return any(self._contains_unbounded(ft) for _, ft in unwrapped.fields)
978 if isinstance(unwrapped, types.UnionType):
979 return any(self._contains_unbounded(ft) for _, ft in unwrapped.fields)
980 if isinstance(unwrapped, types.ArrayType):
981 return self._contains_unbounded(unwrapped.element_type)
982 return False
983
984 def _ordered_emit_types(self) -> Tuple[List[types.ESIType], bool]:
985 """Collect and order types for deterministic emission."""
986 window_into_types: Set[types.ESIType] = set()
987 for esi_type in self.type_id_map.keys():
988 if not self._is_supported_window(esi_type):
989 continue
990 assert isinstance(esi_type, types.WindowType)
991 window_into_types.add(self._unwrap_aliases(esi_type.into_type))
992
993 # Build the skip set up front so the DFS dep-walk below can filter
994 # against it too — otherwise a top-level type can pull a skipped
995 # inner struct back into the emission list via `_collect_decls_*`.
996 skip_set: Set[types.ESIType] = set()
997 for esi_type in self.type_id_map.keys():
998 if isinstance(esi_type,
999 types.StructType) and esi_type in window_into_types:
1000 skip_set.add(esi_type)
1001 self.skipped_types.append(
1002 (esi_type,
1003 "into-type of a windowed helper; subsumed by the helper class"))
1004 continue
1005 if isinstance(esi_type, types.TypeAlias):
1006 inner = esi_type.inner_type
1007 if inner is not None and self._unwrap_aliases(
1008 inner) in window_into_types:
1009 skip_set.add(esi_type)
1010 self.skipped_types.append(
1011 (esi_type, "alias of a windowed helper's into-type"))
1012 continue
1013 # Skip structs/unions/aliases that transitively embed a WindowType
1014 # field. WindowType itself is fine — it emits as its own helper
1015 # class.
1016 if (not isinstance(self._unwrap_aliases(esi_type), types.WindowType) and
1017 self._contains_window(esi_type)):
1018 skip_set.add(esi_type)
1019 self.skipped_types.append((esi_type, "contains a windowed sub-type"))
1020 continue
1021 # Skip structs/unions/aliases that transitively contain an
1022 # unbounded type (e.g. `!esi.any`). No fixed-size raw-bytes buffer
1023 # can hold them, so the per-field emitter has no width to embed.
1024 # WindowType itself reports unbounded width but emits as its own
1025 # helper class, so leave that branch alone.
1026 if (not isinstance(self._unwrap_aliases(esi_type), types.WindowType) and
1027 self._contains_unbounded(esi_type)):
1028 skip_set.add(esi_type)
1029 self.skipped_types.append(
1030 (esi_type, "contains an unbounded sub-type (e.g. !esi.any)"))
1031 continue
1032
1033 emit_types: List[types.ESIType] = []
1034 for esi_type in self.type_id_map.keys():
1035 if esi_type in skip_set:
1036 continue
1037 if (isinstance(esi_type,
1039 self._is_supported_window(esi_type)):
1040 emit_types.append(esi_type)
1041
1042 # Prefer alias-reserved names first, then lexicographic for determinism.
1043 name_to_type = {self.type_id_map[t]: t for t in emit_types}
1044 sorted_names = sorted(name_to_type.keys(),
1045 key=lambda name:
1046 (0 if self.used_names.get(name, False) else 1, name))
1047
1048 ordered: List[types.ESIType] = []
1049 visited: Set[types.ESIType] = set()
1050 visiting: Set[types.ESIType] = set()
1051 has_cycle = False
1052
1053 # Visit callback: DFS to emit dependencies before their users. Skipped
1054 # types are filtered out of the dep set so they can't sneak back in
1055 # via a non-skipped type that happens to reference them (e.g. an
1056 # alias whose dep walk reaches the into-struct of a window helper).
1057 def visit(current: types.ESIType) -> None:
1058 nonlocal has_cycle
1059 if current in visited:
1060 return
1061 if current in visiting:
1062 has_cycle = True
1063 return
1064 visiting.add(current)
1065
1066 deps: Set[types.ESIType] = set()
1067 if isinstance(current, types.TypeAlias):
1068 inner = current.inner_type
1069 if inner is not None:
1070 deps.update(self._collect_decls_from_type(inner))
1071 elif isinstance(current, (types.StructType, types.UnionType)):
1072 for _, field_type in current.fields:
1073 deps.update(self._collect_decls_from_type(field_type))
1074 elif self._is_supported_window(current):
1075 assert isinstance(current, types.WindowType)
1076 deps.update(self._collect_decls_from_window(current))
1077 for dep in sorted(deps, key=lambda dep: self.type_id_map[dep]):
1078 if dep in skip_set:
1079 continue
1080 visit(dep)
1081
1082 visiting.remove(current)
1083 visited.add(current)
1084 ordered.append(current)
1085
1086 for name in sorted_names:
1087 visit(name_to_type[name])
1088
1089 return ordered, has_cycle
1090
1091
1093 """Emit C++ headers from precomputed type ordering."""
1094
1095 def __init__(self, planner: CppTypePlanner) -> None:
1096 self.type_id_map = planner.type_id_map
1097 self.ordered_types = planner.ordered_types
1098 self.skipped_types = planner.skipped_types
1099 self.has_cycle = planner.has_cycle
1100
1101 def type_identifier(self, type: types.ESIType) -> str:
1102 """Get the C++ type string for an ESI type."""
1103 return self._cpp_type(type)
1104
1105 def _cpp_string_literal(self, value: str) -> str:
1106 """Escape a Python string for use as a C++ string literal."""
1107 escaped = value.replace("\\", "\\\\").replace('"', '\\"')
1108 return f'"{escaped}"'
1109
1110 def _get_bitvector_str(self, type: types.ESIType) -> str:
1111 """Get the textual code for the C++ type used to represent an integer
1112 field's value at the API boundary.
1113
1114 Integers up to 64 bits map to the native `int{N}_t` / `uint{N}_t`
1115 (or `bool` for a single bit) storage types so common scalars stay
1116 zero-overhead, behave identically to plain C ints, and stay valid as
1117 template parameters for the existing `TypedReadPort<T>` /
1118 `TypedWritePort<T>` / `TypedFunction<...>` machinery.
1119
1120 Wider integers (signed > 64 bits, unsigned > 64 bits, or any
1121 `BitsType` > 64 bits) fall back to the non-owning view classes from
1122 `esi/Values.h`: `esi::BitVector` for `BitsType`, `esi::IntView` for
1123 signed integers, `esi::UIntView` for unsigned. All three are
1124 non-owning views over the parent struct's bytes, so generated
1125 getters are zero-allocation; see the lifetime note on `BitVector`
1126 and at the top of the generated header.
1127 """
1128 assert isinstance(type, (types.BitsType, types.IntType))
1129
1130 if type.bit_width > 64:
1131 if isinstance(type, types.BitsType):
1132 return "esi::BitVector"
1133 if isinstance(type, types.UIntType):
1134 return "esi::UIntView"
1135 return "esi::IntView"
1136
1137 return self._storage_type(
1138 type.bit_width, not isinstance(type, (types.BitsType, types.UIntType)))
1139
1140 def _storage_type(self, bit_width: int, signed: bool) -> str:
1141 """Get the textual code for a native byte-addressable integer storage
1142 type. Only valid for widths 1..64; wider integers are handled by
1143 `_get_bitvector_str` via the `esi::IntView` / `esi::UIntView` view
1144 classes.
1145 """
1146
1147 if bit_width == 1:
1148 return "bool"
1149 elif bit_width <= 8:
1150 storage_width = 8
1151 elif bit_width <= 16:
1152 storage_width = 16
1153 elif bit_width <= 32:
1154 storage_width = 32
1155 elif bit_width <= 64:
1156 storage_width = 64
1157 else:
1158 raise ValueError(f"Unsupported native integer width: {bit_width}")
1159
1160 if not signed:
1161 return f"uint{storage_width}_t"
1162 return f"int{storage_width}_t"
1163
1164 def _is_value_class_type(self, field_type: types.ESIType) -> bool:
1165 """True if the C++ representation of this integer field is one of
1166 the `esi::{BitVector,IntView,UIntView}` view classes from
1167 `esi/Values.h` rather than a native integer.
1168 """
1169 wrapped = self._unwrap_aliases(field_type)
1170 if not isinstance(wrapped, (types.BitsType, types.IntType)):
1171 return False
1172 return wrapped.bit_width > 64
1173
1174 def _type_byte_width(self, wrapped: types.ESIType) -> int:
1175 """Return the size of a fixed-width type in bytes."""
1176 if wrapped.bit_width < 0:
1177 raise ValueError(f"Unsupported unbounded type width for '{wrapped}'")
1178 return (wrapped.bit_width + 7) // 8
1179
1181 self, array_type: types.ArrayType) -> Tuple[str, List[int]]:
1182 """Return the base C++ type and outer-to-inner dimensions of a nested array."""
1183 dims: List[int] = []
1184 inner: types.ESIType = array_type
1185 while isinstance(inner, types.ArrayType):
1186 dims.append(inner.size)
1187 inner = inner.element_type
1188 base_cpp = self._cpp_type(inner)
1189 return base_cpp, dims
1190
1191 def _std_array_type(self, array_type: types.ArrayType) -> str:
1192 """Return the equivalent nested `std::array<...>` type for an array.
1193
1194 `std::array<T, N>` is layout-compatible in practice with `T[N]` on every
1195 major implementation (and identical under `#pragma pack(1)`), so the
1196 generator uses it everywhere a fixed-size array would appear. This keeps
1197 field/value/ctor types storable in `std::vector` and assignable with `=`.
1198 """
1199 base_cpp, dims = self._array_base_and_dims(array_type)
1200 result = base_cpp
1201 for size in reversed(dims):
1202 result = f"std::array<{result}, {size}>"
1203 return result
1204
1205 def _cpp_type(self, wrapped: types.ESIType) -> str:
1206 """Resolve an ESI type to its C++ identifier."""
1207 if isinstance(wrapped, types.WindowType) and wrapped in self.type_id_map:
1208 return self.type_id_map[wrapped]
1209 if isinstance(wrapped,
1211 # Zero-width composite types (e.g. a struct of only void fields, or an
1212 # alias to such a struct) collapse to `void`. C++ structs are defined to
1213 # have `sizeof >= 1`, so there is no meaningful storage to emit; treat them
1214 # as void everywhere they appear so callers comment them out exactly
1215 # like a direct `VoidType` field.
1216 if wrapped.bit_width == 0:
1217 return "void"
1218 return self.type_id_map[wrapped]
1219 if isinstance(wrapped, types.BundleType):
1220 return "void"
1221 if isinstance(wrapped, types.ChannelType):
1222 return self._cpp_type(wrapped.inner)
1223 if isinstance(wrapped, types.ListType):
1224 raise ValueError("List types require a generated window wrapper")
1225 if isinstance(wrapped, types.VoidType):
1226 return "void"
1227 if isinstance(wrapped, types.AnyType):
1228 return "std::any"
1229 if isinstance(wrapped, (types.BitsType, types.IntType)):
1230 # A zero-width integer carries no data; emit it as `void` so it gets
1231 # commented out in field positions like any other void.
1232 if wrapped.bit_width == 0:
1233 return "void"
1234 return self._get_bitvector_str(wrapped)
1235 if isinstance(wrapped, types.ArrayType):
1236 # `std::array<void, N>` is ill-formed; arrays of zero-width elements
1237 # also collapse to `void`.
1238 if wrapped.bit_width == 0:
1239 return "void"
1240 return self._std_array_type(wrapped)
1241 if type(wrapped) is types.ESIType:
1242 return "std::any"
1243 raise NotImplementedError(
1244 f"Type '{wrapped}' not supported for C++ generation")
1245
1246 def _unwrap_aliases(self, wrapped: types.ESIType) -> types.ESIType:
1247 """Strip alias wrappers to reach the underlying type."""
1248 while isinstance(wrapped, types.TypeAlias):
1249 wrapped = wrapped.inner_type
1250 return wrapped
1251
1252 def _format_window_ctor_param(self, field_name: str,
1253 field_type: types.ESIType) -> str:
1254 """Emit a constructor parameter for generated window helpers.
1255
1256 Small scalar header fields are cheaper to pass by value than by reference.
1257 Larger aggregates stay as const references.
1258 """
1259 field_cpp = self._cpp_type(field_type)
1260 wrapped = self._unwrap_aliases(field_type)
1261 if isinstance(wrapped, (types.BitsType, types.IntType)):
1262 if self._is_value_class_type(field_type):
1263 return f"const {field_cpp} &{field_name}"
1264 return f"{field_cpp} {field_name}"
1265 return f"const {field_cpp} &{field_name}"
1266
1267 def _field_byte_width(self, field_type: types.ESIType) -> int:
1268 """Compute the byte width of a field type, rounding up to full bytes."""
1269 return (field_type.bit_width + 7) // 8
1270
1271 def _safe_byte_width(self, esi_type: types.ESIType) -> Optional[int]:
1272 """Return the bounded byte width of `esi_type`, or `None` if it has no
1273 well-defined static size (e.g. unbounded `!esi.any` or recursive types).
1274 """
1275 try:
1276 bit_width = esi_type.bit_width
1277 except Exception:
1278 return None
1279 if bit_width is None or bit_width < 0:
1280 return None
1281 return (bit_width + 7) // 8
1282
1284 hdr: TextIO,
1285 type_name: str,
1286 expected_bytes: Optional[int],
1287 indent: str = "") -> None:
1288 """Emit a `static_assert` that pins the C++ `sizeof` of a packed type to
1289 the byte width derived from the manifest.
1290
1291 `std::array` and bit-field layout are technically implementation-defined,
1292 so this assertion is the safety net that catches a toolchain that lays
1293 them out differently from the wire format. Skipped silently for types
1294 without a bounded static size.
1295 """
1296 if expected_bytes is None:
1297 return
1298 hdr.write(
1299 f"{indent}static_assert(sizeof({type_name}) == {expected_bytes},\n"
1300 f"{indent} \"{type_name}: packed layout does not match "
1301 f"manifest size\");\n")
1302
1303 def _analyze_window(self, window_type: types.WindowType):
1304 """Extract the metadata needed to emit a bulk list window wrapper."""
1305 into_type = self._unwrap_aliases(window_type.into_type)
1306 if not isinstance(into_type, types.StructType):
1307 raise ValueError("window codegen currently requires a struct into-type")
1308
1309 field_map = {name: field_type for name, field_type in into_type.fields}
1310 list_fields = [
1311 (name, self._unwrap_aliases(field_type))
1312 for name, field_type in into_type.fields
1313 if isinstance(self._unwrap_aliases(field_type), types.ListType)
1314 ]
1315 if len(list_fields) != 1:
1316 raise ValueError("window codegen currently supports exactly one list")
1317
1318 list_field_name, list_type = list_fields[0]
1319 assert isinstance(list_type, types.ListType)
1320
1321 header_frame = None
1322 header_field = None
1323 data_frame = None
1324 data_field = None
1325 for frame in window_type.frames:
1326 for field in frame.fields:
1327 if field.name != list_field_name:
1328 continue
1329 if field.bulk_count_width > 0:
1330 header_frame = frame
1331 header_field = field
1332 elif field.num_items > 0:
1333 data_frame = frame
1334 data_field = field
1335
1336 if header_frame is None or header_field is None:
1337 raise ValueError("window codegen requires a bulk-count header frame")
1338 if data_frame is None or data_field is None:
1339 raise ValueError("window codegen requires a data frame for the list")
1340 if data_field.num_items != 1:
1341 raise ValueError("window codegen currently supports numItems == 1")
1342
1343 ctor_params = [(name, field_type)
1344 for name, field_type in into_type.fields
1345 if name != list_field_name]
1346
1347 header_fields = []
1348 header_bytes = 0
1349 count_field_name = f"{list_field_name}_count"
1350 count_width = header_field.bulk_count_width
1351 count_cpp = self._storage_type(count_width, signed=False)
1352 count_bytes = (count_width + 7) // 8
1353 for field in reversed(header_frame.fields):
1354 if field.name == list_field_name:
1355 header_fields.append((count_field_name, None))
1356 header_bytes += count_bytes
1357 else:
1358 field_type = field_map[field.name]
1359 header_fields.append((field.name, field_type))
1360 header_bytes += self._type_byte_width(field_type)
1361
1362 data_fields = []
1363 data_bytes = 0
1364 for field in reversed(data_frame.fields):
1365 if field.name == list_field_name:
1366 data_fields.append((list_field_name, list_type.element_type))
1367 data_bytes += self._type_byte_width(list_type.element_type)
1368 else:
1369 field_type = field_map[field.name]
1370 data_fields.append((field.name, field_type))
1371 data_bytes += self._type_byte_width(field_type)
1372
1373 frame_bytes = max(header_bytes, data_bytes)
1374
1375 return {
1376 "ctor_params": ctor_params,
1377 "count_cpp": count_cpp,
1378 "count_field_name": count_field_name,
1379 "count_width": count_width,
1380 "data_fields": data_fields,
1381 "data_pad_bytes": frame_bytes - data_bytes,
1382 "element_cpp": self._cpp_type(list_type.element_type),
1383 "frame_bytes": frame_bytes,
1384 "header_fields": header_fields,
1385 "header_pad_bytes": frame_bytes - header_bytes,
1386 "list_field_name": list_field_name,
1387 "window_name": self.type_id_map[window_type],
1388 }
1389
1391 self, struct_type: types.StructType) -> Tuple[Dict[str, int], int]:
1392 """Return `(bit_offset_by_name, total_bits)` for non-void fields.
1393
1394 Fields are laid out LSB-first in wire order, which is reversed manifest
1395 order when `cpp_type.reverse` is set. Caller already knows each field's
1396 type/width from `struct_type.fields`; we only need the per-name offset
1397 and the resulting total bit width to size the storage array.
1398 """
1399 declared = struct_type.fields
1400 if struct_type.cpp_type.reverse:
1401 declared = list(reversed(declared))
1402 bit_offsets: Dict[str, int] = {}
1403 bit_offset = 0
1404 for field_name, field_type in declared:
1405 if self._cpp_type(field_type) == "void":
1406 continue
1407 bit_offsets[field_name] = bit_offset
1408 bit_offset += field_type.bit_width
1409 return bit_offsets, bit_offset
1410
1411 def _is_signed_int_field(self, field_type: types.ESIType) -> bool:
1412 """True if the field is a signed integer (only `IntType`, not `UIntType`
1413 or `BitsType`)."""
1414 wrapped = self._unwrap_aliases(field_type)
1415 return (isinstance(wrapped, types.IntType) and
1416 not isinstance(wrapped, types.UIntType))
1417
1418 def _is_byte_packable(self, esi_type: types.ESIType) -> bool:
1419 """True if the type's in-memory C++ layout is byte-for-byte identical to
1420 its on-wire bit layout, so a flat per-byte copy round-trips it correctly.
1421
1422 This holds only when every scalar leaf occupies a whole number of bytes
1423 that exactly matches its native storage size:
1424
1425 * an integer / bits type whose width is one of the native storage
1426 widths (8/16/32/64). `ui3`, `si5`, a 1-bit `bool`, and even `ui24`
1427 (stored in a wider `uint32_t`) all have storage wider than their
1428 wire width and are therefore *not* byte-packable.
1429 * a struct / union whose total width is a whole number of bytes -- its
1430 `_bytes` buffer already mirrors the wire layout.
1431 * an array whose element type is itself byte-packable, so successive
1432 elements share the same byte stride on the wire and in memory.
1433
1434 Anything that is not byte-packable must be (un)packed element-by-element
1435 from its individual wire bit offset rather than flat-copied.
1436 """
1437 wrapped = self._unwrap_aliases(esi_type)
1438 if isinstance(wrapped, (types.BitsType, types.IntType)):
1439 return wrapped.bit_width in (8, 16, 32, 64)
1440 if isinstance(wrapped, types.ArrayType):
1441 return self._is_byte_packable(wrapped.element_type)
1442 if isinstance(wrapped, (types.StructType, types.UnionType)):
1443 bit_width = wrapped.bit_width
1444 return bit_width is not None and bit_width > 0 and bit_width % 8 == 0
1445 return False
1446
1447 def _emit_field_accessor(self, hdr: TextIO, indent: str, raw_member: str,
1448 self_type: str, field_name: str,
1449 field_type: types.ESIType, bit_offset: int,
1450 bit_width: int) -> None:
1451 """Emit a getter/setter pair that reads/writes `field_name` out of the
1452 raw bytes member `raw_member` at `bit_offset` / `bit_width`.
1453
1454 Setters share the field's name (no `set_` prefix) and return
1455 `self_type &` so the caller can chain calls (e.g.
1456 `Foo{}.a(1).b(2).inner(x)`).
1457
1458 For integer fields the emitter picks between four inline strategies,
1459 fastest first:
1460
1461 * Path A — 8/16/32/64-bit fields at a byte-aligned offset. Read/write
1462 via a `reinterpret_cast` through the underlying `std::array<uint8_t>`,
1463 matching the same aliasing pattern the runtime already relies on in
1464 `MessageData::from<T>()` / `MessageData::as<T>()`.
1465 * Path B — byte-aligned offset and width but a non-standard width
1466 (e.g. `i24`, `i48`). Read each byte directly out of `_bytes` and
1467 OR-shift them together (and the reverse on write — split the
1468 value out one byte at a time). Signed fields read into the
1469 matching unsigned, then sign-extend before returning.
1470 * Path C — sub-byte alignment. Fall back to the
1471 `esi::detail::{read,write}{Un,}signedBits` helpers from
1472 `esi/BitAccess.h`, which loop over the constituent bits.
1473 * Path D — view-class fields. Triggered when `_is_value_class_type`
1474 is true — currently widths above 64 bits, for any of `BitsType`,
1475 signed, or unsigned. Returns a non-owning
1476 `esi::BitVector` / `esi::IntView` / `esi::UIntView` view *into*
1477 the parent struct's `_bytes` -- zero allocation, no copy. The
1478 setter accepts any `esi::BitVector` (so views and owning
1479 subclasses both work) and writes back via `copyBitsOut`. The
1480 returned view dangles when the parent buffer dies; see the
1481 lifetime warning at the top of the generated header.
1482 """
1483 field_cpp = self._cpp_type(field_type)
1484 if field_cpp == "void":
1485 return
1486
1487 wrapped = self._unwrap_aliases(field_type)
1488
1489 if isinstance(wrapped, (types.BitsType, types.IntType)):
1490 # Path D: view-class field. Today this is exactly the
1491 # wider-than-64-bit integer / Bits cases; gated by
1492 # `_is_value_class_type` so the rule lives in one place.
1493 if self._is_value_class_type(field_type):
1494 byte_offset = bit_offset // 8
1495 sub_bit_index = bit_offset % 8
1496 # Number of bytes the view needs to span: the high bit is at
1497 # `sub_bit_index + bit_width - 1`, so we cover
1498 # ceil((sub_bit_index + bit_width) / 8) bytes starting at
1499 # `_bytes.data() + byte_offset`.
1500 span_bytes = (sub_bit_index + bit_width + 7) // 8
1501 hdr.write(f"{indent}{field_cpp} {field_name}() const {{\n")
1502 hdr.write(f"{indent} return {field_cpp}(\n")
1503 hdr.write(f"{indent} std::span<const uint8_t>("
1504 f"{raw_member}.data() + {byte_offset}, {span_bytes}),\n")
1505 hdr.write(f"{indent} static_cast<std::size_t>({bit_width}),\n")
1506 hdr.write(f"{indent} static_cast<uint8_t>({sub_bit_index}));\n")
1507 hdr.write(f"{indent}}}\n")
1508 hdr.write(f"{indent}{self_type} &{field_name}("
1509 f"const {field_cpp} &v) {{\n")
1510 # Walk the input view bit-by-bit and write each bit straight
1511 # into `_bytes` at its target position.
1512 hdr.write(f"{indent} const std::size_t n = "
1513 f"std::min<std::size_t>(v.width(), {bit_width});\n")
1514 hdr.write(
1515 f"{indent} for (std::size_t b = 0; b < {bit_width}; ++b) {{\n")
1516 hdr.write(f"{indent} const std::size_t g = {bit_offset} + b;\n")
1517 hdr.write(f"{indent} const bool val = (b < n) && v.getBit(b);\n")
1518 hdr.write(f"{indent} if (val)\n")
1519 hdr.write(f"{indent} {raw_member}[g / 8] |= "
1520 f"static_cast<uint8_t>(uint8_t{{1}} << (g % 8));\n")
1521 hdr.write(f"{indent} else\n")
1522 hdr.write(f"{indent} {raw_member}[g / 8] &= "
1523 f"static_cast<uint8_t>(~(uint8_t{{1}} << (g % 8)));\n")
1524 hdr.write(f"{indent} }}\n")
1525 hdr.write(f"{indent} return *this;\n")
1526 hdr.write(f"{indent}}}\n")
1527 return
1528
1529 # `bool` is the storage choice for a single bit; bit-precise helpers
1530 # are the simplest fit and the optimiser collapses them on -O2.
1531 if bit_width == 1 and field_cpp == "bool":
1532 hdr.write(f"{indent}bool {field_name}() const {{\n")
1533 hdr.write(f"{indent} return esi::detail::readUnsignedBits<uint8_t, "
1534 f"{bit_offset}, 1>({raw_member}.data()) != 0;\n")
1535 hdr.write(f"{indent}}}\n")
1536 hdr.write(f"{indent}{self_type} &{field_name}(bool v) {{\n")
1537 hdr.write(f"{indent} esi::detail::writeUnsignedBits<uint8_t, "
1538 f"{bit_offset}, 1>({raw_member}.data(), "
1539 f"static_cast<uint8_t>(v) & uint8_t{{1}});\n")
1540 hdr.write(f"{indent} return *this;\n")
1541 hdr.write(f"{indent}}}\n")
1542 return
1543
1544 signed = self._is_signed_int_field(field_type)
1545 byte_aligned = (bit_offset % 8 == 0 and bit_width % 8 == 0)
1546
1547 if byte_aligned:
1548 byte_offset = bit_offset // 8
1549 byte_width = bit_width // 8
1550
1551 # Path A: standard-width byte-aligned integer. `std::memcpy`
1552 # avoids the unaligned-load / strict-aliasing / object-lifetime
1553 # UB that a `reinterpret_cast` deref through a `uint8_t` buffer
1554 # would invoke (MSVC / ASan / UBSan in particular). The compiler
1555 # collapses both calls to a single mov on -O1+.
1556 if bit_width in (8, 16, 32, 64):
1557 hdr.write(f"{indent}{field_cpp} {field_name}() const {{\n")
1558 hdr.write(f"{indent} {field_cpp} out;\n")
1559 hdr.write(f"{indent} std::memcpy(&out, {raw_member}.data() + "
1560 f"{byte_offset}, sizeof({field_cpp}));\n")
1561 hdr.write(f"{indent} return out;\n")
1562 hdr.write(f"{indent}}}\n")
1563 hdr.write(f"{indent}{self_type} &{field_name}({field_cpp} v) {{\n")
1564 hdr.write(f"{indent} std::memcpy({raw_member}.data() + "
1565 f"{byte_offset}, &v, sizeof({field_cpp}));\n")
1566 hdr.write(f"{indent} return *this;\n")
1567 hdr.write(f"{indent}}}\n")
1568 return
1569
1570 # Path B: byte-aligned but non-standard width (e.g. i24, i48). Build
1571 # the value from per-byte shifts in little-endian wire order. For
1572 # signed fields the result is assembled into the matching unsigned
1573 # type first so the sign-extension step can mask cleanly without
1574 # invoking implementation-defined right-shift behaviour on signed
1575 # types.
1576 # `field_cpp` already names the rounded-up storage type
1577 # (e.g. `int32_t` for `i24`); flip the leading `int`/`uint` rather
1578 # than reconstructing the width to avoid emitting non-standard names
1579 # like `uint24_t`.
1580 if signed:
1581 unsigned_cpp = "u" + field_cpp
1582 else:
1583 unsigned_cpp = field_cpp
1584 # Assemble the unsigned value.
1585 read_terms = [
1586 f"static_cast<{unsigned_cpp}>({raw_member}[{byte_offset}])"
1587 ]
1588 for i in range(1, byte_width):
1589 read_terms.append(
1590 f"(static_cast<{unsigned_cpp}>({raw_member}[{byte_offset + i}])"
1591 f" << {i * 8})")
1592 read_expr = " |\n ".join(read_terms)
1593
1594 hdr.write(f"{indent}{field_cpp} {field_name}() const {{\n")
1595 if signed:
1596 hdr.write(f"{indent} {unsigned_cpp} u = {read_expr};\n")
1597 # Sign-extend from the value's high bit.
1598 hdr.write(
1599 f"{indent} if (u & ({unsigned_cpp}{{1}} << {bit_width - 1}))\n")
1600 hdr.write(f"{indent} u |= ~(({unsigned_cpp}{{1}} << {bit_width})"
1601 f" - {unsigned_cpp}{{1}});\n")
1602 hdr.write(f"{indent} return static_cast<{field_cpp}>(u);\n")
1603 else:
1604 hdr.write(f"{indent} return {read_expr};\n")
1605 hdr.write(f"{indent}}}\n")
1606
1607 hdr.write(f"{indent}{self_type} &{field_name}({field_cpp} v) {{\n")
1608 if signed:
1609 hdr.write(f"{indent} {unsigned_cpp} u = "
1610 f"static_cast<{unsigned_cpp}>(v);\n")
1611 value_expr = "u"
1612 else:
1613 value_expr = "v"
1614 for i in range(byte_width):
1615 if i == 0:
1616 hdr.write(f"{indent} {raw_member}[{byte_offset}] = "
1617 f"static_cast<uint8_t>({value_expr});\n")
1618 else:
1619 hdr.write(f"{indent} {raw_member}[{byte_offset + i}] = "
1620 f"static_cast<uint8_t>({value_expr} >> {i * 8});\n")
1621 hdr.write(f"{indent} return *this;\n")
1622 hdr.write(f"{indent}}}\n")
1623 return
1624
1625 # Path C: sub-byte alignment. Delegate to the generic bit helpers.
1626 if signed:
1627 hdr.write(f"{indent}{field_cpp} {field_name}() const {{\n")
1628 hdr.write(f"{indent} return esi::detail::readSignedBits<{field_cpp}, "
1629 f"{bit_offset}, {bit_width}>({raw_member}.data());\n")
1630 hdr.write(f"{indent}}}\n")
1631 hdr.write(f"{indent}{self_type} &{field_name}({field_cpp} v) {{\n")
1632 hdr.write(f"{indent} esi::detail::writeSignedBits<{field_cpp}, "
1633 f"{bit_offset}, {bit_width}>({raw_member}.data(), v);\n")
1634 hdr.write(f"{indent} return *this;\n")
1635 hdr.write(f"{indent}}}\n")
1636 else:
1637 hdr.write(f"{indent}{field_cpp} {field_name}() const {{\n")
1638 hdr.write(
1639 f"{indent} return esi::detail::readUnsignedBits<{field_cpp}, "
1640 f"{bit_offset}, {bit_width}>({raw_member}.data());\n")
1641 hdr.write(f"{indent}}}\n")
1642 hdr.write(f"{indent}{self_type} &{field_name}({field_cpp} v) {{\n")
1643 hdr.write(f"{indent} esi::detail::writeUnsignedBits<{field_cpp}, "
1644 f"{bit_offset}, {bit_width}>({raw_member}.data(), v);\n")
1645 hdr.write(f"{indent} return *this;\n")
1646 hdr.write(f"{indent}}}\n")
1647 return
1648
1649 # Aggregate field (struct/union/array). Supports both byte-aligned and
1650 # arbitrary-bit-aligned embedding. The inner aggregate stores its bits
1651 # LSB-first in its own `_bytes` buffer, so reading/writing reduces to
1652 # copying `bit_width` bits between the parent buffer at `bit_offset`
1653 # and the inner buffer at bit 0. When both the offset and width happen
1654 # to be byte-aligned we emit a direct per-byte copy; otherwise we fall
1655 # back to `esi::detail::copyBitsIn` / `copyBitsOut`, which shuffle the
1656 # bits at whatever position they actually land in the parent's wire
1657 # layout.
1658 assert bit_width >= 0, (
1659 f"field '{field_name}': unbounded aggregate field reached the "
1660 f"emitter; the planner should have excluded the parent struct "
1661 f"via `_contains_unbounded`")
1662 byte_aligned = (bit_offset % 8 == 0 and bit_width % 8 == 0)
1663 byte_offset = bit_offset // 8
1664 byte_width = (bit_width + 7) // 8
1665
1666 # An array whose element is one of the `esi::{BitVector,IntView,
1667 # UIntView}`.
1668 is_view_array = (isinstance(wrapped, types.ArrayType) and
1669 self._is_value_class_type(wrapped.element_type))
1670
1671 # An array whose C++ element layout does not match its on-wire layout,
1672 # so the flat byte-copy / `copyBitsIn` / `copyBitsOut` paths below would
1673 # truncate or misplace elements. Two element kinds need this treatment:
1674 #
1675 # * native-integer elements whose storage is wider than the wire
1676 # width -- sub-byte elements (`i1` -> `bool`, `ui3`) or odd widths
1677 # that round up to a wider storage type (`ui24` -> `uint32_t`).
1678 # * struct / union elements whose total width is not a whole number of
1679 # bytes (e.g. a 5-bit `{ui3, ui2}`): successive elements pack at a
1680 # sub-byte stride on the wire but at a padded whole-byte stride in
1681 # the C++ `std::array`.
1682 #
1683 # In both cases each element is (un)packed one at a time from its own
1684 # wire bit offset `bit_offset + i * elem_width`. Nested-array and
1685 # view-class elements keep their own paths below.
1686 packed_elem = (self._unwrap_aliases(wrapped.element_type) if isinstance(
1687 wrapped, types.ArrayType) else None)
1688 is_packed_array = (isinstance(wrapped, types.ArrayType) and
1689 not is_view_array and
1690 isinstance(packed_elem,
1693 not self._is_byte_packable(wrapped.element_type))
1694
1695 if is_packed_array:
1696 assert isinstance(wrapped, types.ArrayType)
1697 elem_type = wrapped.element_type
1698 elem_cpp = self._cpp_type(elem_type)
1699 elem_width = elem_type.bit_width
1700 elem_count = wrapped.size
1701
1702 if isinstance(packed_elem, (types.StructType, types.UnionType)):
1703 # Aggregate element: its own `_bytes` buffer already mirrors the
1704 # wire layout, so copy `elem_width` bits between the parent buffer
1705 # and the element's bytes (which start at the element's bit 0). The
1706 # `reinterpret_cast<uint8_t *>` mirrors the whole-aggregate byte
1707 # copy emitted further below.
1708 hdr.write(f"{indent}{elem_cpp} {field_name}(std::size_t i) const {{\n")
1709 hdr.write(f"{indent} {elem_cpp} out{{}};\n")
1710 hdr.write(f"{indent} auto *dst = reinterpret_cast<uint8_t *>(&out);\n")
1711 hdr.write(f"{indent} const std::size_t bit_off = "
1712 f"{bit_offset} + i * {elem_width};\n")
1713 hdr.write(
1714 f"{indent} for (std::size_t b = 0; b < {elem_width}; ++b) {{\n")
1715 hdr.write(f"{indent} const std::size_t g = bit_off + b;\n")
1716 hdr.write(f"{indent} if (({raw_member}[g / 8] >> (g % 8)) & 1u)\n")
1717 hdr.write(f"{indent} dst[b / 8] |= "
1718 f"static_cast<uint8_t>(uint8_t{{1}} << (b % 8));\n")
1719 hdr.write(f"{indent} }}\n")
1720 hdr.write(f"{indent} return out;\n")
1721 hdr.write(f"{indent}}}\n")
1722
1723 hdr.write(f"{indent}{self_type} &{field_name}(std::size_t i, "
1724 f"const {elem_cpp} &v) {{\n")
1725 hdr.write(f"{indent} const auto *src = "
1726 f"reinterpret_cast<const uint8_t *>(&v);\n")
1727 hdr.write(f"{indent} const std::size_t bit_off = "
1728 f"{bit_offset} + i * {elem_width};\n")
1729 hdr.write(
1730 f"{indent} for (std::size_t b = 0; b < {elem_width}; ++b) {{\n")
1731 hdr.write(f"{indent} const std::size_t g = bit_off + b;\n")
1732 hdr.write(f"{indent} const uint8_t bit = static_cast<uint8_t>("
1733 f"(src[b / 8] >> (b % 8)) & 1u);\n")
1734 hdr.write(f"{indent} {raw_member}[g / 8] = static_cast<uint8_t>(\n")
1735 hdr.write(
1736 f"{indent} ({raw_member}[g / 8] & static_cast<uint8_t>("
1737 f"~(uint8_t{{1}} << (g % 8)))) |\n")
1738 hdr.write(f"{indent} static_cast<uint8_t>(bit << (g % 8)));\n")
1739 hdr.write(f"{indent} }}\n")
1740 hdr.write(f"{indent} return *this;\n")
1741 hdr.write(f"{indent}}}\n")
1742 else:
1743 signed = self._is_signed_int_field(elem_type)
1744 is_bool = (elem_cpp == "bool")
1745 # Unsigned storage used to assemble/disassemble the element bits; a
1746 # signed value is recovered afterwards with an explicit
1747 # sign-extension so the shifts never touch the sign bit (mirrors
1748 # `readSignedBits`).
1749 if is_bool:
1750 unsigned_cpp = "uint8_t"
1751 elif signed:
1752 unsigned_cpp = "u" + elem_cpp
1753 else:
1754 unsigned_cpp = elem_cpp
1755
1756 # Per-element getter: assemble `elem_width` bits LSB-first starting
1757 # at the element's wire offset `bit_offset + i * elem_width`.
1758 hdr.write(f"{indent}{elem_cpp} {field_name}(std::size_t i) const {{\n")
1759 hdr.write(f"{indent} const std::size_t bit_off = "
1760 f"{bit_offset} + i * {elem_width};\n")
1761 if is_bool:
1762 hdr.write(f"{indent} return (({raw_member}[bit_off / 8] >> "
1763 f"(bit_off % 8)) & 1u) != 0;\n")
1764 else:
1765 hdr.write(f"{indent} {unsigned_cpp} u = 0;\n")
1766 hdr.write(
1767 f"{indent} for (std::size_t b = 0; b < {elem_width}; ++b) {{\n")
1768 hdr.write(f"{indent} const std::size_t g = bit_off + b;\n")
1769 hdr.write(f"{indent} u = static_cast<{unsigned_cpp}>(\n")
1770 hdr.write(f"{indent} u | (static_cast<{unsigned_cpp}>("
1771 f"({raw_member}[g / 8] >> (g % 8)) & 1u) << b));\n")
1772 hdr.write(f"{indent} }}\n")
1773 if signed:
1774 hdr.write(f"{indent} constexpr {unsigned_cpp} signBit = "
1775 f"static_cast<{unsigned_cpp}>({unsigned_cpp}{{1}} << "
1776 f"{elem_width - 1});\n")
1777 hdr.write(f"{indent} u = static_cast<{unsigned_cpp}>("
1778 f"(u ^ signBit) - signBit);\n")
1779 hdr.write(f"{indent} return static_cast<{elem_cpp}>(u);\n")
1780 hdr.write(f"{indent}}}\n")
1781
1782 # Per-element setter: write `elem_width` bits LSB-first into the
1783 # element's wire offset, leaving the surrounding bits untouched.
1784 hdr.write(f"{indent}{self_type} &{field_name}(std::size_t i, "
1785 f"{elem_cpp} v) {{\n")
1786 hdr.write(f"{indent} const std::size_t bit_off = "
1787 f"{bit_offset} + i * {elem_width};\n")
1788 hdr.write(f"{indent} const {unsigned_cpp} u = "
1789 f"static_cast<{unsigned_cpp}>(v);\n")
1790 hdr.write(
1791 f"{indent} for (std::size_t b = 0; b < {elem_width}; ++b) {{\n")
1792 hdr.write(f"{indent} const std::size_t g = bit_off + b;\n")
1793 hdr.write(f"{indent} const uint8_t bit = static_cast<uint8_t>("
1794 f"(u >> b) & {unsigned_cpp}{{1}});\n")
1795 hdr.write(f"{indent} {raw_member}[g / 8] = static_cast<uint8_t>(\n")
1796 hdr.write(
1797 f"{indent} ({raw_member}[g / 8] & static_cast<uint8_t>("
1798 f"~(uint8_t{{1}} << (g % 8)))) |\n")
1799 hdr.write(f"{indent} static_cast<uint8_t>(bit << (g % 8)));\n")
1800 hdr.write(f"{indent} }}\n")
1801 hdr.write(f"{indent} return *this;\n")
1802 hdr.write(f"{indent}}}\n")
1803
1804 # Whole-array getter/setter forward to the per-element accessors so the
1805 # public API matches the byte-packable array case exactly.
1806 hdr.write(f"{indent}{field_cpp} {field_name}() const {{\n")
1807 hdr.write(f"{indent} {field_cpp} out{{}};\n")
1808 hdr.write(f"{indent} for (std::size_t i = 0; i < {elem_count}; ++i)\n")
1809 hdr.write(f"{indent} out[i] = {field_name}(i);\n")
1810 hdr.write(f"{indent} return out;\n")
1811 hdr.write(f"{indent}}}\n")
1812 hdr.write(f"{indent}{self_type} &{field_name}(const {field_cpp} &v) {{\n")
1813 hdr.write(f"{indent} for (std::size_t i = 0; i < {elem_count}; ++i)\n")
1814 hdr.write(f"{indent} {field_name}(i, v[i]);\n")
1815 hdr.write(f"{indent} return *this;\n")
1816 hdr.write(f"{indent}}}\n")
1817 return
1818
1819 if not is_view_array:
1820 if byte_aligned:
1821 # Byte-aligned: direct byte copy between the parent's buffer slice
1822 # and the inner aggregate's `_bytes`. An explicit per-byte loop
1823 # avoids `memcpy` while still collapsing to one on -O2.
1824 hdr.write(f"{indent}{field_cpp} {field_name}() const {{\n")
1825 hdr.write(f"{indent} {field_cpp} out{{}};\n")
1826 hdr.write(f"{indent} for (std::size_t i = 0; i < {byte_width}; ++i)\n")
1827 hdr.write(f"{indent} reinterpret_cast<uint8_t *>(&out)[i] = "
1828 f"{raw_member}[{byte_offset} + i];\n")
1829 hdr.write(f"{indent} return out;\n")
1830 hdr.write(f"{indent}}}\n")
1831 hdr.write(
1832 f"{indent}{self_type} &{field_name}(const {field_cpp} &v) {{\n")
1833 hdr.write(f"{indent} for (std::size_t i = 0; i < {byte_width}; ++i)\n")
1834 hdr.write(f"{indent} {raw_member}[{byte_offset} + i] = "
1835 f"reinterpret_cast<const uint8_t *>(&v)[i];\n")
1836 hdr.write(f"{indent} return *this;\n")
1837 hdr.write(f"{indent}}}\n")
1838 else:
1839 # Non-byte-aligned: delegate to the per-bit copy helpers. The
1840 # inner's `out{}` zero-initialiser is required by `copyBitsIn`,
1841 # which only OR-sets the `1` bits.
1842 hdr.write(f"{indent}{field_cpp} {field_name}() const {{\n")
1843 hdr.write(f"{indent} {field_cpp} out{{}};\n")
1844 hdr.write(f"{indent} esi::detail::copyBitsIn<{bit_offset}, "
1845 f"{bit_width}>({raw_member}.data(), "
1846 f"reinterpret_cast<uint8_t *>(&out));\n")
1847 hdr.write(f"{indent} return out;\n")
1848 hdr.write(f"{indent}}}\n")
1849 hdr.write(
1850 f"{indent}{self_type} &{field_name}(const {field_cpp} &v) {{\n")
1851 hdr.write(f"{indent} esi::detail::copyBitsOut<{bit_offset}, "
1852 f"{bit_width}>({raw_member}.data(), "
1853 f"reinterpret_cast<const uint8_t *>(&v));\n")
1854 hdr.write(f"{indent} return *this;\n")
1855 hdr.write(f"{indent}}}\n")
1856
1857 # Convenience indexed accessors for fixed-size arrays. The whole-array
1858 # getter/setter above is always sufficient; these helpers just save a
1859 # round-trip through a local `std::array` when callers only touch one
1860 # element. Only emitted when the array starts at a byte-aligned
1861 # position and the element width is a whole number of bytes — anything
1862 # else has to go through the whole-array accessor since per-element
1863 # bit shuffling would require its own per-element offset arithmetic.
1864 if (not is_view_array and isinstance(wrapped, types.ArrayType) and
1865 byte_aligned and wrapped.element_type.bit_width % 8 == 0):
1866 elem_cpp = self._cpp_type(wrapped.element_type)
1867 if elem_cpp != "void":
1868 elem_bytes = self._field_byte_width(wrapped.element_type)
1869 hdr.write(f"{indent}{elem_cpp} {field_name}(std::size_t i) const {{\n")
1870 hdr.write(f"{indent} {elem_cpp} out{{}};\n")
1871 hdr.write(f"{indent} for (std::size_t j = 0; j < {elem_bytes}; ++j)\n")
1872 hdr.write(f"{indent} reinterpret_cast<uint8_t *>(&out)[j] = "
1873 f"{raw_member}[{byte_offset} + i * {elem_bytes} + j];\n")
1874 hdr.write(f"{indent} return out;\n")
1875 hdr.write(f"{indent}}}\n")
1876 hdr.write(f"{indent}{self_type} &{field_name}(std::size_t i, "
1877 f"const {elem_cpp} &v) {{\n")
1878 hdr.write(f"{indent} for (std::size_t j = 0; j < {elem_bytes}; ++j)\n")
1879 hdr.write(f"{indent} {raw_member}[{byte_offset} + i * {elem_bytes}"
1880 f" + j] = reinterpret_cast<const uint8_t *>(&v)[j];\n")
1881 hdr.write(f"{indent} return *this;\n")
1882 hdr.write(f"{indent}}}\n")
1883
1884 # Indexed accessors for arrays whose element is one of the view
1885 # classes (`BitVector` / `IntView` / `UIntView`). The whole-array
1886 # byte-copy path is intentionally skipped (the view's storage layout
1887 # doesn't match the wire layout). Instead we emit three accessors:
1888 #
1889 # * `field(i)` / `field(i, v)` -- per-element getter (zero-copy
1890 # view into the relevant slice of `_bytes`) and setter
1891 # (runtime per-bit copy, since `copyBitsOut` needs a
1892 # compile-time offset and the array index is runtime).
1893 # * `field()` -- a lazy `std::views::iota | std::views::transform`
1894 # range that yields the per-element views on demand. Random
1895 # access, no allocation, no element materialised until accessed.
1896 # * `field(const std::array<view, N> &)` -- whole-array setter
1897 # that delegates to the per-element setter N times. Lets the
1898 # emplace-style struct ctor accept view-array fields as a
1899 # single argument.
1900 #
1901 # All three share the parent struct's lifetime; see the warning at
1902 # the top of the generated header.
1903 if is_view_array:
1904 assert isinstance(wrapped, types.ArrayType)
1905 elem_type = wrapped.element_type
1906 elem_cpp = self._cpp_type(elem_type)
1907 elem_width = elem_type.bit_width
1908 elem_count = wrapped.size
1909 hdr.write(f"{indent}{elem_cpp} {field_name}(std::size_t i) const {{\n")
1910 hdr.write(f"{indent} const std::size_t bit_off = "
1911 f"{bit_offset} + i * {elem_width};\n")
1912 hdr.write(f"{indent} return {elem_cpp}(\n")
1913 hdr.write(f"{indent} std::span<const uint8_t>("
1914 f"{raw_member}.data() + bit_off / 8,\n")
1915 hdr.write(f"{indent} "
1916 f"(bit_off % 8 + {elem_width} + 7) / 8),\n")
1917 hdr.write(f"{indent} static_cast<std::size_t>({elem_width}),\n")
1918 hdr.write(f"{indent} static_cast<uint8_t>(bit_off % 8));\n")
1919 hdr.write(f"{indent}}}\n")
1920 hdr.write(f"{indent}{self_type} &{field_name}(std::size_t i, "
1921 f"const {elem_cpp} &v) {{\n")
1922 hdr.write(f"{indent} const std::size_t bit_off = "
1923 f"{bit_offset} + i * {elem_width};\n")
1924 hdr.write(f"{indent} const std::size_t n = "
1925 f"std::min<std::size_t>(v.width(), {elem_width});\n")
1926 hdr.write(
1927 f"{indent} for (std::size_t b = 0; b < {elem_width}; ++b) {{\n")
1928 hdr.write(f"{indent} const std::size_t g = bit_off + b;\n")
1929 hdr.write(f"{indent} const bool val = (b < n) && v.getBit(b);\n")
1930 hdr.write(f"{indent} if (val)\n")
1931 hdr.write(f"{indent} {raw_member}[g / 8] |= "
1932 f"static_cast<uint8_t>(uint8_t{{1}} << (g % 8));\n")
1933 hdr.write(f"{indent} else\n")
1934 hdr.write(f"{indent} {raw_member}[g / 8] &= "
1935 f"static_cast<uint8_t>(~(uint8_t{{1}} << (g % 8)));\n")
1936 hdr.write(f"{indent} }}\n")
1937 hdr.write(f"{indent} return *this;\n")
1938 hdr.write(f"{indent}}}\n")
1939 # Lazy whole-array view: `iota(0, N) | transform([this](i){ ... })`
1940 # gives a random-access range of `elem_cpp` views computed on
1941 # access, with zero up-front allocation.
1942 hdr.write(f"{indent}auto {field_name}() const {{\n")
1943 hdr.write(f"{indent} return std::views::iota("
1944 f"std::size_t{{0}}, std::size_t{{{elem_count}}}) |\n")
1945 hdr.write(f"{indent} std::views::transform("
1946 f"[this](std::size_t i) {{\n")
1947 hdr.write(f"{indent} return this->{field_name}(i);\n")
1948 hdr.write(f"{indent} }});\n")
1949 hdr.write(f"{indent}}}\n")
1950 # Whole-array setter: forwards to the per-element setter for
1951 # each index.
1952 hdr.write(f"{indent}{self_type} &{field_name}("
1953 f"const std::array<{elem_cpp}, {elem_count}> &v) {{\n")
1954 hdr.write(f"{indent} for (std::size_t i = 0; i < {elem_count}; ++i)\n")
1955 hdr.write(f"{indent} {field_name}(i, v[i]);\n")
1956 hdr.write(f"{indent} return *this;\n")
1957 hdr.write(f"{indent}}}\n")
1958
1959 def _ctor_param_type(self, field_type: types.ESIType) -> str:
1960 """Return the C++ constructor-parameter type for a setter call."""
1961 field_cpp = self._cpp_type(field_type)
1962 wrapped = self._unwrap_aliases(field_type)
1963 if isinstance(wrapped, (types.BitsType, types.IntType)):
1964 return field_cpp
1965 return f"const {field_cpp} &"
1966
1967 def _emit_struct(self, hdr: TextIO, struct_type: types.StructType) -> None:
1968 """Emit a packed struct as a raw byte buffer with bit-precise accessors.
1969
1970 The struct's storage is a single `std::array<uint8_t, N>` (where `N`
1971 is the byte width derived from the manifest) whose layout matches the
1972 on-wire bit-packing exactly. Per-field accessors (generated below)
1973 read/write the bits so the wire format does not depend on C++ bit-field
1974 allocation rules, which differ between the Itanium ABI (GCC/Clang) and MSVC.
1975 """
1976 # Zero-width composite types collapse to `void` everywhere they appear
1977 # (see _cpp_type), so there is nothing meaningful to emit here.
1978 if struct_type.bit_width == 0:
1979 return
1980 struct_name = self.type_id_map[struct_type]
1981 bit_offsets, total_bits = self._compute_field_bit_offsets(struct_type)
1982 total_bytes = (total_bits + 7) // 8
1983
1984 # Logical-order field list (manifest order) for the constructor and the
1985 # public accessor list.
1986 logical_fields = [(name, ftype)
1987 for name, ftype in struct_type.fields
1988 if self._cpp_type(ftype) != "void"]
1989
1990 # A struct whose every field collapses to void carries no data the
1991 # generated header can expose. Emit nothing rather than a 1-byte
1992 # placeholder, since (a) the placeholder doesn't reflect any real wire
1993 # layout and (b) a size_assert built from the manifest's `bit_width`
1994 # (which includes void padding) would fail to compile.
1995 if not logical_fields:
1996 return
1997
1998 hdr.write(f"struct {struct_name} {{\n")
1999 # `_bytes` holds the wire layout and is private; the only legitimate
2000 # external view of those bytes is via `MessageData::from()` /
2001 # `MessageData::as()` which reinterpret-cast through the whole struct
2002 # and don't need member-level access.
2003 hdr.write("private:\n")
2004 hdr.write(f" std::array<uint8_t, {max(total_bytes, 1)}> _bytes{{}};\n\n")
2005 hdr.write("public:\n")
2006
2007 hdr.write(f" {struct_name}() = default;\n")
2008 ctor_params = ", ".join(f"{self._ctor_param_type(ftype)} {name}"
2009 for name, ftype in logical_fields)
2010 hdr.write(f" {struct_name}({ctor_params}) {{\n")
2011 # `this->` disambiguates the chained setter calls from the like-named
2012 # parameters that shadow the member functions inside the ctor body.
2013 chain = ".".join(f"{name}({name})" for name, _ in logical_fields)
2014 hdr.write(f" this->{chain};\n")
2015 hdr.write(" }\n\n")
2016
2017 # Per-field accessors in logical (manifest) order so the user-facing
2018 # API mirrors the manifest field order rather than the wire reversal.
2019 for name, ftype in logical_fields:
2020 self._emit_field_accessor(hdr, " ", "_bytes", struct_name, name, ftype,
2021 bit_offsets[name], ftype.bit_width)
2022 hdr.write("\n")
2023
2024 hdr.write(f" static constexpr std::string_view _ESI_ID = "
2025 f"{self._cpp_string_literal(struct_type.id)};\n")
2026 hdr.write("};\n")
2027 expected_bytes = self._safe_byte_width(struct_type)
2028 if expected_bytes is not None and expected_bytes > 0:
2029 self._emit_size_assert(hdr, struct_name, expected_bytes)
2030 hdr.write("\n")
2031
2032 def _emit_union(self, hdr: TextIO, union_type: types.UnionType) -> None:
2033 """Emit a union as a raw byte buffer with per-variant accessors.
2034
2035 The union's storage is a single `std::array<uint8_t, N>` sized to the
2036 widest variant. Each variant lives at the MSB end of the buffer
2037 (matching the existing SV-style packed union layout where padding
2038 occupies the lower bytes), so the byte offset for a variant of size
2039 V is `union_bytes - V`. Sub-byte integer variants are byte-padded to
2040 full bytes within that region, matching the Python runtime's union
2041 serialization.
2042 """
2043 # Zero-width unions collapse to `void` (see _cpp_type) so there is
2044 # nothing meaningful to emit here.
2045 if union_type.bit_width == 0:
2046 return
2047 union_name = self.type_id_map[union_type]
2048 union_bytes = self._field_byte_width(union_type)
2049
2050 hdr.write(f"struct {union_name} {{\n")
2051 # See `_emit_struct` for the access-control rationale.
2052 hdr.write("private:\n")
2053 hdr.write(f" std::array<uint8_t, {union_bytes}> _bytes{{}};\n\n")
2054 hdr.write("public:\n")
2055 hdr.write(f" {union_name}() = default;\n\n")
2056
2057 for field_name, field_type in union_type.fields:
2058 field_cpp = self._cpp_type(field_type)
2059 if field_cpp == "void":
2060 continue
2061 field_bytes = self._field_byte_width(field_type)
2062 byte_offset = union_bytes - field_bytes
2063 bit_offset = byte_offset * 8
2064 bit_width = field_type.bit_width
2065 # Each variant is reached at the same MSB-aligned position regardless
2066 # of width. Reuse `_emit_field_accessor` so we share the integer /
2067 # aggregate code paths and don't duplicate the bit-access boilerplate.
2068 self._emit_field_accessor(hdr, " ", "_bytes", union_name, field_name,
2069 field_type, bit_offset, bit_width)
2070 hdr.write("\n")
2071
2072 hdr.write(f" static constexpr std::string_view _ESI_ID = "
2073 f"{self._cpp_string_literal(union_type.id)};\n")
2074 hdr.write("};\n")
2075 if union_bytes > 0:
2076 self._emit_size_assert(hdr, union_name, union_bytes)
2077 hdr.write("\n")
2078
2079 def _compute_window_frame_layout(self, fields, pad_bytes, count_type_synth):
2080 """Compute (name, type, byte_offset, bit_width) for each window frame field.
2081
2082 Fields are listed in C++ declaration order. They start after `pad_bytes`
2083 bytes of padding and each field consumes `ceil(bit_width/8)` bytes —
2084 matching what the `#pragma pack(1)` layout produced.
2085
2086 The sentinel `(name, None)` count field is materialised as
2087 `count_type_synth` so it can flow through the standard
2088 `_emit_field_accessor` path.
2089 """
2090 layout = []
2091 byte_offset = pad_bytes
2092 for name, ftype in fields:
2093 actual_type = count_type_synth if ftype is None else ftype
2094 bit_width = actual_type.bit_width
2095 layout.append((name, actual_type, byte_offset, bit_width))
2096 byte_offset += (bit_width + 7) // 8
2097 return layout
2098
2099 def _emit_window_frame(self, hdr: TextIO, frame_name: str, frame_bytes: int,
2100 layout) -> None:
2101 """Emit a window header/data frame as a raw-bytes struct with accessors.
2102
2103 Nested inside the window helper class (`indent` = 2 spaces) and uses
2104 the same B3 accessor pattern as top-level structs/unions: a private
2105 `_bytes` array plus per-field getter/setter pairs returning
2106 `frame_name &` to allow chaining.
2107 """
2108 hdr.write(f" struct {frame_name} {{\n")
2109 hdr.write(" private:\n")
2110 hdr.write(f" std::array<uint8_t, {frame_bytes}> _bytes{{}};\n\n")
2111 hdr.write(" public:\n")
2112 for name, ftype, byte_offset, bit_width in layout:
2113 self._emit_field_accessor(hdr, " ", "_bytes", frame_name, name, ftype,
2114 byte_offset * 8, bit_width)
2115 hdr.write("\n")
2116 hdr.write(" };\n")
2117 self._emit_size_assert(hdr, frame_name, frame_bytes, indent=" ")
2118
2119 def _emit_window(self, hdr: TextIO, window_type: types.WindowType) -> None:
2120 """Emit a SegmentedMessageData helper for a serial list window."""
2121 info = self._analyze_window(window_type)
2122 ctor_params = [
2123 self._format_window_ctor_param(name, field_type)
2124 for name, field_type in info["ctor_params"]
2125 ]
2126 value_ctor_params = list(ctor_params)
2127 value_ctor_params.append(
2128 f"const std::vector<value_type> &{info['list_field_name']}")
2129 value_ctor_signature = ", ".join(value_ctor_params)
2130 frame_ctor_params = list(ctor_params)
2131 frame_ctor_params.append("std::vector<data_frame> frames")
2132 frame_ctor_signature = ", ".join(frame_ctor_params)
2133 helper_args = ", ".join(name for name, _ in info["ctor_params"])
2134 helper_call = f"{helper_args}, std::move(frames)" if helper_args else "std::move(frames)"
2135
2136 # Synthesise an unsigned integer type for the sentinel "count" header
2137 # field so it flows through `_emit_field_accessor` like any other
2138 # integer.
2139 count_type_synth = types.UIntType(f"_count_ui{info['count_width']}",
2140 info["count_width"])
2141 data_layout = self._compute_window_frame_layout(info["data_fields"],
2142 info["data_pad_bytes"],
2143 count_type_synth)
2144 header_layout = self._compute_window_frame_layout(info["header_fields"],
2145 info["header_pad_bytes"],
2146 count_type_synth)
2147
2148 hdr.write(
2149 f"struct {info['window_name']} : public esi::SegmentedMessageData {{\n")
2150 hdr.write("public:\n")
2151 hdr.write(f" using value_type = {info['element_cpp']};\n")
2152 hdr.write(f" using count_type = {info['count_cpp']};\n\n")
2153 self._emit_window_frame(hdr, "data_frame", info["frame_bytes"], data_layout)
2154 hdr.write("\n")
2155 hdr.write("private:\n")
2156 self._emit_window_frame(hdr, "header_frame", info["frame_bytes"],
2157 header_layout)
2158 hdr.write("\n")
2159 hdr.write(" header_frame header{};\n")
2160 hdr.write(" std::vector<data_frame> data_frames;\n")
2161 hdr.write(" header_frame footer{};\n\n")
2162 hdr.write(f" void construct({frame_ctor_signature}) {{\n")
2163 hdr.write(" if (frames.empty())\n")
2164 hdr.write(
2165 f" throw std::invalid_argument(\"{info['window_name']}: bulk windowed lists cannot be empty\");\n"
2166 )
2167 hdr.write(
2168 " if (frames.size() > std::numeric_limits<count_type>::max())\n")
2169 hdr.write(
2170 f" throw std::invalid_argument(\"{info['window_name']}: list too large for encoded count\");\n"
2171 )
2172 hdr.write(
2173 f" header.{info['count_field_name']}(static_cast<count_type>(frames.size()));\n"
2174 )
2175 for name, _ in info["ctor_params"]:
2176 hdr.write(f" header.{name}({name});\n")
2177 hdr.write(f" footer.{info['count_field_name']}(0);\n")
2178 hdr.write(" data_frames = std::move(frames);\n")
2179 hdr.write(" }\n\n")
2180 hdr.write("public:\n")
2181 hdr.write(f" {info['window_name']}({frame_ctor_signature}) {{\n")
2182 hdr.write(f" construct({helper_call});\n")
2183 hdr.write(" }\n\n")
2184 hdr.write(f" {info['window_name']}({value_ctor_signature}) {{\n")
2185 hdr.write(" std::vector<data_frame> frames;\n")
2186 hdr.write(f" frames.reserve({info['list_field_name']}.size());\n")
2187 hdr.write(f" for (const auto &element : {info['list_field_name']}) {{\n")
2188 hdr.write(" auto &frame = frames.emplace_back();\n")
2189 hdr.write(f" frame.{info['list_field_name']}(element);\n")
2190 hdr.write(" }\n")
2191 hdr.write(f" construct({helper_call});\n")
2192 hdr.write(" }\n\n")
2193 hdr.write(" size_t numSegments() const override { return 3; }\n")
2194 hdr.write(" esi::Segment segment(size_t idx) const override {\n")
2195 hdr.write(" if (idx == 0)\n")
2196 hdr.write(
2197 " return {reinterpret_cast<const uint8_t *>(&header), sizeof(header)};\n"
2198 )
2199 hdr.write(" if (idx == 1)\n")
2200 hdr.write(
2201 " return {reinterpret_cast<const uint8_t *>(data_frames.data()),\n"
2202 )
2203 hdr.write(" data_frames.size() * sizeof(data_frame)};\n")
2204 hdr.write(" if (idx == 2)\n")
2205 hdr.write(
2206 " return {reinterpret_cast<const uint8_t *>(&footer), sizeof(footer)};\n"
2207 )
2208 hdr.write(
2209 f" throw std::out_of_range(\"{info['window_name']}: invalid segment index\");\n"
2210 )
2211 hdr.write(" }\n\n")
2212 hdr.write(
2213 f" static constexpr std::string_view _ESI_ID = {self._cpp_string_literal(self._unwrap_aliases(window_type.into_type).id)};\n"
2214 )
2215 # The into-type id alone cannot distinguish two different windows over
2216 # the same underlying struct (e.g. serial vs. parallel list encoding).
2217 # Emit the window id so the runtime can verify the wire format too.
2218 hdr.write(
2219 f" static constexpr std::string_view _ESI_WINDOW_ID = {self._cpp_string_literal(window_type.id)};\n"
2220 )
2221 self._emit_window_data_accessors(hdr, info)
2222 self._emit_window_deserializer(hdr, info)
2223 hdr.write("};\n\n")
2224
2225 def _emit_window_data_accessors(self, hdr: TextIO, info) -> None:
2226 """Emit accessors for the header and data fields of a window helper.
2227
2228 Exposes each static header field as a scalar accessor, the count of data
2229 frames, and one vector-valued accessor per data field so decoded values
2230 are easy to inspect on the read side.
2231 """
2232 list_field_name = info["list_field_name"]
2233 hdr.write("\n")
2234 for field_name, field_type in info["header_fields"]:
2235 # Skip the synthetic bulk-count field; it is exposed via
2236 # `<list>_count()` below.
2237 if field_type is None:
2238 continue
2239 cpp = self._cpp_type(field_type)
2240 # The header-frame accessor returns by value (it pulls bytes out of
2241 # the raw buffer), so this is also returned by value regardless of
2242 # whether the underlying type is a scalar or an aggregate.
2243 hdr.write(
2244 f" {cpp} {field_name}() const {{ return header.{field_name}(); }}\n")
2245 hdr.write(
2246 f" size_t {list_field_name}_count() const {{ return data_frames.size(); }}\n"
2247 )
2248 for field_name, field_type in info["data_fields"]:
2249 if field_name == list_field_name:
2250 elem_cpp = "value_type"
2251 else:
2252 elem_cpp = self._cpp_type(field_type)
2253 # The data-frame accessor is now a member function returning by value
2254 # (the underlying `_bytes` is private; getters reconstruct the field
2255 # value from its wire bytes), so the projection has to call it
2256 # rather than form a pointer-to-data-member.
2257 #
2258 # TODO: For byte-aligned aggregate data fields this means iterating
2259 # `field()` copies each element; the pre-B3 pointer-to-member form
2260 # could yield references for free. If profiling shows it matters,
2261 # we can add a separate `field_ref()` accessor on `data_frame` for
2262 # byte-aligned aggregate fields (they're stored contiguously inside
2263 # `_bytes` and the inner type's only member is itself a byte array,
2264 # so a `reinterpret_cast`-backed reference is well-defined) and
2265 # project on that instead.
2266 projection = (f"[](const data_frame &f) -> {elem_cpp} "
2267 f"{{ return f.{field_name}(); }}")
2268 hdr.write(
2269 f" auto {field_name}() const {{\n"
2270 f" return std::views::transform(data_frames, {projection});\n"
2271 f" }}\n")
2272 hdr.write(f" std::vector<{elem_cpp}> {field_name}_vector() const {{\n"
2273 f" std::vector<{elem_cpp}> out;\n"
2274 f" out.reserve(data_frames.size());\n"
2275 f" for (const auto &frame : data_frames)\n"
2276 f" out.push_back(frame.{field_name}());\n"
2277 f" return out;\n"
2278 f" }}\n")
2279
2280 def _emit_window_deserializer(self, hdr: TextIO, info) -> None:
2281 """Emit a few bridge helpers + a `TypeDeserializer` alias.
2282
2283 The actual decoder lives in `esi::SerialListTypeDeserializer<T>`, which
2284 walks the header/data/footer burst protocol generically. Each window
2285 helper only has to expose:
2286
2287 - `_headerCount(const header_frame &)` -> `count_type`
2288 - `_fromFrames(const header_frame &, std::vector<data_frame> &&)`
2289 -> `std::unique_ptr<T>`
2290
2291 plus a `friend class esi::SerialListTypeDeserializer<T>;` so the template
2292 can reach the (private) `header_frame` definition.
2293 """
2294 window_name = info["window_name"]
2295 count_field_name = info["count_field_name"]
2296 ctor_args = ", ".join(f"h.{name}()" for name, _ in info["ctor_params"])
2297 if ctor_args:
2298 ctor_args = f"{ctor_args}, std::move(frames)"
2299 else:
2300 ctor_args = "std::move(frames)"
2301
2302 hdr.write("\n")
2303 hdr.write("private:\n")
2304 hdr.write(
2305 " // Bridge helpers used by esi::SerialListTypeDeserializer<T>; the\n")
2306 hdr.write(
2307 " // template walks the serial-list burst protocol generically and\n")
2308 hdr.write(
2309 " // reaches into `header_frame` via the friend declaration below.\n")
2310 hdr.write(" static count_type _headerCount(const header_frame &h) {\n")
2311 hdr.write(f" return h.{count_field_name}();\n")
2312 hdr.write(" }\n")
2313 hdr.write(f" static std::unique_ptr<{window_name}> _fromFrames(\n")
2314 hdr.write(
2315 " const header_frame &h, std::vector<data_frame> &&frames) {\n")
2316 hdr.write(f" return std::make_unique<{window_name}>({ctor_args});\n")
2317 hdr.write(" }\n")
2318 hdr.write(
2319 f" friend class esi::SerialListTypeDeserializer<{window_name}>;\n\n")
2320 hdr.write("public:\n")
2321 hdr.write(
2322 f" using TypeDeserializer = esi::SerialListTypeDeserializer<{window_name}>;\n"
2323 )
2324
2325 def _emit_alias(self, hdr: TextIO, alias_type: types.TypeAlias) -> None:
2326 """Emit a using alias when the alias targets a different C++ type."""
2327 inner_wrapped = alias_type.inner_type
2328 alias_name = self.type_id_map[alias_type]
2329 inner_cpp = None
2330 if inner_wrapped is not None:
2331 inner_cpp = self._cpp_type(inner_wrapped)
2332 if inner_cpp is None:
2333 inner_cpp = self.type_id_map[alias_type]
2334 if inner_cpp != alias_name:
2335 hdr.write(f"using {alias_name} = {inner_cpp};\n\n")
2336
2337 def write_header(self, output_dir: Path, system_name: str) -> None:
2338 """Emit the fully ordered types.h header into the output directory."""
2339 hdr_file = output_dir / "types.h"
2340 with open(hdr_file, "w", encoding="utf-8") as hdr:
2341 hdr.write(
2342 textwrap.dedent(f"""
2343 // Generated header for {system_name} types.
2344 //
2345 // Lifetime note: accessors that return `esi::BitVector` (for
2346 // `Bits<N>` fields), `esi::IntView` (for `Int<N>`, N > 64), or
2347 // `esi::UIntView` (for `UInt<N>`, N > 64) hand back non-owning
2348 // views into the parent struct's bytes. The view is only valid
2349 // while the parent is alive. Bind the parent to a named local
2350 // first, or construct an owning `esi::Int` / `esi::UInt` /
2351 // `esi::MutableBitVector` from the view if you need the value
2352 // to outlive its source. See `esi/Values.h` for details.
2353 #pragma once
2354
2355 #include <cstdint>
2356 #include <cstddef>
2357 #include <cstring>
2358 #include <algorithm>
2359 #include <any>
2360 #include <array>
2361 #include <limits>
2362 #include <ranges>
2363 #include <stdexcept>
2364 #include <string_view>
2365 #include <utility>
2366 #include <vector>
2367
2368 #include "esi/BitAccess.h"
2369 #include "esi/Common.h"
2370 #include "esi/TypedPorts.h"
2371 #include "esi/Values.h"
2372
2373 namespace {system_name} {{
2374
2375 """))
2376 if self.has_cycle:
2377 sys.stderr.write("Warning: cyclic type dependencies detected.\n")
2378 sys.stderr.write(" Logically this should not be possible.\n")
2379 sys.stderr.write(
2380 " Emitted code may fail to compile due to ordering issues.\n")
2381
2382 for skipped_type, reason in self.skipped_types:
2383 hdr.write(f"// Unsupported type '{skipped_type}': {reason}\n\n")
2384
2385 for emit_type in self.ordered_types:
2386 # Anything that wasn't expressible should have been caught by
2387 # CppTypePlanner and recorded in `skipped_types`; if a problem
2388 # leaks past that, treat it as a planner bug rather than emitting
2389 # a half-written header.
2390 if isinstance(emit_type, types.StructType):
2391 self._emit_struct(hdr, emit_type)
2392 elif isinstance(emit_type, types.UnionType):
2393 self._emit_union(hdr, emit_type)
2394 elif isinstance(emit_type, types.WindowType):
2395 self._emit_window(hdr, emit_type)
2396 elif isinstance(emit_type, types.TypeAlias):
2397 self._emit_alias(hdr, emit_type)
2398
2399 hdr.write(textwrap.dedent(f"""
2400 }} // namespace {system_name}
2401 """))
2402
2403
2404def run(generator: Type[Generator] = CppGenerator,
2405 cmdline_args=sys.argv) -> int:
2406 """Create and run a generator reading options from the command line."""
2407
2408 argparser = argparse.ArgumentParser(
2409 description=f"Generate {generator.language} headers from an ESI manifest",
2410 formatter_class=argparse.RawDescriptionHelpFormatter,
2411 epilog=textwrap.dedent("""
2412 Can read the manifest from either a file OR a running accelerator.
2413
2414 Usage examples:
2415 # To read the manifest from a file:
2416 esi-cppgen --file /path/to/manifest.json
2417
2418 # To read the manifest from a running accelerator:
2419 esi-cppgen --platform cosim --connection localhost:1234
2420 """))
2421
2422 argparser.add_argument("--file",
2423 type=str,
2424 default=None,
2425 help="Path to the manifest file.")
2426 argparser.add_argument(
2427 "--platform",
2428 type=str,
2429 help="Name of platform for live accelerator connection.")
2430 argparser.add_argument(
2431 "--connection",
2432 type=str,
2433 help="Connection string for live accelerator connection.")
2434 argparser.add_argument(
2435 "--output-dir",
2436 type=str,
2437 default="esi",
2438 help="Output directory for generated files. Recommend adding either `esi`"
2439 " or the system name to the end of the path so as to avoid header name"
2440 "conflicts. Defaults to `esi`")
2441 argparser.add_argument(
2442 "--system-name",
2443 type=str,
2444 default="esi_system",
2445 help="Name of the ESI system. For C++, this will be the namespace.")
2446
2447 if (len(cmdline_args) <= 1):
2448 argparser.print_help()
2449 return 1
2450 args = argparser.parse_args(cmdline_args[1:])
2451
2452 if args.file is not None and args.platform is not None:
2453 print("Cannot specify both --file and --platform")
2454 return 1
2455
2456 conn: AcceleratorConnection
2457 if args.file is not None:
2458 # Use os.pathsep (';' on Windows, ':' on Unix) to avoid conflicts with
2459 # drive letters.
2460 conn = Context.default().connect("trace", f"-{os.pathsep}{args.file}")
2461 elif args.platform is not None:
2462 if args.connection is None:
2463 print("Must specify --connection with --platform")
2464 return 1
2465 conn = Context.default().connect(args.platform, args.connection)
2466 else:
2467 print("Must specify either --file or --platform")
2468 return 1
2469
2470 output_dir = Path(args.output_dir)
2471 if output_dir.exists() and not output_dir.is_dir():
2472 print(f"Output directory {output_dir} is not a directory")
2473 return 1
2474 if not output_dir.exists():
2475 output_dir.mkdir(parents=True)
2476
2477 gen = generator(conn)
2478 gen.generate(output_dir, args.system_name)
2479 return 0
2480
2481
2482if __name__ == '__main__':
2483 sys.exit(run())
static void print(TypedAttr val, llvm::raw_ostream &os)
static mlir::Operation * resolve(Context &context, mlir::SymbolRefAttr sym)
#define isdigit(x)
Definition FIRLexer.cpp:26
static StringAttr append(StringAttr base, const Twine &suffix)
Return a attribute with the specified suffix appended.
_PortGroup _scalar_port_group(self, str member_name, port, appid)
Definition codegen.py:274
None _emit_module_class(self, str name, str system_name, ModuleInfo module_info, List[_PortGroup] port_groups, TextIO out)
Definition codegen.py:498
str _cpp_indexed_elem_type(self, port, Optional[str] alias_prefix=None)
Definition codegen.py:163
Dict[str, object] _build_module_instance_map(self)
Definition codegen.py:94
str _cpp_member_type(self, port, Optional[str] alias_prefix=None)
Definition codegen.py:109
str get_consts_str(self, ModuleInfo module_info)
Definition codegen.py:69
str _cpp_ctor_param_type(self, port)
Definition codegen.py:180
str _sanitize_id(str name)
Definition codegen.py:83
List[Tuple[str, str]] _port_using_aliases(self, str alias_prefix, port)
Definition codegen.py:146
str _port_make_unique_arg(str member_name, port)
Definition codegen.py:257
__init__(self, AcceleratorConnection conn)
Definition codegen.py:63
_PortGroup _indexed_ports_group(self, str member_name, str appid_name, port_list)
Definition codegen.py:304
write_modules(self, Path output_dir, str system_name)
Definition codegen.py:652
generate(self, Path output_dir, str system_name)
Definition codegen.py:677
_PortGroup _mixed_struct_group(self, str member_name, str appid_name, port_list)
Definition codegen.py:402
List[_PortGroup] _collect_port_groups(self, dict ports)
Definition codegen.py:452
str _port_find_code(self, str member_name, port, appid)
Definition codegen.py:214
None _emit_struct(self, TextIO hdr, types.StructType struct_type)
Definition codegen.py:1967
bool _is_value_class_type(self, types.ESIType field_type)
Definition codegen.py:1164
bool _is_signed_int_field(self, types.ESIType field_type)
Definition codegen.py:1411
_analyze_window(self, types.WindowType window_type)
Definition codegen.py:1303
None _emit_size_assert(self, TextIO hdr, str type_name, Optional[int] expected_bytes, str indent="")
Definition codegen.py:1287
None _emit_window_deserializer(self, TextIO hdr, info)
Definition codegen.py:2280
None _emit_window_data_accessors(self, TextIO hdr, info)
Definition codegen.py:2225
str _ctor_param_type(self, types.ESIType field_type)
Definition codegen.py:1959
str _storage_type(self, int bit_width, bool signed)
Definition codegen.py:1140
str _std_array_type(self, types.ArrayType array_type)
Definition codegen.py:1191
str _get_bitvector_str(self, types.ESIType type)
Definition codegen.py:1110
None write_header(self, Path output_dir, str system_name)
Definition codegen.py:2337
str _format_window_ctor_param(self, str field_name, types.ESIType field_type)
Definition codegen.py:1253
None _emit_alias(self, TextIO hdr, types.TypeAlias alias_type)
Definition codegen.py:2325
None __init__(self, CppTypePlanner planner)
Definition codegen.py:1095
Tuple[Dict[str, int], int] _compute_field_bit_offsets(self, types.StructType struct_type)
Definition codegen.py:1391
int _field_byte_width(self, types.ESIType field_type)
Definition codegen.py:1267
_compute_window_frame_layout(self, fields, pad_bytes, count_type_synth)
Definition codegen.py:2079
Tuple[str, List[int]] _array_base_and_dims(self, types.ArrayType array_type)
Definition codegen.py:1181
int _type_byte_width(self, types.ESIType wrapped)
Definition codegen.py:1174
types.ESIType _unwrap_aliases(self, types.ESIType wrapped)
Definition codegen.py:1246
str type_identifier(self, types.ESIType type)
Definition codegen.py:1101
None _emit_field_accessor(self, TextIO hdr, str indent, str raw_member, str self_type, str field_name, types.ESIType field_type, int bit_offset, int bit_width)
Definition codegen.py:1450
None _emit_union(self, TextIO hdr, types.UnionType union_type)
Definition codegen.py:2032
str _cpp_string_literal(self, str value)
Definition codegen.py:1105
None _emit_window(self, TextIO hdr, types.WindowType window_type)
Definition codegen.py:2119
Optional[int] _safe_byte_width(self, types.ESIType esi_type)
Definition codegen.py:1271
None _emit_window_frame(self, TextIO hdr, str frame_name, int frame_bytes, layout)
Definition codegen.py:2100
bool _is_byte_packable(self, types.ESIType esi_type)
Definition codegen.py:1418
str _cpp_type(self, types.ESIType wrapped)
Definition codegen.py:1205
str _auto_window_name(self, types.WindowType window_type)
Definition codegen.py:767
bool _contains_unbounded(self, types.ESIType esi_type)
Definition codegen.py:959
bool _contains_window(self, types.ESIType esi_type)
Definition codegen.py:940
Set[types.ESIType] _collect_decls_from_window(self, types.WindowType window_type)
Definition codegen.py:925
types.ESIType _unwrap_aliases(self, types.ESIType wrapped)
Definition codegen.py:786
str _sanitize_name(self, str name)
Definition codegen.py:718
bool _is_supported_window(self, types.ESIType current_type)
Definition codegen.py:791
None _visit_types(self, types.ESIType t, Set[str] visited, visit_fn)
Definition codegen.py:847
str _reserve_name(self, str base, bool is_alias)
Definition codegen.py:735
Tuple[List[types.ESIType], bool] _ordered_emit_types(self)
Definition codegen.py:984
List[types.ESIType] _iter_type_children(self, types.ESIType t)
Definition codegen.py:827
None __init__(self, type_table)
Definition codegen.py:685
Set[types.ESIType] _collect_decls_from_type(self, types.ESIType wrapped)
Definition codegen.py:901
str _auto_struct_name(self, types.StructType struct_type)
Definition codegen.py:751
str _auto_union_name(self, types.UnionType union_type)
Definition codegen.py:759
None _collect_windows(self, types.ESIType t, Set[str] visited)
Definition codegen.py:887
None _collect_structs(self, types.ESIType t, Set[str] visited)
Definition codegen.py:873
None _prepare_types(self, type_table)
Definition codegen.py:701
None _collect_aliases(self, types.ESIType t, Set[str] visited)
Definition codegen.py:860
generate(self, Path output_dir, str system_name)
Definition codegen.py:54
__init__(self, AcceleratorConnection conn)
Definition codegen.py:51
int run(Type[Generator] generator=CppGenerator, cmdline_args=sys.argv)
Definition codegen.py:2405
"AcceleratorConnection" connect(str platform, str connection_str)
Definition __init__.py:28