CIRCT 23.0.0git
Loading...
Searching...
No Matches
codegen.py
Go to the documentation of this file.
1# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
2# See https://llvm.org/LICENSE.txt for license information.
3# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
4"""Code generation from ESI manifests to source code.
5
6Uses a two-pass approach for C++: first collect and name all reachable types,
7then emit structs/aliases in a dependency-ordered sequence so headers are
8standalone and deterministic.
9"""
10
11# C++ header support included with the runtime, though it is intended to be
12# extensible for other languages.
13
14from typing import Dict, List, Set, TextIO, Tuple, Type, Optional
15from .accelerator import AcceleratorConnection, Context
16from .esiCppAccel import ModuleInfo
17from . import types
18from .types import (BundlePort as _BundlePort, FunctionPort as _FunctionPort,
19 CallbackPort as _CallbackPort, ToHostPort as _ToHostPort,
20 FromHostPort as _FromHostPort, MMIORegion as
21 _MMIORegionPort, MetricPort as _MetricPort)
22
23import sys
24import os
25import textwrap
26import argparse
27from dataclasses import dataclass, field as _dc_field
28from pathlib import Path
29
30_thisdir = Path(__file__).absolute().resolve().parent
31
32
33@dataclass
35 """All strings needed to emit one port slot (member + ctor + find) in Connected."""
36 struct_decls: List[str] = _dc_field(default_factory=list)
37 member_decl: str = ""
38 ctor_params: List[str] = _dc_field(default_factory=list)
39 init_entry: str = ""
40 find_code: str = ""
41 make_unique_args: List[str] = _dc_field(default_factory=list)
42 post_connect: str = ""
43 using_aliases: List[Tuple[str, str]] = _dc_field(default_factory=list)
44
45
47 """Base class for all generators."""
48
49 language: Optional[str] = None
50
51 def __init__(self, conn: AcceleratorConnection):
52 self.manifest = conn.manifest()
53
54 def generate(self, output_dir: Path, system_name: str):
55 raise NotImplementedError("Generator.generate() must be overridden")
56
57
59 """Generate C++ headers from an ESI manifest."""
60
61 language = "C++"
62
63 def __init__(self, conn: AcceleratorConnection):
64 super().__init__(conn)
65 self._conn = conn
66 self.type_planner = CppTypePlanner(self.manifest.type_table)
68
69 def get_consts_str(self, module_info: ModuleInfo) -> str:
70 """Get the C++ code for a constant in a module."""
71 const_strs: List[str] = [
72 f"static constexpr {self.type_emitter.type_identifier(const.type)} "
73 f"{name} = 0x{const.value:x};"
74 for name, const in module_info.constants.items()
75 ]
76 return "\n".join(const_strs)
77
78 # ---------------------------------------------------------------------------
79 # Port-emission helpers
80 # ---------------------------------------------------------------------------
81
82 @staticmethod
83 def _sanitize_id(name: str) -> str:
84 """Return a C++-safe identifier from an AppID name."""
85 result = []
86 for ch in name:
87 result.append(ch if (ch.isalnum() or ch == "_") else "_")
88 if not result:
89 return "_port"
90 if result[0].isdigit():
91 result.insert(0, "_")
92 return "".join(result)
93
94 def _build_module_instance_map(self) -> Dict[str, object]:
95 """Walk the live hierarchy and return {module_name: first Instance}."""
96 accel = self._conn.build_accelerator()
97 result: Dict[str, object] = {}
98 queue = list(accel.children.values())
99 while queue:
100 inst = queue.pop(0)
101 info = inst.cpp_hwmodule.info
102 if info is not None:
103 name = info.name
104 if name is not None and name not in result:
105 result[name] = inst
106 queue.extend(inst.children.values())
107 return result
108
109 def _cpp_member_type(self, port, alias_prefix: Optional[str] = None) -> str:
110 """Return the C++ member type string for a port (no member name).
111
112 For typed ports (function/callback/to-host/from-host channels) `alias_prefix`
113 is required; the returned template parameters are written using the alias
114 names (`<prefix>Args`, `<prefix>Result`, `<prefix>Data`) that should be
115 emitted at module-class scope via `_port_using_aliases`. For non-typed
116 ports (MMIO regions, telemetry metrics, plain bundles) `alias_prefix` is
117 ignored and the runtime reference/pointer type is returned directly.
118 """
119 if isinstance(port, _FunctionPort):
120 assert alias_prefix is not None, (
121 "alias_prefix is required for FunctionPort to avoid emitting "
122 "long mangled type names inline (which would also collide across "
123 "modules' `using` declarations)")
124 return (f"esi::TypedFunction<{alias_prefix}Args, "
125 f"{alias_prefix}Result>")
126 if isinstance(port, _CallbackPort):
127 assert alias_prefix is not None, (
128 "alias_prefix is required for CallbackPort")
129 return (f"esi::TypedCallback<{alias_prefix}Args, "
130 f"{alias_prefix}Result>")
131 if isinstance(port, _ToHostPort):
132 assert alias_prefix is not None, (
133 "alias_prefix is required for ToHostPort")
134 return f"esi::TypedReadPort<{alias_prefix}Data>"
135 if isinstance(port, _FromHostPort):
136 assert alias_prefix is not None, (
137 "alias_prefix is required for FromHostPort")
138 return f"esi::TypedWritePort<{alias_prefix}Data>"
139 if isinstance(port, _MMIORegionPort):
140 return "esi::services::MMIO::MMIORegion &"
141 if isinstance(port, _MetricPort):
142 return "esi::services::TelemetryService::Metric &"
143 return "esi::BundlePort &"
144
145 def _port_using_aliases(self, alias_prefix: str,
146 port) -> List[Tuple[str, str]]:
147 """Return (alias_name, type_id) pairs to emit as `using` declarations at
148 module-class scope for the typed-port template parameters."""
149 if isinstance(port, (_FunctionPort, _CallbackPort)):
150 arg = self.type_emitter.type_identifier(port.arg_window_type or
151 port.arg_type)
152 res = self.type_emitter.type_identifier(port.result_window_type or
153 port.result_type)
154 return [(f"{alias_prefix}Args", arg), (f"{alias_prefix}Result", res)]
155 if isinstance(port, (_ToHostPort, _FromHostPort)):
156 data = self.type_emitter.type_identifier(port.data_window_type or
157 port.data_type)
158 return [(f"{alias_prefix}Data", data)]
159 return []
160
162 port,
163 alias_prefix: Optional[str] = None) -> str:
164 """Return the storage type used inside an `IndexedPorts<T>` for `port`.
165
166 Typed ports use the same `TypedFunction<...>` / `TypedReadPort<...>` etc.
167 that `_cpp_member_type` produces. MMIO regions, telemetry metrics, and
168 plain bundle ports are stored as raw pointers because `std::map<int, T&>`
169 is ill-formed.
170 """
171 if isinstance(port, _MMIORegionPort):
172 return "esi::services::MMIO::MMIORegion *"
173 if isinstance(port, _MetricPort):
174 return "esi::services::TelemetryService::Metric *"
175 if isinstance(port,
176 (_FunctionPort, _CallbackPort, _ToHostPort, _FromHostPort)):
177 return self._cpp_member_type(port, alias_prefix=alias_prefix)
178 return "esi::BundlePort *"
179
180 def _cpp_ctor_param_type(self, port) -> str:
181 """Return the C++ constructor parameter type for a port."""
182 if isinstance(port, _FunctionPort):
183 return "esi::services::FuncService::Function *"
184 if isinstance(port, _CallbackPort):
185 return "esi::services::CallService::Callback *"
186 if isinstance(port, _ToHostPort):
187 return "esi::ReadChannelPort &"
188 if isinstance(port, _FromHostPort):
189 return "esi::WriteChannelPort &"
190 if isinstance(port, _MMIORegionPort):
191 return "esi::services::MMIO::MMIORegion &"
192 if isinstance(port, _MetricPort):
193 return "esi::services::TelemetryService::Metric &"
194 return "esi::BundlePort &"
195
196 @staticmethod
197 def _cpp_ctor_param_suffix(port) -> str:
198 """Return the parameter name suffix ('_chan', '_svc', or '_port')."""
199 if isinstance(port, (_ToHostPort, _FromHostPort)):
200 return "_chan"
201 if isinstance(port, (_MMIORegionPort, _MetricPort)):
202 return "_svc"
203 return "_port"
204
205 @staticmethod
206 def _appid_expr(appid) -> str:
207 """Return `esi::AppID(...)` expression for an AppID."""
208 name = appid.name
209 idx = appid.idx
210 if idx is None:
211 return f'esi::AppID("{name}")'
212 return f'esi::AppID("{name}", {idx})'
213
214 def _port_find_code(self, member_name: str, port, appid) -> str:
215 """Return the code snippet that resolves a scalar port in connect()."""
216 ae = self._appid_expr(appid)
217 if isinstance(port, _FunctionPort):
218 v = f"{member_name}_port"
219 return (
220 f"auto *{v} =\n"
221 f" esi::findPortAsOrThrow<esi::services::FuncService::Function>(\n"
222 f" rawModule, {ae});")
223 if isinstance(port, _CallbackPort):
224 v = f"{member_name}_port"
225 return (
226 f"auto *{v} =\n"
227 f" esi::findPortAsOrThrow<esi::services::CallService::Callback>(\n"
228 f" rawModule, {ae});")
229 if isinstance(port, _ToHostPort):
230 v = f"{member_name}_chan"
231 return (
232 f"auto &{v} =\n"
233 f" esi::findPortAsOrThrow<esi::services::ChannelService::ToHost>(\n"
234 f' rawModule, {ae})->getRawRead("data");')
235 if isinstance(port, _FromHostPort):
236 v = f"{member_name}_chan"
237 return (
238 f"auto &{v} =\n"
239 f" esi::findPortAsOrThrow<esi::services::ChannelService::FromHost>(\n"
240 f' rawModule, {ae})->getRawWrite("data");')
241 if isinstance(port, _MMIORegionPort):
242 v = f"{member_name}_svc"
243 return (f"auto &{v} =\n"
244 f" *esi::findPortAsOrThrow<esi::services::MMIO::MMIORegion>(\n"
245 f" rawModule, {ae});")
246 if isinstance(port, _MetricPort):
247 v = f"{member_name}_svc"
248 return (
249 f"auto &{v} =\n"
250 f" *esi::findPortAsOrThrow<esi::services::TelemetryService::Metric>(\n"
251 f" rawModule, {ae});")
252 # plain BundlePort fallback
253 v = f"{member_name}_port"
254 return f"auto &{v} = esi::findPortOrThrow(rawModule, {ae});"
255
256 @staticmethod
257 def _port_make_unique_arg(member_name: str, port) -> str:
258 """Return the argument expression for make_unique<Connected>(...)."""
259 if isinstance(port, (_ToHostPort, _FromHostPort)):
260 return f"{member_name}_chan"
261 if isinstance(port, (_MMIORegionPort, _MetricPort)):
262 return f"{member_name}_svc"
263 return f"{member_name}_port"
264
265 @staticmethod
266 def _port_is_connectable(port) -> bool:
267 """True if the generated connect() should call .connect() on this port.
268
269 CallbackPort.connect() requires a user-supplied callback — skip.
270 MMIORegion, BundlePort — no .connect() method."""
271 return isinstance(port,
272 (_FunctionPort, _ToHostPort, _FromHostPort, _MetricPort))
273
274 def _scalar_port_group(self, member_name: str, port, appid) -> _PortGroup:
275 """Build a _PortGroup for a single scalar (non-indexed) port."""
276 aliases = self._port_using_aliases(member_name, port)
277 alias_prefix = member_name if aliases else None
278 member_type = self._cpp_member_type(port, alias_prefix=alias_prefix)
279 is_ref = member_type.endswith(" &")
280 param_type = self._cpp_ctor_param_type(port)
281 param_suffix = self._cpp_ctor_param_suffix(port)
282 param_name = f"{member_name}{param_suffix}"
283
284 if is_ref:
285 member_decl = f"{member_type}{member_name};"
286 else:
287 member_decl = f"{member_type} {member_name};"
288
289 post = ""
290 if self._port_is_connectable(port):
291 post = f"connected->{member_name}.connect();"
292
293 return _PortGroup(
294 member_decl=member_decl,
295 ctor_params=[f"{param_type} {param_name}"],
296 init_entry=f"{member_name}({param_name})",
297 find_code=self._port_find_code(member_name, port, appid),
298 make_unique_args=[self._port_make_unique_arg(member_name, port)],
299 post_connect=post,
300 using_aliases=aliases,
301 )
302
303 def _indexed_ports_group(self, member_name: str, appid_name: str,
304 port_list) -> _PortGroup:
305 """Build a _PortGroup for a same-name, same-type indexed port array."""
306 # Derive the element type from the first port.
307 first_port = port_list[0][1]
308 aliases = self._port_using_aliases(member_name, first_port)
309 alias_prefix = member_name if aliases else None
310 elem_type = self._cpp_indexed_elem_type(first_port,
311 alias_prefix=alias_prefix)
312 indexed_type = f"esi::IndexedPorts<{elem_type}>"
313 map_var = f"{member_name}_backing"
314 map_type = f"std::map<int, {elem_type}>"
315 indexed_var = f"{member_name}_map"
316
317 # Build the find code: per-index resolve and try_emplace, then freeze
318 # into the IndexedPorts wrapper. The body of the loop differs by port
319 # kind: channel ports need an extra `getRawRead("data")` /
320 # `getRawWrite("data")` step, MMIO regions and metrics store raw
321 # pointers.
322 find_parts = [
323 f"{map_type} {map_var};",
324 f"for (uint32_t idx : esi::findPortIndices(rawModule, "
325 f"\"{appid_name}\")) {{",
326 ]
327 appid_expr = f'esi::AppID("{appid_name}", idx)'
328 if isinstance(first_port, _FunctionPort):
329 find_parts.append(
330 f" {map_var}.try_emplace(\n"
331 f" static_cast<int>(idx),\n"
332 f" esi::findPortAsOrThrow<esi::services::FuncService::Function>"
333 f"(\n"
334 f" rawModule, {appid_expr}));")
335 elif isinstance(first_port, _CallbackPort):
336 find_parts.append(
337 f" {map_var}.try_emplace(\n"
338 f" static_cast<int>(idx),\n"
339 f" esi::findPortAsOrThrow<esi::services::CallService::Callback>"
340 f"(\n"
341 f" rawModule, {appid_expr}));")
342 elif isinstance(first_port, _ToHostPort):
343 # TypedReadPort takes a ReadChannelPort&, not the service port. Resolve
344 # the service port first, then bind its underlying raw read channel.
345 find_parts.append(
346 f" auto *svc =\n"
347 f" esi::findPortAsOrThrow<esi::services::ChannelService::ToHost>"
348 f"(\n"
349 f" rawModule, {appid_expr});\n"
350 f" {map_var}.try_emplace(\n"
351 f" static_cast<int>(idx),\n"
352 f" svc->getRawRead(\"data\"));")
353 elif isinstance(first_port, _FromHostPort):
354 find_parts.append(
355 f" auto *svc =\n"
356 f" esi::findPortAsOrThrow<esi::services::ChannelService::"
357 f"FromHost>(\n"
358 f" rawModule, {appid_expr});\n"
359 f" {map_var}.try_emplace(\n"
360 f" static_cast<int>(idx),\n"
361 f" svc->getRawWrite(\"data\"));")
362 elif isinstance(first_port, _MMIORegionPort):
363 find_parts.append(
364 f" {map_var}.try_emplace(\n"
365 f" static_cast<int>(idx),\n"
366 f" esi::findPortAsOrThrow<esi::services::MMIO::MMIORegion>(\n"
367 f" rawModule, {appid_expr}));")
368 elif isinstance(first_port, _MetricPort):
369 find_parts.append(
370 f" {map_var}.try_emplace(\n"
371 f" static_cast<int>(idx),\n"
372 f" esi::findPortAsOrThrow<esi::services::TelemetryService::"
373 f"Metric>(\n"
374 f" rawModule, {appid_expr}));")
375 else:
376 # Plain BundlePort: any service port that doesn't match a standard
377 # specialization (e.g. a custom `@esi.ServiceDecl`-defined service).
378 find_parts.append(
379 f" {map_var}.try_emplace(\n"
380 f" static_cast<int>(idx),\n"
381 f" &esi::findPortOrThrow(rawModule, {appid_expr}));")
382 find_parts.append("}")
383 find_parts.append(f"{indexed_type} {indexed_var}(std::move({map_var}));")
384
385 post = ""
386 if self._port_is_connectable(first_port):
387 # IndexedPorts now exposes mutable iteration so `port.connect()` is fine.
388 post = (f"for (auto &[idx, port] : connected->{member_name})\n"
389 f" port.connect();")
390
391 return _PortGroup(
392 member_decl=f"{indexed_type} {member_name};",
393 ctor_params=[f"{indexed_type} {indexed_var}"],
394 init_entry=f"{member_name}(std::move({indexed_var}))",
395 find_code="\n".join(find_parts),
396 make_unique_args=[f"std::move({indexed_var})"],
397 post_connect=post,
398 using_aliases=aliases,
399 )
400
401 def _mixed_struct_group(self, member_name: str, appid_name: str,
402 port_list) -> _PortGroup:
403 """Build a _PortGroup for a same-name, mixed-type indexed port group."""
404 struct_name = f"{member_name}_ports"
405 sub_member_decls: List[str] = []
406 ctor_params: List[str] = []
407 init_args: List[str] = []
408 find_parts: List[str] = []
409 make_args: List[str] = []
410 post_parts: List[str] = []
411 using_aliases: List[Tuple[str, str]] = []
412
413 for appid, port in port_list:
414 idx = appid.idx if appid.idx is not None else 0
415 sub_name = f"_{idx}"
416 sub_alias_prefix = f"{member_name}_{idx}"
417 sub_aliases = self._port_using_aliases(sub_alias_prefix, port)
418 using_aliases.extend(sub_aliases)
419 alias_prefix = sub_alias_prefix if sub_aliases else None
420 member_type = self._cpp_member_type(port, alias_prefix=alias_prefix)
421 is_ref = member_type.endswith(" &")
422 if is_ref:
423 sub_member_decls.append(f"{member_type}{sub_name};")
424 else:
425 sub_member_decls.append(f"{member_type} {sub_name};")
426
427 param_type = self._cpp_ctor_param_type(port)
428 param_suffix = self._cpp_ctor_param_suffix(port)
429 param_name = f"{member_name}_{idx}{param_suffix}"
430 ctor_params.append(f"{param_type} {param_name}")
431 init_args.append(param_name)
432 find_parts.append(self._port_find_code(param_name, port, appid))
433 make_args.append(self._port_make_unique_arg(param_name, port))
434 if self._port_is_connectable(port):
435 post_parts.append(f"connected->{member_name}.{sub_name}.connect();")
436
437 struct_decl = (f"struct {struct_name} {{\n" +
438 "".join(f" {d}\n" for d in sub_member_decls) + " };")
439 init_entry = f"{member_name}{{{', '.join(init_args)}}}"
440
441 return _PortGroup(
442 struct_decls=[struct_decl],
443 member_decl=f"{struct_name} {member_name};",
444 ctor_params=ctor_params,
445 init_entry=init_entry,
446 find_code="\n".join(find_parts),
447 make_unique_args=make_args,
448 post_connect="\n".join(post_parts),
449 using_aliases=using_aliases,
450 )
451
452 def _collect_port_groups(self, ports: dict) -> List[_PortGroup]:
453 """Group `ports` (AppID → BundlePort) into _PortGroup list."""
454 # Group by AppID name, preserving sorted order.
455 groups_by_name: Dict[str, list] = {}
456 for appid, port in ports.items():
457 n = appid.name
458 if n not in groups_by_name:
459 groups_by_name[n] = []
460 groups_by_name[n].append((appid, port))
461
462 result: List[_PortGroup] = []
463 for appid_name, port_list in groups_by_name.items():
464 member_name = self._sanitize_id(appid_name)
465 # Sort by idx (None → -1 so scalar ports sort first).
466 port_list.sort(key=lambda x: x[0].idx if x[0].idx is not None else -1)
467
468 if len(port_list) == 1:
469 appid, port = port_list[0]
470 if appid.idx is None:
471 result.append(self._scalar_port_group(member_name, port, appid))
472 else:
473 result.append(
474 self._indexed_ports_group(member_name, appid_name, port_list))
475 continue
476
477 # Multiple ports with the same name.
478 all_indexed = all(a.idx is not None for a, _ in port_list)
479 if not all_indexed:
480 # Degenerate: mix of indexed and non-indexed with the same name.
481 # Emit as a mixed struct for safety.
482 result.append(
483 self._mixed_struct_group(member_name, appid_name, port_list))
484 continue
485
486 all_same_type = len({type(p) for _, p in port_list}) == 1
487 if all_same_type:
488 result.append(
489 self._indexed_ports_group(member_name, appid_name, port_list))
490 else:
491 result.append(
492 self._mixed_struct_group(member_name, appid_name, port_list))
493
494 return result
495
496 def _emit_module_class(self, name: str, system_name: str,
497 module_info: ModuleInfo, port_groups: List[_PortGroup],
498 out: TextIO) -> None:
499 """Emit the full module class to `out`."""
500 out.write(f"/// Generated header for {system_name} module {name}.\n"
501 "#pragma once\n"
502 '#include "types.h"\n'
503 '#include "esi/TypedPorts.h"\n'
504 "\n"
505 "#include <any>\n"
506 "#include <map>\n"
507 "#include <optional>\n"
508 "#include <string>\n"
509 "\n"
510 f"namespace {system_name} {{\n"
511 "\n")
512
513 # Module metadata as a Doxygen comment block above the class.
514 metadata_lines: List[str] = []
515 summary = getattr(module_info, "summary", None)
516 if summary:
517 for line in summary.splitlines():
518 metadata_lines.append(line)
519 for label, attr in (("Version", "version"), ("Repository", "repo"),
520 ("Commit", "commit_hash")):
521 val = getattr(module_info, attr, None)
522 if val:
523 metadata_lines.append(f"{label}: {val}")
524 if metadata_lines:
525 out.write("///\n")
526 for line in metadata_lines:
527 out.write(f"/// {line}\n" if line else "///\n")
528 out.write("///\n")
529
530 out.write(f"class {name} {{\n"
531 "public:\n")
532
533 consts = self.get_consts_str(module_info)
534 if consts:
535 out.write(" // Module constants.\n")
536 out.write(f" {consts}\n\n")
537
538 # Type aliases for typed-port template parameters, hoisted to module scope
539 # so the long mangled names don't appear inline as template arguments.
540 aliases = [a for grp in port_groups for a in grp.using_aliases]
541 if aliases:
542 for alias_name, alias_type in aliases:
543 out.write(f" using {alias_name} = {alias_type};\n")
544 out.write("\n")
545
546 if port_groups:
547 out.write(
548 " /// Holds the resolved, typed ports for this module instance.\n"
549 " /// Returned by `connect()`.\n"
550 " class Connected {\n public:\n")
551
552 # Struct declarations for mixed groups.
553 for grp in port_groups:
554 for decl in grp.struct_decls:
555 out.write(f" {decl}\n")
556 if any(grp.struct_decls for grp in port_groups):
557 out.write("\n")
558
559 # Member declarations.
560 for grp in port_groups:
561 out.write(f" {grp.member_decl}\n")
562 out.write("\n")
563
564 # Constructor.
565 all_params = [p for grp in port_groups for p in grp.ctor_params]
566 out.write(" Connected(\n")
567 for i, param in enumerate(all_params):
568 comma = "," if i < len(all_params) - 1 else ""
569 out.write(f" {param}{comma}\n")
570 out.write(" )\n : ")
571 inits = [grp.init_entry for grp in port_groups]
572 out.write(",\n ".join(inits))
573 out.write(" {}\n };\n\n")
574
575 # Outer constructor.
576 out.write(
577 f" {name}(esi::HWModule *rawModule) : rawModule(rawModule) {{}}\n\n")
578
579 # Module-metadata accessors. These read from the live HWModule's ModuleInfo
580 # so callers can verify that the connected accelerator is compatible with
581 # the build the software was generated against.
582 out.write(
583 " /// The connected module's name as reported by the manifest, or\n"
584 " /// std::nullopt if the module has no metadata.\n"
585 " std::optional<std::string> name() const {\n"
586 " auto info = rawModule->getInfo();\n"
587 " return info ? info->name : std::nullopt;\n"
588 " }\n"
589 " /// The connected module's summary string, if any.\n"
590 " std::optional<std::string> summary() const {\n"
591 " auto info = rawModule->getInfo();\n"
592 " return info ? info->summary : std::nullopt;\n"
593 " }\n"
594 " /// The connected module's version string, if any.\n"
595 " std::optional<std::string> version() const {\n"
596 " auto info = rawModule->getInfo();\n"
597 " return info ? info->version : std::nullopt;\n"
598 " }\n"
599 " /// The connected module's source repository, if any.\n"
600 " std::optional<std::string> repo() const {\n"
601 " auto info = rawModule->getInfo();\n"
602 " return info ? info->repo : std::nullopt;\n"
603 " }\n"
604 " /// The connected module's source commit hash, if any.\n"
605 " std::optional<std::string> commitHash() const {\n"
606 " auto info = rawModule->getInfo();\n"
607 " return info ? info->commitHash : std::nullopt;\n"
608 " }\n"
609 " /// Designer-specified constants for the connected module.\n"
610 " /// Returns an empty map if the module has no metadata.\n"
611 " std::map<std::string, esi::Constant> constants() const {\n"
612 " auto info = rawModule->getInfo();\n"
613 " return info ? info->constants\n"
614 " : std::map<std::string, esi::Constant>{};\n"
615 " }\n"
616 " /// Free-form designer-supplied metadata for the connected module.\n"
617 " /// Returns an empty map if the module has no metadata.\n"
618 " std::map<std::string, std::any> extra() const {\n"
619 " auto info = rawModule->getInfo();\n"
620 " return info ? info->extra : std::map<std::string, std::any>{};\n"
621 " }\n\n")
622
623 if port_groups:
624 out.write(" std::unique_ptr<Connected> connect() {\n")
625
626 # Find / resolve phase.
627 for grp in port_groups:
628 if grp.find_code:
629 for line in grp.find_code.splitlines():
630 out.write(f" {line}\n")
631 out.write("\n")
632
633 # Construct Connected.
634 all_args = [a for grp in port_groups for a in grp.make_unique_args]
635 out.write(" auto connected = std::make_unique<Connected>(\n")
636 for i, arg in enumerate(all_args):
637 comma = "," if i < len(all_args) - 1 else ""
638 out.write(f" {arg}{comma}\n")
639 out.write(" );\n\n")
640
641 # Post-construction connects.
642 for grp in port_groups:
643 if grp.post_connect:
644 for line in grp.post_connect.splitlines():
645 out.write(f" {line}\n")
646
647 out.write(" return connected;\n }\n\n")
648
649 out.write("private:\n esi::HWModule *rawModule;\n};\n\n")
650 out.write(f"}} // namespace {system_name}\n")
651
652 def write_modules(self, output_dir: Path, system_name: str):
653 """Write the C++ header. One for each module in the manifest."""
654 module_instances = self._build_module_instance_map()
655
656 for module_info in self.manifest.module_infos:
657 if module_info.name is None:
658 continue
659 name = module_info.name
660 instance = module_instances.get(name)
661 try:
662 if instance is not None:
663 port_groups = self._collect_port_groups(instance.ports)
664 else:
665 port_groups = []
666 except (NotImplementedError, ValueError) as e:
667 sys.stderr.write(f"Warning: skipping module '{name}': {e}\n")
668 hdr_file = output_dir / f"{name}.h"
669 with open(hdr_file, "w") as hdr:
670 hdr.write(f"// Skipped: {e}\n")
671 continue
672
673 hdr_file = output_dir / f"{name}.h"
674 with open(hdr_file, "w") as hdr:
675 self._emit_module_class(name, system_name, module_info, port_groups,
676 hdr)
677
678 def generate(self, output_dir: Path, system_name: str):
679 self.type_emitter.write_header(output_dir, system_name)
680 self.write_modules(output_dir, system_name)
681
682
684 """Plan C++ type naming and ordering from an ESI manifest."""
685
686 def __init__(self, type_table) -> None:
687 """Initialize the generator with the manifest and target namespace."""
688 # Map manifest type ids to their preferred C++ names.
689 self.type_id_map: Dict[types.ESIType, str] = {}
690 # Track all names already taken to avoid collisions. True => alias-based.
691 self.used_names: Dict[str, bool] = {}
692 # Track alias base names to warn on collisions.
693 self.alias_base_names: Set[str] = set()
694 self.ordered_types: List[types.ESIType] = []
695 self.has_cycle = False
696 self._prepare_types(type_table)
697
698 def _prepare_types(self, type_table) -> None:
699 """Name the types and prepare for emission by registering all reachable
700 types and assigning."""
701 visited: Set[str] = set()
702 for t in type_table:
703 self._collect_aliases(t, visited)
704
705 visited = set()
706 for t in type_table:
707 self._collect_structs(t, visited)
708
709 visited = set()
710 for t in type_table:
711 self._collect_windows(t, visited)
712
714
715 def _sanitize_name(self, name: str) -> str:
716 """Create a C++-safe identifier from the manifest-provided name."""
717 name = name.replace("::", "_")
718 if name.startswith("@"):
719 name = name[1:]
720 sanitized = []
721 for ch in name:
722 if ch.isalnum() or ch == "_":
723 sanitized.append(ch)
724 else:
725 sanitized.append("_")
726 if not sanitized:
727 return "Type"
728 if sanitized[0].isdigit():
729 sanitized.insert(0, "_")
730 return "".join(sanitized)
731
732 def _reserve_name(self, base: str, is_alias: bool) -> str:
733 """Reserve a globally unique identifier using the sanitized base name."""
734 base = self._sanitize_name(base)
735 if is_alias and base in self.alias_base_names:
736 sys.stderr.write(
737 f"Warning: duplicate alias name '{base}' detected; disambiguating.\n")
738 if is_alias:
739 self.alias_base_names.add(base)
740 name = base
741 idx = 1
742 while name in self.used_names:
743 name = f"{base}_{idx}"
744 idx += 1
745 self.used_names[name] = is_alias
746 return name
747
748 def _auto_struct_name(self, struct_type: types.StructType) -> str:
749 """Derive a deterministic name for anonymous structs from their fields."""
750 parts = ["_struct"]
751 for field_name, field_type in struct_type.fields:
752 parts.append(field_name)
753 parts.append(self._sanitize_name(field_type.id))
754 return self._reserve_name("_".join(parts), is_alias=False)
755
756 def _auto_union_name(self, union_type: types.UnionType) -> str:
757 """Derive a deterministic name for anonymous unions from their fields."""
758 parts = ["_union"]
759 for field_name, field_type in union_type.fields:
760 parts.append(field_name)
761 parts.append(self._sanitize_name(field_type.id))
762 return self._reserve_name("_".join(parts), is_alias=False)
763
764 def _auto_window_name(self, window_type: types.WindowType) -> str:
765 """Derive a deterministic name for generated window helpers.
766
767 Two distinct windows can wrap the same `into` struct (e.g. serial and
768 parallel encodings of the same payload), so the helper name must be
769 derived from BOTH the inner type's name and the window's own name/id.
770 """
771 into_type = self._unwrap_aliases(window_type.into_type)
772 into_name = self.type_id_map.get(into_type)
773 window_part = (window_type.name
774 if window_type.name else self._sanitize_name(window_type.id))
775 if into_name:
776 base = f"{into_name}_{window_part}"
777 elif window_type.name:
778 base = window_part
779 else:
780 base = f"_window_{window_part}"
781 return self._reserve_name(base, is_alias=False)
782
783 def _unwrap_aliases(self, wrapped: types.ESIType) -> types.ESIType:
784 while isinstance(wrapped, types.TypeAlias):
785 wrapped = wrapped.inner_type
786 return wrapped
787
788 def _is_supported_window(self, current_type: types.ESIType) -> bool:
789 if not isinstance(current_type, types.WindowType):
790 return False
791 into_type = self._unwrap_aliases(current_type.into_type)
792 if not isinstance(into_type, types.StructType):
793 return False
794
795 # The generated window helper only supports struct-shaped payloads with a
796 # single logical list field to stream across multiple frames.
797 list_fields = []
798 for field_name, field_type in into_type.fields:
799 if isinstance(self._unwrap_aliases(field_type), types.ListType):
800 list_fields.append(field_name)
801 if len(list_fields) != 1:
802 return False
803
804 list_field_name = list_fields[0]
805 header_field = None
806 data_field = None
807 # That list must appear exactly once as a bulk-count field and exactly once
808 # as a single-item data field so the helper can synthesize header/data/footer.
809 for frame in current_type.frames:
810 for field in frame.fields:
811 if field.name != list_field_name:
812 continue
813 if field.bulk_count_width > 0:
814 if header_field is not None:
815 return False
816 header_field = field
817 elif field.num_items > 0:
818 if data_field is not None:
819 return False
820 data_field = field
821 return (header_field is not None and data_field is not None and
822 data_field.num_items == 1)
823
824 def _iter_type_children(self, t: types.ESIType) -> List[types.ESIType]:
825 """Return child types in a stable order for traversal."""
826 if isinstance(t, types.TypeAlias):
827 return [t.inner_type] if t.inner_type is not None else []
828 if isinstance(t, types.BundleType):
829 return [channel.type for channel in t.channels]
830 if isinstance(t, types.ChannelType):
831 return [t.inner]
832 if isinstance(t, types.StructType):
833 return [field_type for _, field_type in t.fields]
834 if isinstance(t, types.UnionType):
835 return [field_type for _, field_type in t.fields]
836 if isinstance(t, types.ListType):
837 return [t.element_type]
838 if isinstance(t, types.WindowType):
839 return [t.into_type]
840 if isinstance(t, types.ArrayType):
841 return [t.element_type]
842 return []
843
844 def _visit_types(self, t: types.ESIType, visited: Set[str], visit_fn) -> None:
845 """Traverse types with alphabetical child ordering in post-order."""
846 if not isinstance(t, types.ESIType):
847 raise TypeError(f"Expected ESIType, got {type(t)}")
848 tid = t.id
849 if tid in visited:
850 return
851 visited.add(tid)
852 children = sorted(self._iter_type_children(t), key=lambda child: child.id)
853 for child in children:
854 self._visit_types(child, visited, visit_fn)
855 visit_fn(t)
856
857 def _collect_aliases(self, t: types.ESIType, visited: Set[str]) -> None:
858 """Scan for aliases and reserve their names (recursive)."""
859
860 # Visit callback: reserve alias names and map aliases to identifiers.
861 def visit(alias_type: types.ESIType) -> None:
862 if not isinstance(alias_type, types.TypeAlias):
863 return
864 if alias_type not in self.type_id_map:
865 alias_name = self._reserve_name(alias_type.name, is_alias=True)
866 self.type_id_map[alias_type] = alias_name
867
868 self._visit_types(t, visited, visit)
869
870 def _collect_structs(self, t: types.ESIType, visited: Set[str]) -> None:
871 """Scan for structs/unions needing auto-names and reserve them."""
872
873 # Visit callback: assign auto-names to unnamed structs and unions.
874 def visit(current_type: types.ESIType) -> None:
875 if current_type in self.type_id_map:
876 return
877 if isinstance(current_type, types.StructType):
878 self.type_id_map[current_type] = self._auto_struct_name(current_type)
879 elif isinstance(current_type, types.UnionType):
880 self.type_id_map[current_type] = self._auto_union_name(current_type)
881
882 self._visit_types(t, visited, visit)
883
884 def _collect_windows(self, t: types.ESIType, visited: Set[str]) -> None:
885 """Scan for supported window types and reserve helper names."""
886
887 def visit(current_type: types.ESIType) -> None:
888 if not self._is_supported_window(current_type):
889 return
890 assert isinstance(current_type, types.WindowType)
891 if current_type in self.type_id_map:
892 return
893 self.type_id_map[current_type] = self._auto_window_name(current_type)
894
895 self._visit_types(t, visited, visit)
896
898 wrapped: types.ESIType) -> Set[types.ESIType]:
899 """Collect types that require top-level declarations for a given type."""
900 deps: Set[types.ESIType] = set()
901
902 # Visit callback: collect structs, unions, and non-struct aliases used by a
903 # type.
904 def visit(current: types.ESIType) -> None:
905 if isinstance(current, types.TypeAlias):
906 inner = current.inner_type
907 if inner is not None and (isinstance(
909 self._is_supported_window(inner)):
910 deps.add(inner)
911 else:
912 deps.add(current)
913 elif isinstance(current, (types.StructType, types.UnionType)):
914 deps.add(current)
915 elif self._is_supported_window(current):
916 deps.add(current)
917
918 self._visit_types(wrapped, set(), visit)
919 return deps
920
922 self, window_type: types.WindowType) -> Set[types.ESIType]:
923 """Collect only the declarations referenced by a generated window helper."""
924 deps: Set[types.ESIType] = set()
925 into_type = self._unwrap_aliases(window_type.into_type)
926 if not isinstance(into_type, types.StructType):
927 return deps
928
929 for _, field_type in into_type.fields:
930 unwrapped = self._unwrap_aliases(field_type)
931 if isinstance(unwrapped, types.ListType):
932 deps.update(self._collect_decls_from_type(unwrapped.element_type))
933 else:
934 deps.update(self._collect_decls_from_type(field_type))
935 return deps
936
937 def _ordered_emit_types(self) -> Tuple[List[types.ESIType], bool]:
938 """Collect and order types for deterministic emission."""
939 window_into_types: Set[types.ESIType] = set()
940 for esi_type in self.type_id_map.keys():
941 if not self._is_supported_window(esi_type):
942 continue
943 assert isinstance(esi_type, types.WindowType)
944 window_into_types.add(self._unwrap_aliases(esi_type.into_type))
945 emit_types: List[types.ESIType] = []
946 for esi_type in self.type_id_map.keys():
947 if isinstance(esi_type,
948 types.StructType) and esi_type in window_into_types:
949 continue
950 if isinstance(esi_type, types.TypeAlias):
951 inner = esi_type.inner_type
952 if inner is not None and self._unwrap_aliases(
953 inner) in window_into_types:
954 continue
955 if (isinstance(esi_type,
957 self._is_supported_window(esi_type)):
958 emit_types.append(esi_type)
959
960 # Prefer alias-reserved names first, then lexicographic for determinism.
961 name_to_type = {self.type_id_map[t]: t for t in emit_types}
962 sorted_names = sorted(name_to_type.keys(),
963 key=lambda name:
964 (0 if self.used_names.get(name, False) else 1, name))
965
966 ordered: List[types.ESIType] = []
967 visited: Set[types.ESIType] = set()
968 visiting: Set[types.ESIType] = set()
969 has_cycle = False
970
971 # Visit callback: DFS to emit dependencies before their users.
972 def visit(current: types.ESIType) -> None:
973 nonlocal has_cycle
974 if current in visited:
975 return
976 if current in visiting:
977 has_cycle = True
978 return
979 visiting.add(current)
980
981 deps: Set[types.ESIType] = set()
982 if isinstance(current, types.TypeAlias):
983 inner = current.inner_type
984 if inner is not None:
985 deps.update(self._collect_decls_from_type(inner))
986 elif isinstance(current, (types.StructType, types.UnionType)):
987 for _, field_type in current.fields:
988 deps.update(self._collect_decls_from_type(field_type))
989 elif self._is_supported_window(current):
990 assert isinstance(current, types.WindowType)
991 deps.update(self._collect_decls_from_window(current))
992 for dep in sorted(deps, key=lambda dep: self.type_id_map[dep]):
993 visit(dep)
994
995 visiting.remove(current)
996 visited.add(current)
997 ordered.append(current)
998
999 for name in sorted_names:
1000 visit(name_to_type[name])
1001
1002 return ordered, has_cycle
1003
1004
1006 """Emit C++ headers from precomputed type ordering."""
1007
1008 def __init__(self, planner: CppTypePlanner) -> None:
1009 self.type_id_map = planner.type_id_map
1010 self.ordered_types = planner.ordered_types
1011 self.has_cycle = planner.has_cycle
1012
1013 def type_identifier(self, type: types.ESIType) -> str:
1014 """Get the C++ type string for an ESI type."""
1015 return self._cpp_type(type)
1016
1017 def _cpp_string_literal(self, value: str) -> str:
1018 """Escape a Python string for use as a C++ string literal."""
1019 escaped = value.replace("\\", "\\\\").replace('"', '\\"')
1020 return f'"{escaped}"'
1021
1022 def _get_bitvector_str(self, type: types.ESIType) -> str:
1023 """Get the textual code for the storage class of an integer type."""
1024 assert isinstance(type, (types.BitsType, types.IntType))
1025
1026 return self._storage_type(
1027 type.bit_width, not isinstance(type, (types.BitsType, types.UIntType)))
1028
1029 def _storage_type(self, bit_width: int, signed: bool) -> str:
1030 """Get the textual code for a byte-addressable integer storage type."""
1031
1032 if bit_width == 1:
1033 return "bool"
1034 elif bit_width <= 8:
1035 storage_width = 8
1036 elif bit_width <= 16:
1037 storage_width = 16
1038 elif bit_width <= 32:
1039 storage_width = 32
1040 elif bit_width <= 64:
1041 storage_width = 64
1042 else:
1043 raise ValueError(f"Unsupported integer width: {bit_width}")
1044
1045 if not signed:
1046 return f"uint{storage_width}_t"
1047 return f"int{storage_width}_t"
1048
1049 def _type_byte_width(self, wrapped: types.ESIType) -> int:
1050 """Return the size of a fixed-width type in bytes."""
1051 if wrapped.bit_width < 0:
1052 raise ValueError(f"Unsupported unbounded type width for '{wrapped}'")
1053 return (wrapped.bit_width + 7) // 8
1054
1056 self, array_type: types.ArrayType) -> Tuple[str, List[int]]:
1057 """Return the base C++ type and outer-to-inner dimensions of a nested array."""
1058 dims: List[int] = []
1059 inner: types.ESIType = array_type
1060 while isinstance(inner, types.ArrayType):
1061 dims.append(inner.size)
1062 inner = inner.element_type
1063 base_cpp = self._cpp_type(inner)
1064 return base_cpp, dims
1065
1066 def _std_array_type(self, array_type: types.ArrayType) -> str:
1067 """Return the equivalent nested `std::array<...>` type for an array.
1068
1069 `std::array<T, N>` is layout-compatible in practice with `T[N]` on every
1070 major implementation (and identical under `#pragma pack(1)`), so the
1071 generator uses it everywhere a fixed-size array would appear. This keeps
1072 field/value/ctor types storable in `std::vector` and assignable with `=`.
1073 """
1074 base_cpp, dims = self._array_base_and_dims(array_type)
1075 result = base_cpp
1076 for size in reversed(dims):
1077 result = f"std::array<{result}, {size}>"
1078 return result
1079
1080 def _cpp_type(self, wrapped: types.ESIType) -> str:
1081 """Resolve an ESI type to its C++ identifier."""
1082 if isinstance(wrapped, types.WindowType) and wrapped in self.type_id_map:
1083 return self.type_id_map[wrapped]
1084 if isinstance(wrapped,
1086 return self.type_id_map[wrapped]
1087 if isinstance(wrapped, types.BundleType):
1088 return "void"
1089 if isinstance(wrapped, types.ChannelType):
1090 return self._cpp_type(wrapped.inner)
1091 if isinstance(wrapped, types.ListType):
1092 raise ValueError("List types require a generated window wrapper")
1093 if isinstance(wrapped, types.VoidType):
1094 return "void"
1095 if isinstance(wrapped, types.AnyType):
1096 return "std::any"
1097 if isinstance(wrapped, (types.BitsType, types.IntType)):
1098 return self._get_bitvector_str(wrapped)
1099 if isinstance(wrapped, types.ArrayType):
1100 return self._std_array_type(wrapped)
1101 if type(wrapped) is types.ESIType:
1102 return "std::any"
1103 raise NotImplementedError(
1104 f"Type '{wrapped}' not supported for C++ generation")
1105
1106 def _unwrap_aliases(self, wrapped: types.ESIType) -> types.ESIType:
1107 """Strip alias wrappers to reach the underlying type."""
1108 while isinstance(wrapped, types.TypeAlias):
1109 wrapped = wrapped.inner_type
1110 return wrapped
1111
1112 def _format_window_field_decl(self, field_name: str,
1113 field_type: types.ESIType) -> str:
1114 """Emit a packed field declaration for generated window helpers.
1115
1116 Arrays use `std::array` (handled by `_cpp_type`), so no bracket syntax is
1117 needed and a single uniform declaration form covers every type.
1118 """
1119 field_cpp = self._cpp_type(field_type)
1120 wrapped = self._unwrap_aliases(field_type)
1121 if isinstance(wrapped, (types.BitsType, types.IntType)) and \
1122 wrapped.bit_width % 8 != 0:
1123 return f"{field_cpp} {field_name} : {wrapped.bit_width};"
1124 return f"{field_cpp} {field_name};"
1125
1126 def _format_window_ctor_param(self, field_name: str,
1127 field_type: types.ESIType) -> str:
1128 """Emit a constructor parameter for generated window helpers.
1129
1130 Small scalar header fields are cheaper to pass by value than by reference.
1131 Larger aggregates stay as const references.
1132 """
1133 field_cpp = self._cpp_type(field_type)
1134 wrapped = self._unwrap_aliases(field_type)
1135 if isinstance(wrapped, (types.BitsType, types.IntType)):
1136 return f"{field_cpp} {field_name}"
1137 return f"const {field_cpp} &{field_name}"
1138
1139 def _emit_window_field_copy(self, hdr: TextIO, dest_expr: str, src_expr: str,
1140 field_type: types.ESIType) -> None:
1141 """Copy a generated window field."""
1142 hdr.write(f" {dest_expr} = {src_expr};\n")
1143
1144 def _field_byte_width(self, field_type: types.ESIType) -> int:
1145 """Compute the byte width of a field type, rounding up to full bytes."""
1146 return (field_type.bit_width + 7) // 8
1147
1148 def _safe_byte_width(self, esi_type: types.ESIType) -> Optional[int]:
1149 """Return the bounded byte width of `esi_type`, or `None` if it has no
1150 well-defined static size (e.g. unbounded `!esi.any` or recursive types).
1151 """
1152 try:
1153 bit_width = esi_type.bit_width
1154 except Exception:
1155 return None
1156 if bit_width is None or bit_width < 0:
1157 return None
1158 return (bit_width + 7) // 8
1159
1161 hdr: TextIO,
1162 type_name: str,
1163 expected_bytes: Optional[int],
1164 indent: str = "") -> None:
1165 """Emit a `static_assert` that pins the C++ `sizeof` of a packed type to
1166 the byte width derived from the manifest.
1167
1168 `std::array` and bit-field layout are technically implementation-defined,
1169 so this assertion is the safety net that catches a toolchain that lays
1170 them out differently from the wire format. Skipped silently for types
1171 without a bounded static size.
1172 """
1173 if expected_bytes is None:
1174 return
1175 hdr.write(
1176 f"{indent}static_assert(sizeof({type_name}) == {expected_bytes},\n"
1177 f"{indent} \"{type_name}: packed layout does not match "
1178 f"manifest size\");\n")
1179
1180 def _analyze_window(self, window_type: types.WindowType):
1181 """Extract the metadata needed to emit a bulk list window wrapper."""
1182 into_type = self._unwrap_aliases(window_type.into_type)
1183 if not isinstance(into_type, types.StructType):
1184 raise ValueError("window codegen currently requires a struct into-type")
1185
1186 field_map = {name: field_type for name, field_type in into_type.fields}
1187 list_fields = [
1188 (name, self._unwrap_aliases(field_type))
1189 for name, field_type in into_type.fields
1190 if isinstance(self._unwrap_aliases(field_type), types.ListType)
1191 ]
1192 if len(list_fields) != 1:
1193 raise ValueError("window codegen currently supports exactly one list")
1194
1195 list_field_name, list_type = list_fields[0]
1196 assert isinstance(list_type, types.ListType)
1197
1198 header_frame = None
1199 header_field = None
1200 data_frame = None
1201 data_field = None
1202 for frame in window_type.frames:
1203 for field in frame.fields:
1204 if field.name != list_field_name:
1205 continue
1206 if field.bulk_count_width > 0:
1207 header_frame = frame
1208 header_field = field
1209 elif field.num_items > 0:
1210 data_frame = frame
1211 data_field = field
1212
1213 if header_frame is None or header_field is None:
1214 raise ValueError("window codegen requires a bulk-count header frame")
1215 if data_frame is None or data_field is None:
1216 raise ValueError("window codegen requires a data frame for the list")
1217 if data_field.num_items != 1:
1218 raise ValueError("window codegen currently supports numItems == 1")
1219
1220 ctor_params = [(name, field_type)
1221 for name, field_type in into_type.fields
1222 if name != list_field_name]
1223
1224 header_fields = []
1225 header_bytes = 0
1226 count_field_name = f"{list_field_name}_count"
1227 count_width = header_field.bulk_count_width
1228 count_cpp = self._storage_type(count_width, signed=False)
1229 count_bytes = (count_width + 7) // 8
1230 for field in reversed(header_frame.fields):
1231 if field.name == list_field_name:
1232 header_fields.append((count_field_name, None))
1233 header_bytes += count_bytes
1234 else:
1235 field_type = field_map[field.name]
1236 header_fields.append((field.name, field_type))
1237 header_bytes += self._type_byte_width(field_type)
1238
1239 data_fields = []
1240 data_bytes = 0
1241 for field in reversed(data_frame.fields):
1242 if field.name == list_field_name:
1243 data_fields.append((list_field_name, list_type.element_type))
1244 data_bytes += self._type_byte_width(list_type.element_type)
1245 else:
1246 field_type = field_map[field.name]
1247 data_fields.append((field.name, field_type))
1248 data_bytes += self._type_byte_width(field_type)
1249
1250 frame_bytes = max(header_bytes, data_bytes)
1251
1252 return {
1253 "ctor_params": ctor_params,
1254 "count_cpp": count_cpp,
1255 "count_field_name": count_field_name,
1256 "count_width": count_width,
1257 "data_fields": data_fields,
1258 "data_pad_bytes": frame_bytes - data_bytes,
1259 "element_cpp": self._cpp_type(list_type.element_type),
1260 "frame_bytes": frame_bytes,
1261 "header_fields": header_fields,
1262 "header_pad_bytes": frame_bytes - header_bytes,
1263 "list_field_name": list_field_name,
1264 "window_name": self.type_id_map[window_type],
1265 }
1266
1267 def _emit_struct(self, hdr: TextIO, struct_type: types.StructType) -> None:
1268 """Emit a packed struct declaration plus its type id string."""
1269 fields = list(struct_type.fields)
1270 if struct_type.cpp_type.reverse:
1271 fields = list(reversed(fields))
1272 field_decls: List[str] = []
1273 for field_name, field_type in fields:
1274 field_cpp = self._cpp_type(field_type)
1275 if field_cpp == "void":
1276 # `void` is not a valid C++ field type; emit a comment so the field
1277 # remains visible in the generated header without breaking compilation.
1278 field_decls.append(f"// void {field_name};")
1279 continue
1280 wrapped = self._unwrap_aliases(field_type)
1281 if isinstance(wrapped, (types.BitsType, types.IntType)):
1282 # TODO: Bitfield layout is implementation-defined; consider
1283 # byte-aligned storage with explicit pack/unpack helpers.
1284 bitfield_width = wrapped.bit_width
1285 if bitfield_width >= 0:
1286 field_decls.append(f"{field_cpp} {field_name} : {bitfield_width};")
1287 else:
1288 field_decls.append(f"{field_cpp} {field_name};")
1289 else:
1290 field_decls.append(f"{field_cpp} {field_name};")
1291 struct_name = self.type_id_map[struct_type]
1292 hdr.write("#pragma pack(push, 1)\n")
1293 hdr.write(f"struct {struct_name} {{\n")
1294 for decl in field_decls:
1295 hdr.write(f" {decl}\n")
1296 hdr.write("\n")
1297 # Logical-order constructor: parameters follow `struct_type.fields` order
1298 # (the user-facing order from the manifest), independent of any wire-layout
1299 # reversal applied to the member declarations above.
1300 logical_fields = [(name, ftype)
1301 for name, ftype in struct_type.fields
1302 if self._cpp_type(ftype) != "void"]
1303 if logical_fields:
1304 hdr.write(f" {struct_name}() = default;\n")
1305 ctor_params = ", ".join(
1306 self._format_window_ctor_param(name, ftype)
1307 for name, ftype in logical_fields)
1308 inits = ", ".join(f"{name}({name})" for name, _ in logical_fields)
1309 hdr.write(f" {struct_name}({ctor_params}) : {inits} {{}}\n\n")
1310 hdr.write(
1311 f" static constexpr std::string_view _ESI_ID = {self._cpp_string_literal(struct_type.id)};\n"
1312 )
1313 hdr.write("};\n")
1314 self._emit_size_assert(hdr, struct_name, self._safe_byte_width(struct_type))
1315 hdr.write("#pragma pack(pop)\n\n")
1316
1317 def _emit_union(self, hdr: TextIO, union_type: types.UnionType) -> None:
1318 """Emit a packed union declaration plus its type id string.
1319
1320 Fields narrower than the union width get wrapper structs with a `_pad`
1321 byte array so the data sits at the MSB end, matching SV packed union
1322 layout where padding occupies the LSBs / lower addresses.
1323 """
1324 union_name = self.type_id_map[union_type]
1325 union_bytes = self._field_byte_width(union_type)
1326 fields = list(union_type.fields)
1327
1328 hdr.write("#pragma pack(push, 1)\n")
1329 # First pass: emit wrapper structs for fields that need padding.
1330 wrapper_names: Dict[str, str] = {}
1331 for field_name, field_type in fields:
1332 if self._cpp_type(field_type) == "void":
1333 continue
1334 field_bytes = self._field_byte_width(field_type)
1335 pad_bytes = union_bytes - field_bytes
1336 if pad_bytes > 0:
1337 wrapper = f"{union_name}_{field_name}"
1338 wrapper_names[field_name] = wrapper
1339 hdr.write(f"struct {wrapper} {{\n")
1340 hdr.write(f" uint8_t _pad[{pad_bytes}];\n")
1341 field_cpp = self._cpp_type(field_type)
1342 hdr.write(f" {field_cpp} {field_name};\n")
1343 hdr.write("};\n")
1344 self._emit_size_assert(hdr, wrapper, union_bytes)
1345
1346 # Second pass: emit the union itself.
1347 union_field_decls: List[str] = []
1348 for field_name, field_type in fields:
1349 field_cpp = self._cpp_type(field_type)
1350 if field_cpp == "void":
1351 # `void` is not a valid C++ union member; emit a comment placeholder.
1352 union_field_decls.append(f"// void {field_name};")
1353 elif field_name in wrapper_names:
1354 union_field_decls.append(f"{wrapper_names[field_name]} {field_name};")
1355 else:
1356 union_field_decls.append(f"{field_cpp} {field_name};")
1357 hdr.write(f"union {union_name} {{\n")
1358 for decl in union_field_decls:
1359 hdr.write(f" {decl}\n")
1360 hdr.write("\n")
1361 hdr.write(
1362 f" static constexpr std::string_view _ESI_ID = {self._cpp_string_literal(union_type.id)};\n"
1363 )
1364 hdr.write("};\n")
1365 self._emit_size_assert(hdr, union_name, union_bytes)
1366 hdr.write("#pragma pack(pop)\n\n")
1367
1368 def _emit_window(self, hdr: TextIO, window_type: types.WindowType) -> None:
1369 """Emit a SegmentedMessageData helper for a serial list window."""
1370 info = self._analyze_window(window_type)
1371 ctor_params = [
1372 self._format_window_ctor_param(name, field_type)
1373 for name, field_type in info["ctor_params"]
1374 ]
1375 value_ctor_params = list(ctor_params)
1376 value_ctor_params.append(
1377 f"const std::vector<value_type> &{info['list_field_name']}")
1378 value_ctor_signature = ", ".join(value_ctor_params)
1379 frame_ctor_params = list(ctor_params)
1380 frame_ctor_params.append("std::vector<data_frame> frames")
1381 frame_ctor_signature = ", ".join(frame_ctor_params)
1382 helper_args = ", ".join(name for name, _ in info["ctor_params"])
1383 helper_call = f"{helper_args}, std::move(frames)" if helper_args else "std::move(frames)"
1384
1385 hdr.write(
1386 f"struct {info['window_name']} : public esi::SegmentedMessageData {{\n")
1387 hdr.write("public:\n")
1388 hdr.write(f" using value_type = {info['element_cpp']};\n")
1389 hdr.write(f" using count_type = {info['count_cpp']};\n\n")
1390 hdr.write("#pragma pack(push, 1)\n")
1391 hdr.write(" struct data_frame {\n")
1392 if info["data_pad_bytes"] > 0:
1393 hdr.write(f" uint8_t _pad[{info['data_pad_bytes']}];\n")
1394 for field_name, field_type in info["data_fields"]:
1395 decl = self._format_window_field_decl(field_name, field_type)
1396 hdr.write(f" {decl}\n")
1397 hdr.write(" };\n")
1398 self._emit_size_assert(hdr, "data_frame", info["frame_bytes"], indent=" ")
1399 hdr.write("#pragma pack(pop)\n\n")
1400 hdr.write("private:\n")
1401 hdr.write("#pragma pack(push, 1)\n")
1402 hdr.write(" struct header_frame {\n")
1403 if info["header_pad_bytes"] > 0:
1404 hdr.write(f" uint8_t _pad[{info['header_pad_bytes']}];\n")
1405 for field_name, field_type in info["header_fields"]:
1406 if field_type is None:
1407 if info["count_width"] % 8 == 0:
1408 decl = f"count_type {field_name};"
1409 else:
1410 decl = f"count_type {field_name} : {info['count_width']};"
1411 else:
1412 decl = self._format_window_field_decl(field_name, field_type)
1413 hdr.write(f" {decl}\n")
1414 hdr.write(" };\n")
1415 self._emit_size_assert(hdr,
1416 "header_frame",
1417 info["frame_bytes"],
1418 indent=" ")
1419 hdr.write("#pragma pack(pop)\n\n")
1420 hdr.write(" header_frame header{};\n")
1421 hdr.write(" std::vector<data_frame> data_frames;\n")
1422 hdr.write(" header_frame footer{};\n\n")
1423 hdr.write(f" void construct({frame_ctor_signature}) {{\n")
1424 hdr.write(" if (frames.empty())\n")
1425 hdr.write(
1426 f" throw std::invalid_argument(\"{info['window_name']}: bulk windowed lists cannot be empty\");\n"
1427 )
1428 hdr.write(
1429 " if (frames.size() > std::numeric_limits<count_type>::max())\n")
1430 hdr.write(
1431 f" throw std::invalid_argument(\"{info['window_name']}: list too large for encoded count\");\n"
1432 )
1433 hdr.write(
1434 f" header.{info['count_field_name']} = static_cast<count_type>(frames.size());\n"
1435 )
1436 for name, _ in info["ctor_params"]:
1437 field_type = next(
1438 field_type for field_name, field_type in info["ctor_params"]
1439 if field_name == name)
1440 self._emit_window_field_copy(hdr, f"header.{name}", name, field_type)
1441 hdr.write(f" footer.{info['count_field_name']} = 0;\n")
1442 hdr.write(" data_frames = std::move(frames);\n")
1443 hdr.write(" }\n\n")
1444 hdr.write("public:\n")
1445 hdr.write(f" {info['window_name']}({frame_ctor_signature}) {{\n")
1446 hdr.write(f" construct({helper_call});\n")
1447 hdr.write(" }\n\n")
1448 hdr.write(f" {info['window_name']}({value_ctor_signature}) {{\n")
1449 hdr.write(" std::vector<data_frame> frames;\n")
1450 hdr.write(f" frames.reserve({info['list_field_name']}.size());\n")
1451 hdr.write(f" for (const auto &element : {info['list_field_name']}) {{\n")
1452 hdr.write(" auto &frame = frames.emplace_back();\n")
1453 hdr.write(f" frame.{info['list_field_name']} = element;\n")
1454 hdr.write(" }\n")
1455 hdr.write(f" construct({helper_call});\n")
1456 hdr.write(" }\n\n")
1457 hdr.write(" size_t numSegments() const override { return 3; }\n")
1458 hdr.write(" esi::Segment segment(size_t idx) const override {\n")
1459 hdr.write(" if (idx == 0)\n")
1460 hdr.write(
1461 " return {reinterpret_cast<const uint8_t *>(&header), sizeof(header)};\n"
1462 )
1463 hdr.write(" if (idx == 1)\n")
1464 hdr.write(
1465 " return {reinterpret_cast<const uint8_t *>(data_frames.data()),\n"
1466 )
1467 hdr.write(" data_frames.size() * sizeof(data_frame)};\n")
1468 hdr.write(" if (idx == 2)\n")
1469 hdr.write(
1470 " return {reinterpret_cast<const uint8_t *>(&footer), sizeof(footer)};\n"
1471 )
1472 hdr.write(
1473 f" throw std::out_of_range(\"{info['window_name']}: invalid segment index\");\n"
1474 )
1475 hdr.write(" }\n\n")
1476 hdr.write(
1477 f" static constexpr std::string_view _ESI_ID = {self._cpp_string_literal(self._unwrap_aliases(window_type.into_type).id)};\n"
1478 )
1479 # The into-type id alone cannot distinguish two different windows over
1480 # the same underlying struct (e.g. serial vs. parallel list encoding).
1481 # Emit the window id so the runtime can verify the wire format too.
1482 hdr.write(
1483 f" static constexpr std::string_view _ESI_WINDOW_ID = {self._cpp_string_literal(window_type.id)};\n"
1484 )
1485 self._emit_window_data_accessors(hdr, info)
1486 self._emit_window_deserializer(hdr, info)
1487 hdr.write("};\n\n")
1488
1489 def _emit_window_data_accessors(self, hdr: TextIO, info) -> None:
1490 """Emit accessors for the header and data fields of a window helper.
1491
1492 Exposes each static header field as a scalar accessor, the count of data
1493 frames, and one vector-valued accessor per data field so decoded values
1494 are easy to inspect on the read side.
1495 """
1496 list_field_name = info["list_field_name"]
1497 hdr.write("\n")
1498 for field_name, field_type in info["header_fields"]:
1499 # Skip the synthetic bulk-count field; it is exposed via
1500 # `<list>_count()` below.
1501 if field_type is None:
1502 continue
1503 cpp = self._cpp_type(field_type)
1504 unwrapped_header = self._unwrap_aliases(field_type)
1505 # Aggregate types (structs/unions/std::array) get a const-ref accessor;
1506 # bit-vector scalars are returned by value.
1507 if isinstance(unwrapped_header,
1509 hdr.write(
1510 f" {cpp} {field_name}() const {{ return header.{field_name}; }}\n")
1511 else:
1512 hdr.write(
1513 f" const {cpp} &{field_name}() const {{ return header.{field_name}; }}\n"
1514 )
1515 hdr.write(
1516 f" size_t {list_field_name}_count() const {{ return data_frames.size(); }}\n"
1517 )
1518 for field_name, field_type in info["data_fields"]:
1519 # All field types — scalars, structs, and arrays-as-`std::array` — are
1520 # uniformly addressable via pointer-to-member, except bit-fields which
1521 # have no pointer-to-member representation.
1522 unwrapped_data = self._unwrap_aliases(field_type)
1523 if field_name == list_field_name:
1524 elem_cpp = "value_type"
1525 else:
1526 elem_cpp = self._cpp_type(field_type)
1527 # C++ does not allow forming a pointer-to-member for a bit-field, so for
1528 # non-byte-aligned integer fields we fall back to a lambda projection
1529 # (which copies by value on each dereference) instead of a
1530 # pointer-to-member projection.
1531 is_bitfield = (isinstance(unwrapped_data,
1533 unwrapped_data.bit_width % 8 != 0)
1534 if is_bitfield:
1535 projection = f"[](const data_frame &f) {{ return f.{field_name}; }}"
1536 else:
1537 projection = f"&data_frame::{field_name}"
1538 hdr.write(
1539 f" auto {field_name}() const {{\n"
1540 f" return std::views::transform(data_frames, {projection});\n"
1541 f" }}\n")
1542 hdr.write(f" std::vector<{elem_cpp}> {field_name}_vector() const {{\n"
1543 f" std::vector<{elem_cpp}> out;\n"
1544 f" out.reserve(data_frames.size());\n"
1545 f" for (const auto &frame : data_frames)\n"
1546 f" out.push_back(frame.{field_name});\n"
1547 f" return out;\n"
1548 f" }}\n")
1549
1550 def _emit_window_deserializer(self, hdr: TextIO, info) -> None:
1551 """Emit a few bridge helpers + a `TypeDeserializer` alias.
1552
1553 The actual decoder lives in `esi::SerialListTypeDeserializer<T>`, which
1554 walks the header/data/footer burst protocol generically. Each window
1555 helper only has to expose:
1556
1557 - `_headerCount(const header_frame &)` -> `count_type`
1558 - `_fromFrames(const header_frame &, std::vector<data_frame> &&)`
1559 -> `std::unique_ptr<T>`
1560
1561 plus a `friend class esi::SerialListTypeDeserializer<T>;` so the template
1562 can reach the (private) `header_frame` definition.
1563 """
1564 window_name = info["window_name"]
1565 count_field_name = info["count_field_name"]
1566 ctor_args = ", ".join(f"h.{name}" for name, _ in info["ctor_params"])
1567 if ctor_args:
1568 ctor_args = f"{ctor_args}, std::move(frames)"
1569 else:
1570 ctor_args = "std::move(frames)"
1571
1572 hdr.write("\n")
1573 hdr.write("private:\n")
1574 hdr.write(
1575 " // Bridge helpers used by esi::SerialListTypeDeserializer<T>; the\n")
1576 hdr.write(
1577 " // template walks the serial-list burst protocol generically and\n")
1578 hdr.write(
1579 " // reaches into `header_frame` via the friend declaration below.\n")
1580 hdr.write(" static count_type _headerCount(const header_frame &h) {\n")
1581 hdr.write(f" return h.{count_field_name};\n")
1582 hdr.write(" }\n")
1583 hdr.write(f" static std::unique_ptr<{window_name}> _fromFrames(\n")
1584 hdr.write(
1585 " const header_frame &h, std::vector<data_frame> &&frames) {\n")
1586 hdr.write(f" return std::make_unique<{window_name}>({ctor_args});\n")
1587 hdr.write(" }\n")
1588 hdr.write(
1589 f" friend class esi::SerialListTypeDeserializer<{window_name}>;\n\n")
1590 hdr.write("public:\n")
1591 hdr.write(
1592 f" using TypeDeserializer = esi::SerialListTypeDeserializer<{window_name}>;\n"
1593 )
1594
1595 def _emit_alias(self, hdr: TextIO, alias_type: types.TypeAlias) -> None:
1596 """Emit a using alias when the alias targets a different C++ type."""
1597 inner_wrapped = alias_type.inner_type
1598 alias_name = self.type_id_map[alias_type]
1599 inner_cpp = None
1600 if inner_wrapped is not None:
1601 inner_cpp = self._cpp_type(inner_wrapped)
1602 if inner_cpp is None:
1603 inner_cpp = self.type_id_map[alias_type]
1604 if inner_cpp != alias_name:
1605 hdr.write(f"using {alias_name} = {inner_cpp};\n\n")
1606
1607 def write_header(self, output_dir: Path, system_name: str) -> None:
1608 """Emit the fully ordered types.h header into the output directory."""
1609 hdr_file = output_dir / "types.h"
1610 with open(hdr_file, "w") as hdr:
1611 hdr.write(
1612 textwrap.dedent(f"""
1613 // Generated header for {system_name} types.
1614 #pragma once
1615
1616 #include <cstdint>
1617 #include <cstddef>
1618 #include <any>
1619 #include <array>
1620 #include <limits>
1621 #include <ranges>
1622 #include <stdexcept>
1623 #include <string_view>
1624 #include <utility>
1625 #include <vector>
1626
1627 #include "esi/Common.h"
1628 #include "esi/TypedPorts.h"
1629
1630 namespace {system_name} {{
1631
1632 """))
1633 if self.has_cycle:
1634 sys.stderr.write("Warning: cyclic type dependencies detected.\n")
1635 sys.stderr.write(" Logically this should not be possible.\n")
1636 sys.stderr.write(
1637 " Emitted code may fail to compile due to ordering issues.\n")
1638
1639 for emit_type in self.ordered_types:
1640 try:
1641 if isinstance(emit_type, types.StructType):
1642 self._emit_struct(hdr, emit_type)
1643 elif isinstance(emit_type, types.UnionType):
1644 self._emit_union(hdr, emit_type)
1645 elif isinstance(emit_type, types.WindowType):
1646 self._emit_window(hdr, emit_type)
1647 elif isinstance(emit_type, types.TypeAlias):
1648 self._emit_alias(hdr, emit_type)
1649 except (ValueError, NotImplementedError) as e:
1650 sys.stderr.write(f"Warning: skipping type '{emit_type}': {e}\n")
1651 hdr.write(f"// Unsupported type '{emit_type}': {e}\n\n")
1652
1653 hdr.write(textwrap.dedent(f"""
1654 }} // namespace {system_name}
1655 """))
1656
1657
1658def run(generator: Type[Generator] = CppGenerator,
1659 cmdline_args=sys.argv) -> int:
1660 """Create and run a generator reading options from the command line."""
1661
1662 argparser = argparse.ArgumentParser(
1663 description=f"Generate {generator.language} headers from an ESI manifest",
1664 formatter_class=argparse.RawDescriptionHelpFormatter,
1665 epilog=textwrap.dedent("""
1666 Can read the manifest from either a file OR a running accelerator.
1667
1668 Usage examples:
1669 # To read the manifest from a file:
1670 esi-cppgen --file /path/to/manifest.json
1671
1672 # To read the manifest from a running accelerator:
1673 esi-cppgen --platform cosim --connection localhost:1234
1674 """))
1675
1676 argparser.add_argument("--file",
1677 type=str,
1678 default=None,
1679 help="Path to the manifest file.")
1680 argparser.add_argument(
1681 "--platform",
1682 type=str,
1683 help="Name of platform for live accelerator connection.")
1684 argparser.add_argument(
1685 "--connection",
1686 type=str,
1687 help="Connection string for live accelerator connection.")
1688 argparser.add_argument(
1689 "--output-dir",
1690 type=str,
1691 default="esi",
1692 help="Output directory for generated files. Recommend adding either `esi`"
1693 " or the system name to the end of the path so as to avoid header name"
1694 "conflicts. Defaults to `esi`")
1695 argparser.add_argument(
1696 "--system-name",
1697 type=str,
1698 default="esi_system",
1699 help="Name of the ESI system. For C++, this will be the namespace.")
1700
1701 if (len(cmdline_args) <= 1):
1702 argparser.print_help()
1703 return 1
1704 args = argparser.parse_args(cmdline_args[1:])
1705
1706 if args.file is not None and args.platform is not None:
1707 print("Cannot specify both --file and --platform")
1708 return 1
1709
1710 conn: AcceleratorConnection
1711 if args.file is not None:
1712 # Use os.pathsep (';' on Windows, ':' on Unix) to avoid conflicts with
1713 # drive letters.
1714 conn = Context.default().connect("trace", f"-{os.pathsep}{args.file}")
1715 elif args.platform is not None:
1716 if args.connection is None:
1717 print("Must specify --connection with --platform")
1718 return 1
1719 conn = Context.default().connect(args.platform, args.connection)
1720 else:
1721 print("Must specify either --file or --platform")
1722 return 1
1723
1724 output_dir = Path(args.output_dir)
1725 if output_dir.exists() and not output_dir.is_dir():
1726 print(f"Output directory {output_dir} is not a directory")
1727 return 1
1728 if not output_dir.exists():
1729 output_dir.mkdir(parents=True)
1730
1731 gen = generator(conn)
1732 gen.generate(output_dir, args.system_name)
1733 return 0
1734
1735
1736if __name__ == '__main__':
1737 sys.exit(run())
static void print(TypedAttr val, llvm::raw_ostream &os)
static mlir::Operation * resolve(Context &context, mlir::SymbolRefAttr sym)
#define isdigit(x)
Definition FIRLexer.cpp:26
static StringAttr append(StringAttr base, const Twine &suffix)
Return a attribute with the specified suffix appended.
_PortGroup _scalar_port_group(self, str member_name, port, appid)
Definition codegen.py:274
None _emit_module_class(self, str name, str system_name, ModuleInfo module_info, List[_PortGroup] port_groups, TextIO out)
Definition codegen.py:498
str _cpp_indexed_elem_type(self, port, Optional[str] alias_prefix=None)
Definition codegen.py:163
Dict[str, object] _build_module_instance_map(self)
Definition codegen.py:94
str _cpp_member_type(self, port, Optional[str] alias_prefix=None)
Definition codegen.py:109
str get_consts_str(self, ModuleInfo module_info)
Definition codegen.py:69
str _cpp_ctor_param_type(self, port)
Definition codegen.py:180
str _sanitize_id(str name)
Definition codegen.py:83
List[Tuple[str, str]] _port_using_aliases(self, str alias_prefix, port)
Definition codegen.py:146
str _port_make_unique_arg(str member_name, port)
Definition codegen.py:257
__init__(self, AcceleratorConnection conn)
Definition codegen.py:63
_PortGroup _indexed_ports_group(self, str member_name, str appid_name, port_list)
Definition codegen.py:304
write_modules(self, Path output_dir, str system_name)
Definition codegen.py:652
generate(self, Path output_dir, str system_name)
Definition codegen.py:678
_PortGroup _mixed_struct_group(self, str member_name, str appid_name, port_list)
Definition codegen.py:402
List[_PortGroup] _collect_port_groups(self, dict ports)
Definition codegen.py:452
str _port_find_code(self, str member_name, port, appid)
Definition codegen.py:214
None _emit_struct(self, TextIO hdr, types.StructType struct_type)
Definition codegen.py:1267
str _format_window_field_decl(self, str field_name, types.ESIType field_type)
Definition codegen.py:1113
_analyze_window(self, types.WindowType window_type)
Definition codegen.py:1180
None _emit_size_assert(self, TextIO hdr, str type_name, Optional[int] expected_bytes, str indent="")
Definition codegen.py:1164
None _emit_window_deserializer(self, TextIO hdr, info)
Definition codegen.py:1550
None _emit_window_data_accessors(self, TextIO hdr, info)
Definition codegen.py:1489
str _storage_type(self, int bit_width, bool signed)
Definition codegen.py:1029
str _std_array_type(self, types.ArrayType array_type)
Definition codegen.py:1066
str _get_bitvector_str(self, types.ESIType type)
Definition codegen.py:1022
None write_header(self, Path output_dir, str system_name)
Definition codegen.py:1607
str _format_window_ctor_param(self, str field_name, types.ESIType field_type)
Definition codegen.py:1127
None _emit_alias(self, TextIO hdr, types.TypeAlias alias_type)
Definition codegen.py:1595
None __init__(self, CppTypePlanner planner)
Definition codegen.py:1008
int _field_byte_width(self, types.ESIType field_type)
Definition codegen.py:1144
Tuple[str, List[int]] _array_base_and_dims(self, types.ArrayType array_type)
Definition codegen.py:1056
int _type_byte_width(self, types.ESIType wrapped)
Definition codegen.py:1049
types.ESIType _unwrap_aliases(self, types.ESIType wrapped)
Definition codegen.py:1106
str type_identifier(self, types.ESIType type)
Definition codegen.py:1013
None _emit_union(self, TextIO hdr, types.UnionType union_type)
Definition codegen.py:1317
str _cpp_string_literal(self, str value)
Definition codegen.py:1017
None _emit_window(self, TextIO hdr, types.WindowType window_type)
Definition codegen.py:1368
Optional[int] _safe_byte_width(self, types.ESIType esi_type)
Definition codegen.py:1148
None _emit_window_field_copy(self, TextIO hdr, str dest_expr, str src_expr, types.ESIType field_type)
Definition codegen.py:1140
str _cpp_type(self, types.ESIType wrapped)
Definition codegen.py:1080
str _auto_window_name(self, types.WindowType window_type)
Definition codegen.py:764
Set[types.ESIType] _collect_decls_from_window(self, types.WindowType window_type)
Definition codegen.py:922
types.ESIType _unwrap_aliases(self, types.ESIType wrapped)
Definition codegen.py:783
str _sanitize_name(self, str name)
Definition codegen.py:715
bool _is_supported_window(self, types.ESIType current_type)
Definition codegen.py:788
None _visit_types(self, types.ESIType t, Set[str] visited, visit_fn)
Definition codegen.py:844
str _reserve_name(self, str base, bool is_alias)
Definition codegen.py:732
Tuple[List[types.ESIType], bool] _ordered_emit_types(self)
Definition codegen.py:937
List[types.ESIType] _iter_type_children(self, types.ESIType t)
Definition codegen.py:824
None __init__(self, type_table)
Definition codegen.py:686
Set[types.ESIType] _collect_decls_from_type(self, types.ESIType wrapped)
Definition codegen.py:898
str _auto_struct_name(self, types.StructType struct_type)
Definition codegen.py:748
str _auto_union_name(self, types.UnionType union_type)
Definition codegen.py:756
None _collect_windows(self, types.ESIType t, Set[str] visited)
Definition codegen.py:884
None _collect_structs(self, types.ESIType t, Set[str] visited)
Definition codegen.py:870
None _prepare_types(self, type_table)
Definition codegen.py:698
None _collect_aliases(self, types.ESIType t, Set[str] visited)
Definition codegen.py:857
generate(self, Path output_dir, str system_name)
Definition codegen.py:54
__init__(self, AcceleratorConnection conn)
Definition codegen.py:51
int run(Type[Generator] generator=CppGenerator, cmdline_args=sys.argv)
Definition codegen.py:1659
"AcceleratorConnection" connect(str platform, str connection_str)
Definition __init__.py:27