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