CIRCT 24.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 # Keep declared (MSB-first) struct field order. Each frame shares the
1146 # union's LSB, with any slack at the high end. Use exact bit widths so
1147 # sub-byte count/static fields land at the same offsets as in hardware.
1148 header_fields = []
1149 header_bits = 0
1150 count_field_name = f"{list_field_name}_count"
1151 count_width = header_field.bulk_count_width
1152 count_cpp = self._storage_type(count_width, signed=False)
1153 for field in header_frame.fields:
1154 if field.name == list_field_name:
1155 header_fields.append((count_field_name, None))
1156 header_bits += count_width
1157 else:
1158 field_type = field_map[field.name]
1159 header_fields.append((field.name, field_type))
1160 header_bits += field_type.bit_width
1161
1162 data_fields = []
1163 data_bits = 0
1164 for field in data_frame.fields:
1165 if field.name == list_field_name:
1166 data_fields.append((list_field_name, list_type.element_type))
1167 data_bits += list_type.element_type.bit_width
1168 else:
1169 field_type = field_map[field.name]
1170 data_fields.append((field.name, field_type))
1171 data_bits += field_type.bit_width
1172
1173 # The frame/union width is the wider of the two variants' content. The
1174 # byte-array storage rounds that up to whole bytes (any extra high bits
1175 # stay zero); both frames share it so `sizeof(header) == sizeof(data)`.
1176 frame_bits = max(header_bits, data_bits)
1177 frame_bytes = (frame_bits + 7) // 8
1178
1179 return {
1180 "ctor_params": ctor_params,
1181 "count_cpp": count_cpp,
1182 "count_field_name": count_field_name,
1183 "count_width": count_width,
1184 "data_fields": data_fields,
1185 "element_cpp": self._cpp_type(list_type.element_type),
1186 "frame_bits": frame_bits,
1187 "frame_bytes": frame_bytes,
1188 "header_fields": header_fields,
1189 "list_field_name": list_field_name,
1190 "window_name": self.type_id_map[window_type],
1191 }
1192
1194 self, struct_type: types.StructType) -> Tuple[Dict[str, int], int]:
1195 """Return `(bit_offset_by_name, total_bits)` for non-void fields.
1196
1197 Fields are laid out LSB-first in wire order, which is reversed manifest
1198 order when `cpp_type.reverse` is set. Caller already knows each field's
1199 type/width from `struct_type.fields`; we only need the per-name offset
1200 and the resulting total bit width to size the storage array.
1201 """
1202 declared = struct_type.fields
1203 if struct_type.cpp_type.reverse:
1204 declared = list(reversed(declared))
1205 bit_offsets: Dict[str, int] = {}
1206 bit_offset = 0
1207 for field_name, field_type in declared:
1208 if self._cpp_type(field_type) == "void":
1209 continue
1210 bit_offsets[field_name] = bit_offset
1211 bit_offset += field_type.bit_width
1212 return bit_offsets, bit_offset
1213
1214 def _is_signed_int_field(self, field_type: types.ESIType) -> bool:
1215 """True if the field is a signed integer (only `IntType`, not `UIntType`
1216 or `BitsType`)."""
1217 wrapped = self._unwrap_aliases(field_type)
1218 return (isinstance(wrapped, types.IntType) and
1219 not isinstance(wrapped, types.UIntType))
1220
1221 def _emit_view_store(self, w: IndentedWriter, raw_member: str, src: str,
1222 off_expr: str, width: int) -> None:
1223 """Emit a loop writing the low `width` bits of the view `src` into
1224 `raw_member`, with bit `b` landing at wire bit `off_expr + b`. Shared by
1225 the scalar view-class field setter and the per-element view-array setter.
1226 """
1227 w.line(f"const std::size_t n = "
1228 f"std::min<std::size_t>({src}.width(), {width});")
1229 with w.block(f"for (std::size_t b = 0; b < {width}; ++b)"):
1230 w.line(f"const std::size_t g = {off_expr} + b;")
1231 w.line(f"const bool val = (b < n) && {src}.getBit(b);")
1232 w.line("if (val)")
1233 with w.indented():
1234 w.line(f"{raw_member}[g / 8] |= "
1235 f"static_cast<uint8_t>(uint8_t{{1}} << (g % 8));")
1236 w.line("else")
1237 with w.indented():
1238 w.line(f"{raw_member}[g / 8] &= "
1239 f"static_cast<uint8_t>(~(uint8_t{{1}} << (g % 8)));")
1240
1241 def _emit_field_accessor(self, w: IndentedWriter, raw_member: str,
1242 self_type: str, field_name: str,
1243 field_type: types.ESIType, bit_offset: int,
1244 bit_width: int) -> None:
1245 """Emit a getter/setter pair that reads/writes `field_name` out of the
1246 raw bytes member `raw_member` at `bit_offset` / `bit_width`.
1247
1248 Setters share the field's name (no `set_` prefix) and return
1249 `self_type &` so the caller can chain calls (e.g.
1250 `Foo{}.a(1).b(2).inner(x)`).
1251
1252 For integer fields the emitter picks between three inline strategies:
1253
1254 * Native ints (<= 64 bits). Delegate to the compile-time
1255 `esi::detail::{read,write}{Un,}signedBits` helpers from
1256 `esi/BitAccess.h`, which loop over the constituent bits at the
1257 field's constant offset/width. The non-type template parameters
1258 let the optimiser fully unroll the loop, so a byte-aligned
1259 standard-width field still collapses to a single load/store on
1260 -O1+ -- no special-cased `memcpy` / per-byte fast path required.
1261 * `bool` (a single bit). Same helpers, specialised to read/write one
1262 bit and hand back a `bool`.
1263 * View-class fields. Triggered when `_is_value_class_type` is true --
1264 currently widths above 64 bits, for any of `BitsType`, signed, or
1265 unsigned. Returns a non-owning
1266 `esi::BitVector` / `esi::IntView` / `esi::UIntView` view *into*
1267 the parent struct's `_bytes` -- zero allocation, no copy. The
1268 setter accepts any `esi::BitVector` (so views and owning
1269 subclasses both work) and writes back bit-by-bit. The returned
1270 view dangles when the parent buffer dies; see the lifetime warning
1271 at the top of the generated header.
1272 """
1273 field_cpp = self._cpp_type(field_type)
1274 if field_cpp == "void":
1275 return
1276
1277 wrapped = self._unwrap_aliases(field_type)
1278
1279 if isinstance(wrapped, (types.BitsType, types.IntType)):
1280 # View-class field. Today this is exactly the wider-than-64-bit
1281 # integer / Bits cases; gated by `_is_value_class_type` so the rule
1282 # lives in one place.
1283 if self._is_value_class_type(field_type):
1284 byte_offset = bit_offset // 8
1285 sub_bit_index = bit_offset % 8
1286 # Number of bytes the view needs to span: the high bit is at
1287 # `sub_bit_index + bit_width - 1`, so we cover
1288 # ceil((sub_bit_index + bit_width) / 8) bytes starting at
1289 # `_bytes.data() + byte_offset`.
1290 span_bytes = (sub_bit_index + bit_width + 7) // 8
1291 with w.block(f"{field_cpp} {field_name}() const"):
1292 w.line(f"return {field_cpp}(")
1293 w.line(f" std::span<const uint8_t>("
1294 f"{raw_member}.data() + {byte_offset}, {span_bytes}),")
1295 w.line(f" static_cast<std::size_t>({bit_width}),")
1296 w.line(f" static_cast<uint8_t>({sub_bit_index}));")
1297 with w.block(f"{self_type} &{field_name}(const {field_cpp} &v)"):
1298 # Walk the input view bit-by-bit and write each bit straight
1299 # into `_bytes` at its target position.
1300 self._emit_view_store(w, raw_member, "v", str(bit_offset), bit_width)
1301 w.line("return *this;")
1302 return
1303
1304 # `bool` is the storage choice for a single bit; bit-precise helpers
1305 # are the simplest fit and the optimiser collapses them on -O2.
1306 if bit_width == 1 and field_cpp == "bool":
1307 with w.block(f"bool {field_name}() const"):
1308 w.line(f"return esi::detail::readUnsignedBits<uint8_t, "
1309 f"{bit_offset}, 1>({raw_member}.data()) != 0;")
1310 with w.block(f"{self_type} &{field_name}(bool v)"):
1311 w.line(f"esi::detail::writeUnsignedBits<uint8_t, "
1312 f"{bit_offset}, 1>({raw_member}.data(), "
1313 f"static_cast<uint8_t>(v) & uint8_t{{1}});")
1314 w.line("return *this;")
1315 return
1316
1317 signed = self._is_signed_int_field(field_type)
1318 kind = "Signed" if signed else "Unsigned"
1319
1320 # Delegate to the generic compile-time bit helpers from
1321 # `esi/BitAccess.h`. They read/write `Width` bits starting at the
1322 # field's constant `BitOffset` (LSB-first) for any width up to 64 --
1323 # byte-aligned or not -- and sign-extend signed fields. Because
1324 # `BitOffset` / `Width` are non-type template parameters the optimiser
1325 # fully unrolls the per-bit loop, so byte-aligned standard-width fields
1326 # (`ui8` / `ui32` / ...) still collapse to a single load/store on
1327 # -O1+, so a single helper covers every width without any
1328 # hand-written byte-copy fast paths.
1329 with w.block(f"{field_cpp} {field_name}() const"):
1330 w.line(f"return esi::detail::read{kind}Bits<{field_cpp}, "
1331 f"{bit_offset}, {bit_width}>({raw_member}.data());")
1332 with w.block(f"{self_type} &{field_name}({field_cpp} v)"):
1333 w.line(f"esi::detail::write{kind}Bits<{field_cpp}, "
1334 f"{bit_offset}, {bit_width}>({raw_member}.data(), v);")
1335 w.line("return *this;")
1336 return
1337
1338 # Aggregate field (struct / union / array). The inner aggregate stores
1339 # its bits LSB-first in its own `_bytes` buffer, so reading/writing a
1340 # struct or union reduces to copying `bit_width` bits between the parent
1341 # buffer at `bit_offset` and the inner buffer at bit 0. Arrays are
1342 # delegated to `_emit_array_accessor`, a recursive per-element (un)packer
1343 # that places every scalar leaf at its true wire offset -- so sub-byte
1344 # ints, odd widths, sub-byte aggregates, and *arbitrarily nested* arrays
1345 # of those all round-trip, instead of being flat-copied (which only works
1346 # when the C++ element layout already matches the wire, e.g. `4 x ui8`).
1347 assert bit_width >= 0, (
1348 f"field '{field_name}': unbounded aggregate field reached the "
1349 f"emitter; the planner should have excluded the parent struct "
1350 f"via `_contains_unbounded`")
1351
1352 # Arrays of plain (non view-class) elements: one uniform recursion.
1353 if (isinstance(wrapped, types.ArrayType) and
1354 not self._is_value_class_type(wrapped.element_type)):
1355 self._emit_array_accessor(w, raw_member, self_type, field_name, wrapped,
1356 bit_offset)
1357 return
1358
1359 # An array whose element is a >64-bit view class is handled by the
1360 # dedicated span / lazy-range accessors at the end of this method.
1361 is_view_array = (isinstance(wrapped, types.ArrayType) and
1362 self._is_value_class_type(wrapped.element_type))
1363 byte_aligned = (bit_offset % 8 == 0 and bit_width % 8 == 0)
1364 byte_offset = bit_offset // 8
1365 byte_width = (bit_width + 7) // 8
1366
1367 if not is_view_array:
1368 if byte_aligned:
1369 # Byte-aligned: direct byte copy between the parent's buffer slice
1370 # and the inner aggregate's `_bytes`. An explicit per-byte loop
1371 # avoids `memcpy` while still collapsing to one on -O2.
1372 with w.block(f"{field_cpp} {field_name}() const"):
1373 w.line(f"{field_cpp} out{{}};")
1374 w.line(f"for (std::size_t i = 0; i < {byte_width}; ++i)")
1375 with w.indented():
1376 w.line(f"reinterpret_cast<uint8_t *>(&out)[i] = "
1377 f"{raw_member}[{byte_offset} + i];")
1378 w.line("return out;")
1379 with w.block(f"{self_type} &{field_name}(const {field_cpp} &v)"):
1380 w.line(f"for (std::size_t i = 0; i < {byte_width}; ++i)")
1381 with w.indented():
1382 w.line(f"{raw_member}[{byte_offset} + i] = "
1383 f"reinterpret_cast<const uint8_t *>(&v)[i];")
1384 w.line("return *this;")
1385 else:
1386 # Non-byte-aligned: delegate to the per-bit copy helpers. The
1387 # inner's `out{}` zero-initialiser is required by `copyBitsIn`,
1388 # which only OR-sets the `1` bits.
1389 with w.block(f"{field_cpp} {field_name}() const"):
1390 w.line(f"{field_cpp} out{{}};")
1391 w.line(f"esi::detail::copyBitsIn<{bit_offset}, "
1392 f"{bit_width}>({raw_member}.data(), "
1393 f"reinterpret_cast<uint8_t *>(&out));")
1394 w.line("return out;")
1395 with w.block(f"{self_type} &{field_name}(const {field_cpp} &v)"):
1396 w.line(f"esi::detail::copyBitsOut<{bit_offset}, "
1397 f"{bit_width}>({raw_member}.data(), "
1398 f"reinterpret_cast<const uint8_t *>(&v));")
1399 w.line("return *this;")
1400
1401 # Indexed accessors for arrays whose element is one of the view
1402 # classes (`BitVector` / `IntView` / `UIntView`). The whole-array
1403 # byte-copy path is intentionally skipped (the view's storage layout
1404 # doesn't match the wire layout). Instead we emit three accessors:
1405 #
1406 # * `field(i)` / `field(i, v)` -- per-element getter (zero-copy
1407 # view into the relevant slice of `_bytes`) and setter
1408 # (runtime per-bit copy, since `copyBitsOut` needs a
1409 # compile-time offset and the array index is runtime).
1410 # * `field()` -- a lazy `std::views::iota | std::views::transform`
1411 # range that yields the per-element views on demand. Random
1412 # access, no allocation, no element materialised until accessed.
1413 # * `field(const std::array<view, N> &)` -- whole-array setter
1414 # that delegates to the per-element setter N times. Lets the
1415 # emplace-style struct ctor accept view-array fields as a
1416 # single argument.
1417 #
1418 # All three share the parent struct's lifetime; see the warning at
1419 # the top of the generated header.
1420 if is_view_array:
1421 assert isinstance(wrapped, types.ArrayType)
1422 elem_type = wrapped.element_type
1423 elem_cpp = self._cpp_type(elem_type)
1424 elem_width = elem_type.bit_width
1425 elem_count = wrapped.size
1426 with w.block(f"{elem_cpp} {field_name}(std::size_t i) const"):
1427 w.line(f"const std::size_t bit_off = {bit_offset} + i * {elem_width};")
1428 w.line(f"return {elem_cpp}(")
1429 w.line(f" std::span<const uint8_t>("
1430 f"{raw_member}.data() + bit_off / 8,")
1431 w.line(f" "
1432 f"(bit_off % 8 + {elem_width} + 7) / 8),")
1433 w.line(f" static_cast<std::size_t>({elem_width}),")
1434 w.line(f" static_cast<uint8_t>(bit_off % 8));")
1435 with w.block(f"{self_type} &{field_name}(std::size_t i, "
1436 f"const {elem_cpp} &v)"):
1437 w.line(f"const std::size_t bit_off = {bit_offset} + i * {elem_width};")
1438 self._emit_view_store(w, raw_member, "v", "bit_off", elem_width)
1439 w.line("return *this;")
1440 # Lazy whole-array view: `iota(0, N) | transform([this](i){ ... })`
1441 # gives a random-access range of `elem_cpp` views computed on
1442 # access, with zero up-front allocation.
1443 with w.block(f"auto {field_name}() const"):
1444 w.line(f"return std::views::iota("
1445 f"std::size_t{{0}}, std::size_t{{{elem_count}}}) |")
1446 w.line(" std::views::transform([this](std::size_t i) {")
1447 w.line(f" return this->{field_name}(i);")
1448 w.line(" });")
1449 # Whole-array setter: forwards to the per-element setter for
1450 # each index.
1451 with w.block(f"{self_type} &{field_name}("
1452 f"const std::array<{elem_cpp}, {elem_count}> &v)"):
1453 w.line(f"for (std::size_t i = 0; i < {elem_count}; ++i)")
1454 with w.indented():
1455 w.line(f"{field_name}(i, v[i]);")
1456 w.line("return *this;")
1457
1458 def _emit_array_accessor(self, w: IndentedWriter, raw_member: str,
1459 self_type: str, field_name: str,
1460 array_type: types.ArrayType,
1461 bit_offset: int) -> None:
1462 """Emit the accessor set for a fixed-size array field.
1463
1464 Four accessors are emitted, matching the existing array API:
1465
1466 * `Elem field(std::size_t i)` / `Self &field(std::size_t i, const Elem &)`
1467 -- per-element get/set, where `Elem` is the (possibly itself an
1468 array) element type.
1469 * `Whole field()` / `Self &field(const Whole &)` -- whole-array
1470 get/set, expressed in terms of the per-element accessors.
1471
1472 The per-element get/set bodies are produced by `_emit_unpack` /
1473 `_emit_pack`, which recurse to the scalar leaves and place each leaf at
1474 its true wire offset `bit_offset + i * elemWidth (+ ...)`. This is what
1475 lets sub-byte, odd-width, sub-byte-aggregate, and *arbitrarily nested*
1476 array elements round-trip even when the C++ element layout does not
1477 match the wire layout.
1478 """
1479 elem_type = array_type.element_type
1480 elem_cpp = self._cpp_type(elem_type)
1481 elem_width = elem_type.bit_width
1482 elem_count = array_type.size
1483 whole_cpp = self._cpp_type(array_type)
1484 elem_off = f"{bit_offset} + i * {elem_width}"
1485
1486 with w.block(f"{elem_cpp} {field_name}(std::size_t i) const"):
1487 w.line(f"{elem_cpp} out{{}};")
1488 self._emit_unpack(w, raw_member, "out", elem_off, elem_type, 1)
1489 w.line("return out;")
1490
1491 with w.block(f"{self_type} &{field_name}(std::size_t i, "
1492 f"const {elem_cpp} &v)"):
1493 self._emit_pack(w, raw_member, "v", elem_off, elem_type, 1)
1494 w.line("return *this;")
1495
1496 with w.block(f"{whole_cpp} {field_name}() const"):
1497 w.line(f"{whole_cpp} out{{}};")
1498 w.line(f"for (std::size_t i = 0; i < {elem_count}; ++i)")
1499 with w.indented():
1500 w.line(f"out[i] = {field_name}(i);")
1501 w.line("return out;")
1502
1503 with w.block(f"{self_type} &{field_name}(const {whole_cpp} &v)"):
1504 w.line(f"for (std::size_t i = 0; i < {elem_count}; ++i)")
1505 with w.indented():
1506 w.line(f"{field_name}(i, v[i]);")
1507 w.line("return *this;")
1508
1509 def _emit_unpack(self, w: IndentedWriter, raw_member: str, dst: str, off: str,
1510 esi_type: types.ESIType, depth: int) -> None:
1511 """Emit statements that read a value of `esi_type` out of `raw_member`
1512 at wire bit offset `off` (a C++ `size_t` expression) into the
1513 already-declared, zero-initialised C++ lvalue `dst`.
1514
1515 Recurses through arrays (one loop per dimension) down to scalar / struct
1516 / union leaves, which are read with the runtime-offset bit helpers.
1517 """
1518 wrapped = self._unwrap_aliases(esi_type)
1519 if isinstance(wrapped, types.ArrayType):
1520 idx = f"i{depth}"
1521 ew = wrapped.element_type.bit_width
1522 with w.block(f"for (std::size_t {idx} = 0; {idx} < "
1523 f"{wrapped.size}; ++{idx})"):
1524 self._emit_unpack(w, raw_member, f"{dst}[{idx}]",
1525 f"{off} + {idx} * {ew}", wrapped.element_type,
1526 depth + 1)
1527 return
1528 if isinstance(wrapped, (types.StructType, types.UnionType)):
1529 # The inner aggregate's own `_bytes` already mirror the wire layout, so
1530 # copy its bits straight in. `dst` is zero-initialised by the caller,
1531 # as `copyBitsInDyn` only OR-sets the `1` bits.
1532 w.line(f"esi::detail::copyBitsInDyn({raw_member}.data(), {off},")
1533 w.line(f" reinterpret_cast<uint8_t *>(&{dst}), {wrapped.bit_width});")
1534 return
1535 if isinstance(wrapped, (types.BitsType, types.IntType)):
1536 if self._is_value_class_type(esi_type):
1537 raise NotImplementedError(
1538 "arrays of integers wider than 64 bits are not supported")
1539 cpp = self._cpp_type(esi_type)
1540 if cpp == "bool":
1541 w.line(f"{dst} = esi::detail::readUnsignedBitsDyn<uint8_t>("
1542 f"{raw_member}.data(), {off}, 1) != 0;")
1543 elif self._is_signed_int_field(esi_type):
1544 w.line(f"{dst} = esi::detail::readSignedBitsDyn<{cpp}>("
1545 f"{raw_member}.data(), {off}, {wrapped.bit_width});")
1546 else:
1547 w.line(f"{dst} = esi::detail::readUnsignedBitsDyn<{cpp}>("
1548 f"{raw_member}.data(), {off}, {wrapped.bit_width});")
1549 return
1550 raise NotImplementedError(
1551 f"unsupported array element type '{wrapped}' for C++ generation")
1552
1553 def _emit_pack(self, w: IndentedWriter, raw_member: str, src: str, off: str,
1554 esi_type: types.ESIType, depth: int) -> None:
1555 """Inverse of `_emit_unpack`: write the C++ value `src` of `esi_type`
1556 into `raw_member` at wire bit offset `off`."""
1557 wrapped = self._unwrap_aliases(esi_type)
1558 if isinstance(wrapped, types.ArrayType):
1559 idx = f"i{depth}"
1560 ew = wrapped.element_type.bit_width
1561 with w.block(f"for (std::size_t {idx} = 0; {idx} < "
1562 f"{wrapped.size}; ++{idx})"):
1563 self._emit_pack(w, raw_member, f"{src}[{idx}]", f"{off} + {idx} * {ew}",
1564 wrapped.element_type, depth + 1)
1565 return
1566 if isinstance(wrapped, (types.StructType, types.UnionType)):
1567 w.line(f"esi::detail::copyBitsOutDyn({raw_member}.data(), {off},")
1568 w.line(f" reinterpret_cast<const uint8_t *>(&{src}), "
1569 f"{wrapped.bit_width});")
1570 return
1571 if isinstance(wrapped, (types.BitsType, types.IntType)):
1572 if self._is_value_class_type(esi_type):
1573 raise NotImplementedError(
1574 "arrays of integers wider than 64 bits are not supported")
1575 cpp = self._cpp_type(esi_type)
1576 if cpp == "bool":
1577 w.line(f"esi::detail::writeUnsignedBitsDyn<uint8_t>("
1578 f"{raw_member}.data(), "
1579 f"static_cast<uint8_t>({src}) & uint8_t{{1}}, {off}, 1);")
1580 elif self._is_signed_int_field(esi_type):
1581 w.line(f"esi::detail::writeSignedBitsDyn<{cpp}>("
1582 f"{raw_member}.data(), {src}, {off}, {wrapped.bit_width});")
1583 else:
1584 w.line(f"esi::detail::writeUnsignedBitsDyn<{cpp}>("
1585 f"{raw_member}.data(), {src}, {off}, {wrapped.bit_width});")
1586 return
1587 raise NotImplementedError(
1588 f"unsupported array element type '{wrapped}' for C++ generation")
1589
1590 def _ctor_param_type(self, field_type: types.ESIType) -> str:
1591 """Return the C++ constructor-parameter type for a setter call."""
1592 field_cpp = self._cpp_type(field_type)
1593 wrapped = self._unwrap_aliases(field_type)
1594 if isinstance(wrapped, (types.BitsType, types.IntType)):
1595 return field_cpp
1596 return f"const {field_cpp} &"
1597
1598 def _emit_struct(self, w: IndentedWriter,
1599 struct_type: types.StructType) -> None:
1600 """Emit a packed struct as a raw byte buffer with bit-precise accessors.
1601
1602 The struct's storage is a single `std::array<uint8_t, N>` (where `N`
1603 is the byte width derived from the manifest) whose layout matches the
1604 on-wire bit-packing exactly. Per-field accessors (generated below)
1605 read/write the bits so the wire format does not depend on C++ bit-field
1606 allocation rules, which differ between the Itanium ABI (GCC/Clang) and MSVC.
1607 """
1608 # Zero-width composite types collapse to `void` everywhere they appear
1609 # (see _cpp_type), so there is nothing meaningful to emit here.
1610 if struct_type.bit_width == 0:
1611 return
1612 struct_name = self.type_id_map[struct_type]
1613 bit_offsets, total_bits = self._compute_field_bit_offsets(struct_type)
1614 total_bytes = (total_bits + 7) // 8
1615
1616 # Logical-order field list (manifest order) for the constructor and the
1617 # public accessor list.
1618 logical_fields = [(name, ftype)
1619 for name, ftype in struct_type.fields
1620 if self._cpp_type(ftype) != "void"]
1621
1622 # A struct whose every field collapses to void carries no data the
1623 # generated header can expose. Emit nothing rather than a 1-byte
1624 # placeholder, since (a) the placeholder doesn't reflect any real wire
1625 # layout and (b) a size_assert built from the manifest's `bit_width`
1626 # (which includes void padding) would fail to compile.
1627 if not logical_fields:
1628 return
1629
1630 with w.block(f"struct {struct_name}", tail=";"):
1631 # `_bytes` holds the wire layout and is private; the only legitimate
1632 # external view of those bytes is via `MessageData::from()` /
1633 # `MessageData::as()` which reinterpret-cast through the whole struct
1634 # and don't need member-level access.
1635 w.access("private:")
1636 w.line(f"std::array<uint8_t, {max(total_bytes, 1)}> _bytes{{}};")
1637 w.line()
1638 w.access("public:")
1639 w.line(f"{struct_name}() = default;")
1640 ctor_params = ", ".join(f"{self._ctor_param_type(ftype)} {name}"
1641 for name, ftype in logical_fields)
1642 with w.block(f"{struct_name}({ctor_params})"):
1643 # `this->` disambiguates the chained setter calls from the like-named
1644 # parameters that shadow the member functions inside the ctor body.
1645 chain = ".".join(f"{name}({name})" for name, _ in logical_fields)
1646 w.line(f"this->{chain};")
1647 w.line()
1648
1649 # Per-field accessors in logical (manifest) order so the user-facing
1650 # API mirrors the manifest field order rather than the wire reversal.
1651 for name, ftype in logical_fields:
1652 self._emit_field_accessor(w, "_bytes", struct_name, name, ftype,
1653 bit_offsets[name], ftype.bit_width)
1654 w.line()
1655
1656 w.line(f"static constexpr std::string_view _ESI_ID = "
1657 f"{self._cpp_string_literal(struct_type.id)};")
1658 expected_bytes = self._safe_byte_width(struct_type)
1659 if expected_bytes is not None and expected_bytes > 0:
1660 self._emit_size_assert(w, struct_name, expected_bytes)
1661 w.line()
1662
1663 def _emit_union(self, w: IndentedWriter, union_type: types.UnionType) -> None:
1664 """Emit a union as a raw byte buffer with per-variant accessors.
1665
1666 The union's storage is a single `std::array<uint8_t, N>` sized to the
1667 widest variant. Each variant starts at bit zero, matching the hw.union
1668 bitcast layout. Accessors use the exact member bit width and preserve
1669 bits outside that member.
1670 """
1671 # Zero-width unions collapse to `void` (see _cpp_type) so there is
1672 # nothing meaningful to emit here.
1673 if union_type.bit_width == 0:
1674 return
1675 union_name = self.type_id_map[union_type]
1676 union_bytes = self._field_byte_width(union_type)
1677
1678 with w.block(f"struct {union_name}", tail=";"):
1679 # See `_emit_struct` for the access-control rationale.
1680 w.access("private:")
1681 w.line(f"std::array<uint8_t, {union_bytes}> _bytes{{}};")
1682 w.line()
1683 w.access("public:")
1684 w.line(f"{union_name}() = default;")
1685 w.line()
1686
1687 for field_name, field_type in union_type.fields:
1688 field_cpp = self._cpp_type(field_type)
1689 if field_cpp == "void":
1690 continue
1691 self._emit_field_accessor(w, "_bytes", union_name, field_name,
1692 field_type, 0, field_type.bit_width)
1693 w.line()
1694
1695 w.line(f"static constexpr std::string_view _ESI_ID = "
1696 f"{self._cpp_string_literal(union_type.id)};")
1697 if union_bytes > 0:
1698 self._emit_size_assert(w, union_name, union_bytes)
1699 w.line()
1700
1702 self, fields: List[Tuple[str, Optional[types.ESIType]]], frame_bits: int,
1703 count_type_synth: types.ESIType
1704 ) -> List[Tuple[str, types.ESIType, int, int]]:
1705 """Compute (name, type, bit_offset, bit_width) for each window frame field.
1706
1707 Fields are listed in declared (MSB-first) struct order. The last field
1708 starts at bit zero, matching how CIRCT lowers the serial-window frame
1709 union. Earlier fields sit immediately above it with no inter-field
1710 padding, and any slack (`frame_bits - content_bits`) is at the MSB end.
1711 The returned `bit_offset` is the field's LSB position within the frame's
1712 little-endian `_bytes` array, ready to hand straight to
1713 `_emit_field_accessor`.
1714
1715 The sentinel `(name, None)` count field is materialised as
1716 `count_type_synth` so it can flow through the standard
1717 `_emit_field_accessor` path.
1718
1719 Raises `ValueError` if a field is unbounded (`bit_width < 0`) or if the
1720 accumulated content overflows `frame_bits` -- either would produce bogus
1721 offsets, so fail fast rather than emit silently-wrong accessors.
1722 """
1723 layout = []
1724 bit_offset = 0
1725 for name, ftype in reversed(fields):
1726 actual_type = count_type_synth if ftype is None else ftype
1727 bit_width = actual_type.bit_width
1728 if bit_width < 0:
1729 raise ValueError(
1730 f"window frame field '{name}' has an unbounded width; window "
1731 f"codegen requires every frame field to be fixed-width")
1732 if bit_offset + bit_width > frame_bits:
1733 raise ValueError(
1734 f"window frame field '{name}' overflows the {frame_bits}-bit frame "
1735 f"(content is wider than the frame width)")
1736 layout.append((name, actual_type, bit_offset, bit_width))
1737 bit_offset += bit_width
1738 layout.reverse()
1739 return layout
1740
1742 self, w: IndentedWriter, frame_name: str, frame_bytes: int,
1743 layout: List[Tuple[str, types.ESIType, int, int]]) -> None:
1744 """Emit a window header/data frame as a raw-bytes struct with accessors.
1745
1746 Nested inside the window helper class, it uses the same accessor pattern
1747 as top-level structs/unions: a private `_bytes` array plus per-field
1748 getter/setter pairs returning `frame_name &` to allow chaining.
1749 """
1750 with w.block(f"struct {frame_name}", tail=";"):
1751 w.access("private:")
1752 w.line(f"std::array<uint8_t, {frame_bytes}> _bytes{{}};")
1753 w.line()
1754 w.access("public:")
1755 for name, ftype, bit_offset, bit_width in layout:
1756 self._emit_field_accessor(w, "_bytes", frame_name, name, ftype,
1757 bit_offset, bit_width)
1758 w.line()
1759 self._emit_size_assert(w, frame_name, frame_bytes)
1760
1761 def _emit_window(self, hdr: IndentedWriter,
1762 window_type: types.WindowType) -> None:
1763 """Emit a SegmentedMessageData helper for a serial list window.
1764
1765 This emitter builds its own pre-indented text, so it uses the writer
1766 purely as a verbatim `hdr.write(...)` sink. The nested frame structs,
1767 however, are produced by the line-based `_emit_window_frame`, so their
1768 calls are wrapped in `hdr.indented()` to place them at the window
1769 class's member indent.
1770 """
1771 info = self._analyze_window(window_type)
1772 ctor_params = [
1773 self._format_window_ctor_param(name, field_type)
1774 for name, field_type in info["ctor_params"]
1775 ]
1776 value_ctor_params = list(ctor_params)
1777 value_ctor_params.append(
1778 f"const std::vector<value_type> &{info['list_field_name']}")
1779 value_ctor_signature = ", ".join(value_ctor_params)
1780 frame_ctor_params = list(ctor_params)
1781 frame_ctor_params.append("std::vector<data_frame> frames")
1782 frame_ctor_signature = ", ".join(frame_ctor_params)
1783 helper_args = ", ".join(name for name, _ in info["ctor_params"])
1784 helper_call = f"{helper_args}, std::move(frames)" if helper_args else "std::move(frames)"
1785
1786 # Synthesise an unsigned integer type for the sentinel "count" header
1787 # field so it flows through `_emit_field_accessor` like any other
1788 # integer.
1789 count_type_synth = types.UIntType(f"_count_ui{info['count_width']}",
1790 info["count_width"])
1791 data_layout = self._compute_window_frame_layout(info["data_fields"],
1792 info["frame_bits"],
1793 count_type_synth)
1794 header_layout = self._compute_window_frame_layout(info["header_fields"],
1795 info["frame_bits"],
1796 count_type_synth)
1797
1798 # Largest list length encodable in a single burst's count field. Lists
1799 # longer than this are split across multiple header/data bursts (the
1800 # read-side `SerialListTypeDeserializer` reassembles them), each burst
1801 # carrying at most this many data frames. `count_width` is <= 64 (the
1802 # count field's storage type tops out at 64 bits), so this literal always
1803 # fits in a `size_t`.
1804 max_batch_val = (1 << info["count_width"]) - 1
1805
1806 hdr.write(
1807 f"struct {info['window_name']} : public esi::SegmentedMessageData {{\n")
1808 hdr.write("public:\n")
1809 hdr.write(f" using value_type = {info['element_cpp']};\n")
1810 hdr.write(f" using count_type = {info['count_cpp']};\n\n")
1811 with hdr.indented():
1812 self._emit_window_frame(hdr, "data_frame", info["frame_bytes"],
1813 data_layout)
1814 hdr.write("\n")
1815 hdr.write("private:\n")
1816 with hdr.indented():
1817 self._emit_window_frame(hdr, "header_frame", info["frame_bytes"],
1818 header_layout)
1819 hdr.write("\n")
1820 hdr.write(" // One header per burst (bursts 2..N are continuations whose\n"
1821 " // only meaningful field is the count); the data frames of\n"
1822 " // every burst are stored contiguously in `data_frames` and\n"
1823 " // sliced per burst by `segment()`.\n")
1824 hdr.write(" std::vector<header_frame> headers;\n")
1825 hdr.write(" std::vector<data_frame> data_frames;\n")
1826 hdr.write(" header_frame footer{};\n\n")
1827 hdr.write(" // Largest list length encodable in one burst's count field.\n"
1828 " // Longer lists are chunked across multiple bursts.\n")
1829 hdr.write(f" static constexpr size_t _kMaxBatch = {max_batch_val}ULL;\n\n")
1830 hdr.write(f" void construct({frame_ctor_signature}) {{\n")
1831 hdr.write(" if (frames.empty())\n")
1832 hdr.write(
1833 f" throw std::invalid_argument(\"{info['window_name']}: bulk windowed lists cannot be empty\");\n"
1834 )
1835 hdr.write(" data_frames = std::move(frames);\n")
1836 hdr.write(" // Split the list into bursts no larger than the count\n"
1837 " // field can encode. The read side reassembles the bursts\n"
1838 " // back into a single list.\n")
1839 hdr.write(" const size_t total = data_frames.size();\n")
1840 hdr.write(
1841 " const size_t numBatches = (total + _kMaxBatch - 1) / _kMaxBatch;\n"
1842 )
1843 hdr.write(" headers.assign(numBatches, header_frame{});\n")
1844 hdr.write(" for (size_t b = 0; b < numBatches; ++b) {\n")
1845 hdr.write(
1846 " const size_t batchSize =\n"
1847 " std::min<size_t>(_kMaxBatch, total - b * _kMaxBatch);\n")
1848 hdr.write(
1849 f" headers[b].{info['count_field_name']}(static_cast<count_type>(batchSize));\n"
1850 )
1851 hdr.write(" }\n")
1852 hdr.write(
1853 " // Only the first burst's header carries the static fields.\n")
1854 for name, _ in info["ctor_params"]:
1855 hdr.write(f" headers.front().{name}({name});\n")
1856 hdr.write(f" footer.{info['count_field_name']}(0);\n")
1857 hdr.write(" }\n\n")
1858 hdr.write("public:\n")
1859 hdr.write(f" {info['window_name']}({frame_ctor_signature}) {{\n")
1860 hdr.write(f" construct({helper_call});\n")
1861 hdr.write(" }\n\n")
1862 hdr.write(f" {info['window_name']}({value_ctor_signature}) {{\n")
1863 hdr.write(" std::vector<data_frame> frames;\n")
1864 hdr.write(f" frames.reserve({info['list_field_name']}.size());\n")
1865 hdr.write(f" for (const auto &element : {info['list_field_name']}) {{\n")
1866 hdr.write(" auto &frame = frames.emplace_back();\n")
1867 hdr.write(f" frame.{info['list_field_name']}(element);\n")
1868 hdr.write(" }\n")
1869 hdr.write(f" construct({helper_call});\n")
1870 hdr.write(" }\n\n")
1871 hdr.write(" size_t numSegments() const override {\n"
1872 " return 2 * headers.size() + 1;\n"
1873 " }\n")
1874 hdr.write(" esi::Segment segment(size_t idx) const override {\n")
1875 hdr.write(" const size_t footerIdx = 2 * headers.size();\n")
1876 hdr.write(" if (idx == footerIdx)\n")
1877 hdr.write(
1878 " return {reinterpret_cast<const uint8_t *>(&footer), sizeof(footer)};\n"
1879 )
1880 hdr.write(" if (idx > footerIdx)\n")
1881 hdr.write(
1882 f" throw std::out_of_range(\"{info['window_name']}: invalid segment index\");\n"
1883 )
1884 hdr.write(" const size_t b = idx / 2;\n")
1885 hdr.write(" if (idx % 2 == 0)\n")
1886 hdr.write(" return {reinterpret_cast<const uint8_t *>(&headers[b]),\n"
1887 " sizeof(header_frame)};\n")
1888 hdr.write(" const size_t start = b * _kMaxBatch;\n")
1889 hdr.write(
1890 " const size_t batchSize =\n"
1891 " std::min<size_t>(_kMaxBatch, data_frames.size() - start);\n")
1892 hdr.write(
1893 " return {reinterpret_cast<const uint8_t *>(data_frames.data() + start),\n"
1894 " batchSize * sizeof(data_frame)};\n")
1895 hdr.write(" }\n\n")
1896 hdr.write(
1897 f" static constexpr std::string_view _ESI_ID = {self._cpp_string_literal(self._unwrap_aliases(window_type.into_type).id)};\n"
1898 )
1899 # The into-type id alone cannot distinguish two different windows over
1900 # the same underlying struct (e.g. serial vs. parallel list encoding).
1901 # Emit the window id so the runtime can verify the wire format too.
1902 hdr.write(
1903 f" static constexpr std::string_view _ESI_WINDOW_ID = {self._cpp_string_literal(window_type.id)};\n"
1904 )
1905 self._emit_window_data_accessors(hdr, info)
1906 self._emit_window_deserializer(hdr, info)
1907 hdr.write("};\n\n")
1908
1909 def _emit_window_data_accessors(self, hdr: IndentedWriter,
1910 info: Dict[str, Any]) -> None:
1911 """Emit accessors for the header and data fields of a window helper.
1912
1913 Exposes each static header field as a scalar accessor, the count of data
1914 frames, and one vector-valued accessor per data field so decoded values
1915 are easy to inspect on the read side.
1916 """
1917 list_field_name = info["list_field_name"]
1918 hdr.write("\n")
1919 for field_name, field_type in info["header_fields"]:
1920 # Skip the synthetic bulk-count field; it is exposed via
1921 # `<list>_count()` below.
1922 if field_type is None:
1923 continue
1924 cpp = self._cpp_type(field_type)
1925 # The header-frame accessor returns by value (it pulls bytes out of
1926 # the raw buffer), so this is also returned by value regardless of
1927 # whether the underlying type is a scalar or an aggregate. The static
1928 # fields live on the first burst's header.
1929 hdr.write(
1930 f" {cpp} {field_name}() const {{ return headers.front().{field_name}(); }}\n"
1931 )
1932 hdr.write(
1933 f" size_t {list_field_name}_count() const {{ return data_frames.size(); }}\n"
1934 )
1935 for field_name, field_type in info["data_fields"]:
1936 if field_name == list_field_name:
1937 elem_cpp = "value_type"
1938 else:
1939 elem_cpp = self._cpp_type(field_type)
1940 # The data-frame accessor is now a member function returning by value
1941 # (the underlying `_bytes` is private; getters reconstruct the field
1942 # value from its wire bytes), so the projection has to call it
1943 # rather than form a pointer-to-data-member.
1944 #
1945 # TODO: For byte-aligned aggregate data fields this means iterating
1946 # `field()` copies each element; the pre-B3 pointer-to-member form
1947 # could yield references for free. If profiling shows it matters,
1948 # we can add a separate `field_ref()` accessor on `data_frame` for
1949 # byte-aligned aggregate fields (they're stored contiguously inside
1950 # `_bytes` and the inner type's only member is itself a byte array,
1951 # so a `reinterpret_cast`-backed reference is well-defined) and
1952 # project on that instead.
1953 projection = (f"[](const data_frame &f) -> {elem_cpp} "
1954 f"{{ return f.{field_name}(); }}")
1955 hdr.write(
1956 f" auto {field_name}() const {{\n"
1957 f" return std::views::transform(data_frames, {projection});\n"
1958 f" }}\n")
1959 hdr.write(f" std::vector<{elem_cpp}> {field_name}_vector() const {{\n"
1960 f" std::vector<{elem_cpp}> out;\n"
1961 f" out.reserve(data_frames.size());\n"
1962 f" for (const auto &frame : data_frames)\n"
1963 f" out.push_back(frame.{field_name}());\n"
1964 f" return out;\n"
1965 f" }}\n")
1966
1967 def _emit_window_deserializer(self, hdr: IndentedWriter,
1968 info: Dict[str, Any]) -> None:
1969 """Emit a few bridge helpers + a `TypeDeserializer` alias.
1970
1971 The actual decoder lives in `esi::SerialListTypeDeserializer<T>`, which
1972 walks the header/data/footer burst protocol generically. Each window
1973 helper only has to expose:
1974
1975 - `_headerCount(const header_frame &)` -> `count_type`
1976 - `_fromFrames(const header_frame &, std::vector<data_frame> &&)`
1977 -> `std::unique_ptr<T>`
1978
1979 plus a `friend class esi::SerialListTypeDeserializer<T>;` so the template
1980 can reach the (private) `header_frame` definition.
1981 """
1982 window_name = info["window_name"]
1983 count_field_name = info["count_field_name"]
1984 ctor_args = ", ".join(f"h.{name}()" for name, _ in info["ctor_params"])
1985 if ctor_args:
1986 ctor_args = f"{ctor_args}, std::move(frames)"
1987 else:
1988 ctor_args = "std::move(frames)"
1989
1990 hdr.write("\n")
1991 hdr.write("private:\n")
1992 hdr.write(
1993 " // Bridge helpers used by esi::SerialListTypeDeserializer<T>; the\n")
1994 hdr.write(
1995 " // template walks the serial-list burst protocol generically and\n")
1996 hdr.write(
1997 " // reaches into `header_frame` via the friend declaration below.\n")
1998 hdr.write(" static count_type _headerCount(const header_frame &h) {\n")
1999 hdr.write(f" return h.{count_field_name}();\n")
2000 hdr.write(" }\n")
2001 hdr.write(f" static std::unique_ptr<{window_name}> _fromFrames(\n")
2002 hdr.write(
2003 " const header_frame &h, std::vector<data_frame> &&frames) {\n")
2004 hdr.write(f" return std::make_unique<{window_name}>({ctor_args});\n")
2005 hdr.write(" }\n")
2006 hdr.write(
2007 f" friend class esi::SerialListTypeDeserializer<{window_name}>;\n\n")
2008 hdr.write("public:\n")
2009 hdr.write(
2010 f" using TypeDeserializer = esi::SerialListTypeDeserializer<{window_name}>;\n"
2011 )
2012
2013 def _emit_alias(self, w: IndentedWriter, alias_type: types.TypeAlias) -> None:
2014 """Emit a using alias when the alias targets a different C++ type."""
2015 inner_wrapped = alias_type.inner_type
2016 alias_name = self.type_id_map[alias_type]
2017 inner_cpp = None
2018 if inner_wrapped is not None:
2019 inner_cpp = self._cpp_type(inner_wrapped)
2020 if inner_cpp is None:
2021 inner_cpp = self.type_id_map[alias_type]
2022 if inner_cpp != alias_name:
2023 w.line(f"using {alias_name} = {inner_cpp};")
2024 w.line()
2025
2026 def write_header(self, output_dir: Path, system_name: str) -> None:
2027 """Emit the fully ordered types.h header into the output directory."""
2028 if self.has_cycle:
2029 sys.stderr.write("Warning: cyclic type dependencies detected.\n")
2030 sys.stderr.write(" Logically this should not be possible.\n")
2031 sys.stderr.write(
2032 " Emitted code may fail to compile due to ordering issues.\n")
2033
2034 hdr_file = output_dir / "types.h"
2035 with open(hdr_file, "w", encoding="utf-8") as hdr:
2036 w = IndentedWriter(hdr)
2037 w.write(
2038 textwrap.dedent(f"""
2039 // Generated header for {system_name} types.
2040 //
2041 // Lifetime note: accessors that return `esi::BitVector` (for
2042 // `Bits<N>` fields), `esi::IntView` (for `Int<N>`, N > 64), or
2043 // `esi::UIntView` (for `UInt<N>`, N > 64) hand back non-owning
2044 // views into the parent struct's bytes. The view is only valid
2045 // while the parent is alive. Bind the parent to a named local
2046 // first, or construct an owning `esi::Int` / `esi::UInt` /
2047 // `esi::MutableBitVector` from the view if you need the value
2048 // to outlive its source. See `esi/Values.h` for details.
2049 #pragma once
2050
2051 #include <cstdint>
2052 #include <cstddef>
2053 #include <cstring>
2054 #include <algorithm>
2055 #include <any>
2056 #include <array>
2057 #include <limits>
2058 #include <ranges>
2059 #include <stdexcept>
2060 #include <string_view>
2061 #include <utility>
2062 #include <vector>
2063
2064 #include "esi/BitAccess.h"
2065 #include "esi/Common.h"
2066 #include "esi/TypedPorts.h"
2067 #include "esi/Values.h"
2068
2069 namespace {system_name} {{
2070
2071 """))
2072
2073 for skipped_type, reason in self.skipped_types:
2074 w.write(f"// Unsupported type '{skipped_type}': {reason}\n\n")
2075
2076 for emit_type in self.ordered_types:
2077 # Anything that wasn't expressible should have been caught by
2078 # CppTypePlanner and recorded in `skipped_types`; if a problem
2079 # leaks past that, treat it as a planner bug rather than emitting
2080 # a half-written header.
2081 if isinstance(emit_type, types.StructType):
2082 self._emit_struct(w, emit_type)
2083 elif isinstance(emit_type, types.UnionType):
2084 self._emit_union(w, emit_type)
2085 elif isinstance(emit_type, types.WindowType):
2086 self._emit_window(w, emit_type)
2087 elif isinstance(emit_type, types.TypeAlias):
2088 self._emit_alias(w, emit_type)
2089
2090 w.write(textwrap.dedent(f"""
2091 }} // namespace {system_name}
2092 """))
2093
2094
2095def run(generator: Type[Generator] = CppGenerator,
2096 cmdline_args: List[str] = sys.argv) -> int:
2097 """Create and run a generator reading options from the command line."""
2098
2099 argparser = argparse.ArgumentParser(
2100 description=f"Generate {generator.language} headers from an ESI manifest",
2101 formatter_class=argparse.RawDescriptionHelpFormatter,
2102 epilog=textwrap.dedent("""
2103 Can read the manifest from either a file OR a running accelerator.
2104
2105 Usage examples:
2106 # To read the manifest from a file:
2107 esi-cppgen --file /path/to/manifest.json
2108
2109 # To read the manifest from a running accelerator:
2110 esi-cppgen --platform cosim --connection localhost:1234
2111 """))
2112
2113 argparser.add_argument("--file",
2114 type=str,
2115 default=None,
2116 help="Path to the manifest file.")
2117 argparser.add_argument(
2118 "--platform",
2119 type=str,
2120 help="Name of platform for live accelerator connection.")
2121 argparser.add_argument(
2122 "--connection",
2123 type=str,
2124 help="Connection string for live accelerator connection.")
2125 argparser.add_argument(
2126 "--output-dir",
2127 type=str,
2128 default="esi",
2129 help="Output directory for generated files. Recommend adding either `esi`"
2130 " or the system name to the end of the path so as to avoid header name"
2131 "conflicts. Defaults to `esi`")
2132 argparser.add_argument(
2133 "--system-name",
2134 type=str,
2135 default="esi_system",
2136 help="Name of the ESI system. For C++, this will be the namespace.")
2137
2138 if (len(cmdline_args) <= 1):
2139 argparser.print_help()
2140 return 1
2141 args = argparser.parse_args(cmdline_args[1:])
2142
2143 if args.file is not None and args.platform is not None:
2144 print("Cannot specify both --file and --platform")
2145 return 1
2146
2147 conn: AcceleratorConnection
2148 if args.file is not None:
2149 # Use os.pathsep (';' on Windows, ':' on Unix) to avoid conflicts with
2150 # drive letters.
2151 conn = Context.default().connect("trace", f"-{os.pathsep}{args.file}")
2152 elif args.platform is not None:
2153 if args.connection is None:
2154 print("Must specify --connection with --platform")
2155 return 1
2156 conn = Context.default().connect(args.platform, args.connection)
2157 else:
2158 print("Must specify either --file or --platform")
2159 return 1
2160
2161 output_dir = Path(args.output_dir)
2162 if output_dir.exists() and not output_dir.is_dir():
2163 print(f"Output directory {output_dir} is not a directory")
2164 return 1
2165 if not output_dir.exists():
2166 output_dir.mkdir(parents=True)
2167
2168 gen = generator(conn)
2169 gen.generate(output_dir, args.system_name)
2170 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