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", encoding="utf-8") 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", encoding="utf-8") 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 _contains_window(self, esi_type: types.ESIType) -> bool:
938 """Return True if `esi_type` is or transitively contains a WindowType.
939
940 Structs (and aliases/unions that reference them) which embed a window
941 cannot be emitted as C++ packed structs because the C++ window helper
942 is a variable-size multi-frame container. This helper is used to
943 exclude such types from the emission list entirely.
944 """
945 unwrapped = self._unwrap_aliases(esi_type)
946 if isinstance(unwrapped, types.WindowType):
947 return True
948 if isinstance(unwrapped, types.StructType):
949 return any(self._contains_window(ft) for _, ft in unwrapped.fields)
950 if isinstance(unwrapped, types.UnionType):
951 return any(self._contains_window(ft) for _, ft in unwrapped.fields)
952 if isinstance(unwrapped, types.ArrayType):
953 return self._contains_window(unwrapped.element_type)
954 return False
955
956 def _ordered_emit_types(self) -> Tuple[List[types.ESIType], bool]:
957 """Collect and order types for deterministic emission."""
958 window_into_types: Set[types.ESIType] = set()
959 for esi_type in self.type_id_map.keys():
960 if not self._is_supported_window(esi_type):
961 continue
962 assert isinstance(esi_type, types.WindowType)
963 window_into_types.add(self._unwrap_aliases(esi_type.into_type))
964 emit_types: List[types.ESIType] = []
965 for esi_type in self.type_id_map.keys():
966 if isinstance(esi_type,
967 types.StructType) and esi_type in window_into_types:
968 continue
969 if isinstance(esi_type, types.TypeAlias):
970 inner = esi_type.inner_type
971 if inner is not None and self._unwrap_aliases(
972 inner) in window_into_types:
973 continue
974 # Skip structs/unions/aliases that transitively embed a WindowType field.
975 # WindowType itself is fine — it emits as its own helper class.
976 if (not isinstance(self._unwrap_aliases(esi_type), types.WindowType) and
977 self._contains_window(esi_type)):
978 continue
979 if (isinstance(esi_type,
981 self._is_supported_window(esi_type)):
982 emit_types.append(esi_type)
983
984 # Prefer alias-reserved names first, then lexicographic for determinism.
985 name_to_type = {self.type_id_map[t]: t for t in emit_types}
986 sorted_names = sorted(name_to_type.keys(),
987 key=lambda name:
988 (0 if self.used_names.get(name, False) else 1, name))
989
990 ordered: List[types.ESIType] = []
991 visited: Set[types.ESIType] = set()
992 visiting: Set[types.ESIType] = set()
993 has_cycle = False
994
995 # Visit callback: DFS to emit dependencies before their users.
996 def visit(current: types.ESIType) -> None:
997 nonlocal has_cycle
998 if current in visited:
999 return
1000 if current in visiting:
1001 has_cycle = True
1002 return
1003 visiting.add(current)
1004
1005 deps: Set[types.ESIType] = set()
1006 if isinstance(current, types.TypeAlias):
1007 inner = current.inner_type
1008 if inner is not None:
1009 deps.update(self._collect_decls_from_type(inner))
1010 elif isinstance(current, (types.StructType, types.UnionType)):
1011 for _, field_type in current.fields:
1012 deps.update(self._collect_decls_from_type(field_type))
1013 elif self._is_supported_window(current):
1014 assert isinstance(current, types.WindowType)
1015 deps.update(self._collect_decls_from_window(current))
1016 for dep in sorted(deps, key=lambda dep: self.type_id_map[dep]):
1017 visit(dep)
1018
1019 visiting.remove(current)
1020 visited.add(current)
1021 ordered.append(current)
1022
1023 for name in sorted_names:
1024 visit(name_to_type[name])
1025
1026 return ordered, has_cycle
1027
1028
1030 """Emit C++ headers from precomputed type ordering."""
1031
1032 def __init__(self, planner: CppTypePlanner) -> None:
1033 self.type_id_map = planner.type_id_map
1034 self.ordered_types = planner.ordered_types
1035 self.has_cycle = planner.has_cycle
1036
1037 def type_identifier(self, type: types.ESIType) -> str:
1038 """Get the C++ type string for an ESI type."""
1039 return self._cpp_type(type)
1040
1041 def _cpp_string_literal(self, value: str) -> str:
1042 """Escape a Python string for use as a C++ string literal."""
1043 escaped = value.replace("\\", "\\\\").replace('"', '\\"')
1044 return f'"{escaped}"'
1045
1046 def _get_bitvector_str(self, type: types.ESIType) -> str:
1047 """Get the textual code for the storage class of an integer type."""
1048 assert isinstance(type, (types.BitsType, types.IntType))
1049
1050 return self._storage_type(
1051 type.bit_width, not isinstance(type, (types.BitsType, types.UIntType)))
1052
1053 def _storage_type(self, bit_width: int, signed: bool) -> str:
1054 """Get the textual code for a byte-addressable integer storage type."""
1055
1056 if bit_width == 1:
1057 return "bool"
1058 elif bit_width <= 8:
1059 storage_width = 8
1060 elif bit_width <= 16:
1061 storage_width = 16
1062 elif bit_width <= 32:
1063 storage_width = 32
1064 elif bit_width <= 64:
1065 storage_width = 64
1066 else:
1067 raise ValueError(f"Unsupported integer width: {bit_width}")
1068
1069 if not signed:
1070 return f"uint{storage_width}_t"
1071 return f"int{storage_width}_t"
1072
1073 def _type_byte_width(self, wrapped: types.ESIType) -> int:
1074 """Return the size of a fixed-width type in bytes."""
1075 if wrapped.bit_width < 0:
1076 raise ValueError(f"Unsupported unbounded type width for '{wrapped}'")
1077 return (wrapped.bit_width + 7) // 8
1078
1080 self, array_type: types.ArrayType) -> Tuple[str, List[int]]:
1081 """Return the base C++ type and outer-to-inner dimensions of a nested array."""
1082 dims: List[int] = []
1083 inner: types.ESIType = array_type
1084 while isinstance(inner, types.ArrayType):
1085 dims.append(inner.size)
1086 inner = inner.element_type
1087 base_cpp = self._cpp_type(inner)
1088 return base_cpp, dims
1089
1090 def _std_array_type(self, array_type: types.ArrayType) -> str:
1091 """Return the equivalent nested `std::array<...>` type for an array.
1092
1093 `std::array<T, N>` is layout-compatible in practice with `T[N]` on every
1094 major implementation (and identical under `#pragma pack(1)`), so the
1095 generator uses it everywhere a fixed-size array would appear. This keeps
1096 field/value/ctor types storable in `std::vector` and assignable with `=`.
1097 """
1098 base_cpp, dims = self._array_base_and_dims(array_type)
1099 result = base_cpp
1100 for size in reversed(dims):
1101 result = f"std::array<{result}, {size}>"
1102 return result
1103
1104 def _cpp_type(self, wrapped: types.ESIType) -> str:
1105 """Resolve an ESI type to its C++ identifier."""
1106 if isinstance(wrapped, types.WindowType) and wrapped in self.type_id_map:
1107 return self.type_id_map[wrapped]
1108 if isinstance(wrapped,
1110 # Zero-width composite types (e.g. a struct of only void fields, or an
1111 # alias to such a struct) collapse to `void`. C++ structs are defined to
1112 # have `sizeof >= 1`, so there is no meaningful storage to emit; treat them
1113 # as void everywhere they appear so callers comment them out exactly
1114 # like a direct `VoidType` field.
1115 if wrapped.bit_width == 0:
1116 return "void"
1117 return self.type_id_map[wrapped]
1118 if isinstance(wrapped, types.BundleType):
1119 return "void"
1120 if isinstance(wrapped, types.ChannelType):
1121 return self._cpp_type(wrapped.inner)
1122 if isinstance(wrapped, types.ListType):
1123 raise ValueError("List types require a generated window wrapper")
1124 if isinstance(wrapped, types.VoidType):
1125 return "void"
1126 if isinstance(wrapped, types.AnyType):
1127 return "std::any"
1128 if isinstance(wrapped, (types.BitsType, types.IntType)):
1129 # A zero-width integer carries no data; emit it as `void` so it gets
1130 # commented out in field positions like any other void.
1131 if wrapped.bit_width == 0:
1132 return "void"
1133 return self._get_bitvector_str(wrapped)
1134 if isinstance(wrapped, types.ArrayType):
1135 # `std::array<void, N>` is ill-formed; arrays of zero-width elements
1136 # also collapse to `void`.
1137 if wrapped.bit_width == 0:
1138 return "void"
1139 return self._std_array_type(wrapped)
1140 if type(wrapped) is types.ESIType:
1141 return "std::any"
1142 raise NotImplementedError(
1143 f"Type '{wrapped}' not supported for C++ generation")
1144
1145 def _unwrap_aliases(self, wrapped: types.ESIType) -> types.ESIType:
1146 """Strip alias wrappers to reach the underlying type."""
1147 while isinstance(wrapped, types.TypeAlias):
1148 wrapped = wrapped.inner_type
1149 return wrapped
1150
1151 def _format_window_field_decl(self, field_name: str,
1152 field_type: types.ESIType) -> str:
1153 """Emit a packed field declaration for generated window helpers.
1154
1155 Arrays use `std::array` (handled by `_cpp_type`), so no bracket syntax is
1156 needed and a single uniform declaration form covers every type.
1157 """
1158 field_cpp = self._cpp_type(field_type)
1159 wrapped = self._unwrap_aliases(field_type)
1160 if isinstance(wrapped, (types.BitsType, types.IntType)) and \
1161 wrapped.bit_width % 8 != 0:
1162 return f"{field_cpp} {field_name} : {wrapped.bit_width};"
1163 return f"{field_cpp} {field_name};"
1164
1165 def _format_window_ctor_param(self, field_name: str,
1166 field_type: types.ESIType) -> str:
1167 """Emit a constructor parameter for generated window helpers.
1168
1169 Small scalar header fields are cheaper to pass by value than by reference.
1170 Larger aggregates stay as const references.
1171 """
1172 field_cpp = self._cpp_type(field_type)
1173 wrapped = self._unwrap_aliases(field_type)
1174 if isinstance(wrapped, (types.BitsType, types.IntType)):
1175 return f"{field_cpp} {field_name}"
1176 return f"const {field_cpp} &{field_name}"
1177
1178 def _emit_window_field_copy(self, hdr: TextIO, dest_expr: str, src_expr: str,
1179 field_type: types.ESIType) -> None:
1180 """Copy a generated window field."""
1181 hdr.write(f" {dest_expr} = {src_expr};\n")
1182
1183 def _field_byte_width(self, field_type: types.ESIType) -> int:
1184 """Compute the byte width of a field type, rounding up to full bytes."""
1185 return (field_type.bit_width + 7) // 8
1186
1187 def _safe_byte_width(self, esi_type: types.ESIType) -> Optional[int]:
1188 """Return the bounded byte width of `esi_type`, or `None` if it has no
1189 well-defined static size (e.g. unbounded `!esi.any` or recursive types).
1190 """
1191 try:
1192 bit_width = esi_type.bit_width
1193 except Exception:
1194 return None
1195 if bit_width is None or bit_width < 0:
1196 return None
1197 return (bit_width + 7) // 8
1198
1200 hdr: TextIO,
1201 type_name: str,
1202 expected_bytes: Optional[int],
1203 indent: str = "") -> None:
1204 """Emit a `static_assert` that pins the C++ `sizeof` of a packed type to
1205 the byte width derived from the manifest.
1206
1207 `std::array` and bit-field layout are technically implementation-defined,
1208 so this assertion is the safety net that catches a toolchain that lays
1209 them out differently from the wire format. Skipped silently for types
1210 without a bounded static size.
1211 """
1212 if expected_bytes is None:
1213 return
1214 hdr.write(
1215 f"{indent}static_assert(sizeof({type_name}) == {expected_bytes},\n"
1216 f"{indent} \"{type_name}: packed layout does not match "
1217 f"manifest size\");\n")
1218
1219 def _analyze_window(self, window_type: types.WindowType):
1220 """Extract the metadata needed to emit a bulk list window wrapper."""
1221 into_type = self._unwrap_aliases(window_type.into_type)
1222 if not isinstance(into_type, types.StructType):
1223 raise ValueError("window codegen currently requires a struct into-type")
1224
1225 field_map = {name: field_type for name, field_type in into_type.fields}
1226 list_fields = [
1227 (name, self._unwrap_aliases(field_type))
1228 for name, field_type in into_type.fields
1229 if isinstance(self._unwrap_aliases(field_type), types.ListType)
1230 ]
1231 if len(list_fields) != 1:
1232 raise ValueError("window codegen currently supports exactly one list")
1233
1234 list_field_name, list_type = list_fields[0]
1235 assert isinstance(list_type, types.ListType)
1236
1237 header_frame = None
1238 header_field = None
1239 data_frame = None
1240 data_field = None
1241 for frame in window_type.frames:
1242 for field in frame.fields:
1243 if field.name != list_field_name:
1244 continue
1245 if field.bulk_count_width > 0:
1246 header_frame = frame
1247 header_field = field
1248 elif field.num_items > 0:
1249 data_frame = frame
1250 data_field = field
1251
1252 if header_frame is None or header_field is None:
1253 raise ValueError("window codegen requires a bulk-count header frame")
1254 if data_frame is None or data_field is None:
1255 raise ValueError("window codegen requires a data frame for the list")
1256 if data_field.num_items != 1:
1257 raise ValueError("window codegen currently supports numItems == 1")
1258
1259 ctor_params = [(name, field_type)
1260 for name, field_type in into_type.fields
1261 if name != list_field_name]
1262
1263 header_fields = []
1264 header_bytes = 0
1265 count_field_name = f"{list_field_name}_count"
1266 count_width = header_field.bulk_count_width
1267 count_cpp = self._storage_type(count_width, signed=False)
1268 count_bytes = (count_width + 7) // 8
1269 for field in reversed(header_frame.fields):
1270 if field.name == list_field_name:
1271 header_fields.append((count_field_name, None))
1272 header_bytes += count_bytes
1273 else:
1274 field_type = field_map[field.name]
1275 header_fields.append((field.name, field_type))
1276 header_bytes += self._type_byte_width(field_type)
1277
1278 data_fields = []
1279 data_bytes = 0
1280 for field in reversed(data_frame.fields):
1281 if field.name == list_field_name:
1282 data_fields.append((list_field_name, list_type.element_type))
1283 data_bytes += self._type_byte_width(list_type.element_type)
1284 else:
1285 field_type = field_map[field.name]
1286 data_fields.append((field.name, field_type))
1287 data_bytes += self._type_byte_width(field_type)
1288
1289 frame_bytes = max(header_bytes, data_bytes)
1290
1291 return {
1292 "ctor_params": ctor_params,
1293 "count_cpp": count_cpp,
1294 "count_field_name": count_field_name,
1295 "count_width": count_width,
1296 "data_fields": data_fields,
1297 "data_pad_bytes": frame_bytes - data_bytes,
1298 "element_cpp": self._cpp_type(list_type.element_type),
1299 "frame_bytes": frame_bytes,
1300 "header_fields": header_fields,
1301 "header_pad_bytes": frame_bytes - header_bytes,
1302 "list_field_name": list_field_name,
1303 "window_name": self.type_id_map[window_type],
1304 }
1305
1306 def _emit_struct(self, hdr: TextIO, struct_type: types.StructType) -> None:
1307 """Emit a packed struct declaration plus its type id string."""
1308 # Zero-width composite types collapse to `void` everywhere they appear
1309 # (see _cpp_type), so there is nothing meaningful to emit here.
1310 if struct_type.bit_width == 0:
1311 return
1312 fields = list(struct_type.fields)
1313 if struct_type.cpp_type.reverse:
1314 fields = list(reversed(fields))
1315 field_decls: List[str] = []
1316 for field_name, field_type in fields:
1317 field_cpp = self._cpp_type(field_type)
1318 if field_cpp == "void":
1319 # `void` is not a valid C++ field type; emit a comment so the field
1320 # remains visible in the generated header without breaking compilation.
1321 field_decls.append(f"// void {field_name};")
1322 continue
1323 wrapped = self._unwrap_aliases(field_type)
1324 if isinstance(wrapped, (types.BitsType, types.IntType)):
1325 # TODO: Bitfield layout is implementation-defined; consider
1326 # byte-aligned storage with explicit pack/unpack helpers.
1327 bitfield_width = wrapped.bit_width
1328 if bitfield_width >= 0:
1329 field_decls.append(f"{field_cpp} {field_name} : {bitfield_width};")
1330 else:
1331 field_decls.append(f"{field_cpp} {field_name};")
1332 else:
1333 field_decls.append(f"{field_cpp} {field_name};")
1334 struct_name = self.type_id_map[struct_type]
1335 hdr.write("#pragma pack(push, 1)\n")
1336 hdr.write(f"struct {struct_name} {{\n")
1337 for decl in field_decls:
1338 hdr.write(f" {decl}\n")
1339 hdr.write("\n")
1340 # Logical-order constructor: parameters follow `struct_type.fields` order
1341 # (the user-facing order from the manifest), independent of any wire-layout
1342 # reversal applied to the member declarations above.
1343 logical_fields = [(name, ftype)
1344 for name, ftype in struct_type.fields
1345 if self._cpp_type(ftype) != "void"]
1346 if logical_fields:
1347 hdr.write(f" {struct_name}() = default;\n")
1348 ctor_params = ", ".join(
1349 self._format_window_ctor_param(name, ftype)
1350 for name, ftype in logical_fields)
1351 inits = ", ".join(f"{name}({name})" for name, _ in logical_fields)
1352 hdr.write(f" {struct_name}({ctor_params}) : {inits} {{}}\n\n")
1353 hdr.write(
1354 f" static constexpr std::string_view _ESI_ID = {self._cpp_string_literal(struct_type.id)};\n"
1355 )
1356 hdr.write("};\n")
1357 # Void / zero-width fields are commented out and contribute 0 to the
1358 # struct's bit_width (see VoidType.bit_width), so the manifest-derived
1359 # size already matches the emitted C++ layout. An all-void or empty
1360 # struct still has `sizeof >= 1` in C++, so skip the assert in that
1361 # degenerate case.
1362 expected_bytes = self._safe_byte_width(struct_type)
1363 if expected_bytes is not None and expected_bytes > 0:
1364 self._emit_size_assert(hdr, struct_name, expected_bytes)
1365 hdr.write("#pragma pack(pop)\n\n")
1366
1367 def _emit_union(self, hdr: TextIO, union_type: types.UnionType) -> None:
1368 """Emit a packed union declaration plus its type id string.
1369
1370 Fields narrower than the union width get wrapper structs with a `_pad`
1371 byte array so the data sits at the MSB end, matching SV packed union
1372 layout where padding occupies the LSBs / lower addresses.
1373 """
1374 # Zero-width unions collapse to `void` (see _cpp_type) so there is
1375 # nothing meaningful to emit here.
1376 if union_type.bit_width == 0:
1377 return
1378 union_name = self.type_id_map[union_type]
1379 fields = list(union_type.fields)
1380 union_bytes = self._field_byte_width(union_type)
1381
1382 hdr.write("#pragma pack(push, 1)\n")
1383 # First pass: emit wrapper structs for fields that need padding.
1384 wrapper_names: Dict[str, str] = {}
1385 for field_name, field_type in fields:
1386 if self._cpp_type(field_type) == "void":
1387 continue
1388 field_bytes = self._field_byte_width(field_type)
1389 pad_bytes = union_bytes - field_bytes
1390 if pad_bytes > 0:
1391 wrapper = f"{union_name}_{field_name}"
1392 wrapper_names[field_name] = wrapper
1393 hdr.write(f"struct {wrapper} {{\n")
1394 hdr.write(f" uint8_t _pad[{pad_bytes}];\n")
1395 field_cpp = self._cpp_type(field_type)
1396 hdr.write(f" {field_cpp} {field_name};\n")
1397 hdr.write("};\n")
1398 self._emit_size_assert(hdr, wrapper, union_bytes)
1399
1400 # Second pass: emit the union itself.
1401 union_field_decls: List[str] = []
1402 for field_name, field_type in fields:
1403 field_cpp = self._cpp_type(field_type)
1404 if field_cpp == "void":
1405 # `void` is not a valid C++ union member; emit a comment placeholder.
1406 union_field_decls.append(f"// void {field_name};")
1407 elif field_name in wrapper_names:
1408 union_field_decls.append(f"{wrapper_names[field_name]} {field_name};")
1409 else:
1410 union_field_decls.append(f"{field_cpp} {field_name};")
1411 hdr.write(f"union {union_name} {{\n")
1412 for decl in union_field_decls:
1413 hdr.write(f" {decl}\n")
1414 hdr.write("\n")
1415 hdr.write(
1416 f" static constexpr std::string_view _ESI_ID = {self._cpp_string_literal(union_type.id)};\n"
1417 )
1418 hdr.write("};\n")
1419 # Skip the assertion for all-void / empty unions; an empty union has
1420 # `sizeof >= 1` in C++ and would always fail an `== 0` assert.
1421 if union_bytes > 0:
1422 self._emit_size_assert(hdr, union_name, union_bytes)
1423 hdr.write("#pragma pack(pop)\n\n")
1424
1425 def _emit_window(self, hdr: TextIO, window_type: types.WindowType) -> None:
1426 """Emit a SegmentedMessageData helper for a serial list window."""
1427 info = self._analyze_window(window_type)
1428 ctor_params = [
1429 self._format_window_ctor_param(name, field_type)
1430 for name, field_type in info["ctor_params"]
1431 ]
1432 value_ctor_params = list(ctor_params)
1433 value_ctor_params.append(
1434 f"const std::vector<value_type> &{info['list_field_name']}")
1435 value_ctor_signature = ", ".join(value_ctor_params)
1436 frame_ctor_params = list(ctor_params)
1437 frame_ctor_params.append("std::vector<data_frame> frames")
1438 frame_ctor_signature = ", ".join(frame_ctor_params)
1439 helper_args = ", ".join(name for name, _ in info["ctor_params"])
1440 helper_call = f"{helper_args}, std::move(frames)" if helper_args else "std::move(frames)"
1441
1442 hdr.write(
1443 f"struct {info['window_name']} : public esi::SegmentedMessageData {{\n")
1444 hdr.write("public:\n")
1445 hdr.write(f" using value_type = {info['element_cpp']};\n")
1446 hdr.write(f" using count_type = {info['count_cpp']};\n\n")
1447 hdr.write("#pragma pack(push, 1)\n")
1448 hdr.write(" struct data_frame {\n")
1449 if info["data_pad_bytes"] > 0:
1450 hdr.write(f" uint8_t _pad[{info['data_pad_bytes']}];\n")
1451 for field_name, field_type in info["data_fields"]:
1452 decl = self._format_window_field_decl(field_name, field_type)
1453 hdr.write(f" {decl}\n")
1454 hdr.write(" };\n")
1455 self._emit_size_assert(hdr, "data_frame", info["frame_bytes"], indent=" ")
1456 hdr.write("#pragma pack(pop)\n\n")
1457 hdr.write("private:\n")
1458 hdr.write("#pragma pack(push, 1)\n")
1459 hdr.write(" struct header_frame {\n")
1460 if info["header_pad_bytes"] > 0:
1461 hdr.write(f" uint8_t _pad[{info['header_pad_bytes']}];\n")
1462 for field_name, field_type in info["header_fields"]:
1463 if field_type is None:
1464 if info["count_width"] % 8 == 0:
1465 decl = f"count_type {field_name};"
1466 else:
1467 decl = f"count_type {field_name} : {info['count_width']};"
1468 else:
1469 decl = self._format_window_field_decl(field_name, field_type)
1470 hdr.write(f" {decl}\n")
1471 hdr.write(" };\n")
1472 self._emit_size_assert(hdr,
1473 "header_frame",
1474 info["frame_bytes"],
1475 indent=" ")
1476 hdr.write("#pragma pack(pop)\n\n")
1477 hdr.write(" header_frame header{};\n")
1478 hdr.write(" std::vector<data_frame> data_frames;\n")
1479 hdr.write(" header_frame footer{};\n\n")
1480 hdr.write(f" void construct({frame_ctor_signature}) {{\n")
1481 hdr.write(" if (frames.empty())\n")
1482 hdr.write(
1483 f" throw std::invalid_argument(\"{info['window_name']}: bulk windowed lists cannot be empty\");\n"
1484 )
1485 hdr.write(
1486 " if (frames.size() > std::numeric_limits<count_type>::max())\n")
1487 hdr.write(
1488 f" throw std::invalid_argument(\"{info['window_name']}: list too large for encoded count\");\n"
1489 )
1490 hdr.write(
1491 f" header.{info['count_field_name']} = static_cast<count_type>(frames.size());\n"
1492 )
1493 for name, _ in info["ctor_params"]:
1494 field_type = next(
1495 field_type for field_name, field_type in info["ctor_params"]
1496 if field_name == name)
1497 self._emit_window_field_copy(hdr, f"header.{name}", name, field_type)
1498 hdr.write(f" footer.{info['count_field_name']} = 0;\n")
1499 hdr.write(" data_frames = std::move(frames);\n")
1500 hdr.write(" }\n\n")
1501 hdr.write("public:\n")
1502 hdr.write(f" {info['window_name']}({frame_ctor_signature}) {{\n")
1503 hdr.write(f" construct({helper_call});\n")
1504 hdr.write(" }\n\n")
1505 hdr.write(f" {info['window_name']}({value_ctor_signature}) {{\n")
1506 hdr.write(" std::vector<data_frame> frames;\n")
1507 hdr.write(f" frames.reserve({info['list_field_name']}.size());\n")
1508 hdr.write(f" for (const auto &element : {info['list_field_name']}) {{\n")
1509 hdr.write(" auto &frame = frames.emplace_back();\n")
1510 hdr.write(f" frame.{info['list_field_name']} = element;\n")
1511 hdr.write(" }\n")
1512 hdr.write(f" construct({helper_call});\n")
1513 hdr.write(" }\n\n")
1514 hdr.write(" size_t numSegments() const override { return 3; }\n")
1515 hdr.write(" esi::Segment segment(size_t idx) const override {\n")
1516 hdr.write(" if (idx == 0)\n")
1517 hdr.write(
1518 " return {reinterpret_cast<const uint8_t *>(&header), sizeof(header)};\n"
1519 )
1520 hdr.write(" if (idx == 1)\n")
1521 hdr.write(
1522 " return {reinterpret_cast<const uint8_t *>(data_frames.data()),\n"
1523 )
1524 hdr.write(" data_frames.size() * sizeof(data_frame)};\n")
1525 hdr.write(" if (idx == 2)\n")
1526 hdr.write(
1527 " return {reinterpret_cast<const uint8_t *>(&footer), sizeof(footer)};\n"
1528 )
1529 hdr.write(
1530 f" throw std::out_of_range(\"{info['window_name']}: invalid segment index\");\n"
1531 )
1532 hdr.write(" }\n\n")
1533 hdr.write(
1534 f" static constexpr std::string_view _ESI_ID = {self._cpp_string_literal(self._unwrap_aliases(window_type.into_type).id)};\n"
1535 )
1536 # The into-type id alone cannot distinguish two different windows over
1537 # the same underlying struct (e.g. serial vs. parallel list encoding).
1538 # Emit the window id so the runtime can verify the wire format too.
1539 hdr.write(
1540 f" static constexpr std::string_view _ESI_WINDOW_ID = {self._cpp_string_literal(window_type.id)};\n"
1541 )
1542 self._emit_window_data_accessors(hdr, info)
1543 self._emit_window_deserializer(hdr, info)
1544 hdr.write("};\n\n")
1545
1546 def _emit_window_data_accessors(self, hdr: TextIO, info) -> None:
1547 """Emit accessors for the header and data fields of a window helper.
1548
1549 Exposes each static header field as a scalar accessor, the count of data
1550 frames, and one vector-valued accessor per data field so decoded values
1551 are easy to inspect on the read side.
1552 """
1553 list_field_name = info["list_field_name"]
1554 hdr.write("\n")
1555 for field_name, field_type in info["header_fields"]:
1556 # Skip the synthetic bulk-count field; it is exposed via
1557 # `<list>_count()` below.
1558 if field_type is None:
1559 continue
1560 cpp = self._cpp_type(field_type)
1561 unwrapped_header = self._unwrap_aliases(field_type)
1562 # Aggregate types (structs/unions/std::array) get a const-ref accessor;
1563 # bit-vector scalars are returned by value.
1564 if isinstance(unwrapped_header,
1566 hdr.write(
1567 f" {cpp} {field_name}() const {{ return header.{field_name}; }}\n")
1568 else:
1569 hdr.write(
1570 f" const {cpp} &{field_name}() const {{ return header.{field_name}; }}\n"
1571 )
1572 hdr.write(
1573 f" size_t {list_field_name}_count() const {{ return data_frames.size(); }}\n"
1574 )
1575 for field_name, field_type in info["data_fields"]:
1576 # All field types — scalars, structs, and arrays-as-`std::array` — are
1577 # uniformly addressable via pointer-to-member, except bit-fields which
1578 # have no pointer-to-member representation.
1579 unwrapped_data = self._unwrap_aliases(field_type)
1580 if field_name == list_field_name:
1581 elem_cpp = "value_type"
1582 else:
1583 elem_cpp = self._cpp_type(field_type)
1584 # C++ does not allow forming a pointer-to-member for a bit-field, so for
1585 # non-byte-aligned integer fields we fall back to a lambda projection
1586 # (which copies by value on each dereference) instead of a
1587 # pointer-to-member projection.
1588 is_bitfield = (isinstance(unwrapped_data,
1590 unwrapped_data.bit_width % 8 != 0)
1591 if is_bitfield:
1592 projection = f"[](const data_frame &f) {{ return f.{field_name}; }}"
1593 else:
1594 projection = f"&data_frame::{field_name}"
1595 hdr.write(
1596 f" auto {field_name}() const {{\n"
1597 f" return std::views::transform(data_frames, {projection});\n"
1598 f" }}\n")
1599 hdr.write(f" std::vector<{elem_cpp}> {field_name}_vector() const {{\n"
1600 f" std::vector<{elem_cpp}> out;\n"
1601 f" out.reserve(data_frames.size());\n"
1602 f" for (const auto &frame : data_frames)\n"
1603 f" out.push_back(frame.{field_name});\n"
1604 f" return out;\n"
1605 f" }}\n")
1606
1607 def _emit_window_deserializer(self, hdr: TextIO, info) -> None:
1608 """Emit a few bridge helpers + a `TypeDeserializer` alias.
1609
1610 The actual decoder lives in `esi::SerialListTypeDeserializer<T>`, which
1611 walks the header/data/footer burst protocol generically. Each window
1612 helper only has to expose:
1613
1614 - `_headerCount(const header_frame &)` -> `count_type`
1615 - `_fromFrames(const header_frame &, std::vector<data_frame> &&)`
1616 -> `std::unique_ptr<T>`
1617
1618 plus a `friend class esi::SerialListTypeDeserializer<T>;` so the template
1619 can reach the (private) `header_frame` definition.
1620 """
1621 window_name = info["window_name"]
1622 count_field_name = info["count_field_name"]
1623 ctor_args = ", ".join(f"h.{name}" for name, _ in info["ctor_params"])
1624 if ctor_args:
1625 ctor_args = f"{ctor_args}, std::move(frames)"
1626 else:
1627 ctor_args = "std::move(frames)"
1628
1629 hdr.write("\n")
1630 hdr.write("private:\n")
1631 hdr.write(
1632 " // Bridge helpers used by esi::SerialListTypeDeserializer<T>; the\n")
1633 hdr.write(
1634 " // template walks the serial-list burst protocol generically and\n")
1635 hdr.write(
1636 " // reaches into `header_frame` via the friend declaration below.\n")
1637 hdr.write(" static count_type _headerCount(const header_frame &h) {\n")
1638 hdr.write(f" return h.{count_field_name};\n")
1639 hdr.write(" }\n")
1640 hdr.write(f" static std::unique_ptr<{window_name}> _fromFrames(\n")
1641 hdr.write(
1642 " const header_frame &h, std::vector<data_frame> &&frames) {\n")
1643 hdr.write(f" return std::make_unique<{window_name}>({ctor_args});\n")
1644 hdr.write(" }\n")
1645 hdr.write(
1646 f" friend class esi::SerialListTypeDeserializer<{window_name}>;\n\n")
1647 hdr.write("public:\n")
1648 hdr.write(
1649 f" using TypeDeserializer = esi::SerialListTypeDeserializer<{window_name}>;\n"
1650 )
1651
1652 def _emit_alias(self, hdr: TextIO, alias_type: types.TypeAlias) -> None:
1653 """Emit a using alias when the alias targets a different C++ type."""
1654 inner_wrapped = alias_type.inner_type
1655 alias_name = self.type_id_map[alias_type]
1656 inner_cpp = None
1657 if inner_wrapped is not None:
1658 inner_cpp = self._cpp_type(inner_wrapped)
1659 if inner_cpp is None:
1660 inner_cpp = self.type_id_map[alias_type]
1661 if inner_cpp != alias_name:
1662 hdr.write(f"using {alias_name} = {inner_cpp};\n\n")
1663
1664 def write_header(self, output_dir: Path, system_name: str) -> None:
1665 """Emit the fully ordered types.h header into the output directory."""
1666 hdr_file = output_dir / "types.h"
1667 with open(hdr_file, "w", encoding="utf-8") as hdr:
1668 hdr.write(
1669 textwrap.dedent(f"""
1670 // Generated header for {system_name} types.
1671 #pragma once
1672
1673 #include <cstdint>
1674 #include <cstddef>
1675 #include <any>
1676 #include <array>
1677 #include <limits>
1678 #include <ranges>
1679 #include <stdexcept>
1680 #include <string_view>
1681 #include <utility>
1682 #include <vector>
1683
1684 #include "esi/Common.h"
1685 #include "esi/TypedPorts.h"
1686
1687 namespace {system_name} {{
1688
1689 """))
1690 if self.has_cycle:
1691 sys.stderr.write("Warning: cyclic type dependencies detected.\n")
1692 sys.stderr.write(" Logically this should not be possible.\n")
1693 sys.stderr.write(
1694 " Emitted code may fail to compile due to ordering issues.\n")
1695
1696 for emit_type in self.ordered_types:
1697 try:
1698 if isinstance(emit_type, types.StructType):
1699 self._emit_struct(hdr, emit_type)
1700 elif isinstance(emit_type, types.UnionType):
1701 self._emit_union(hdr, emit_type)
1702 elif isinstance(emit_type, types.WindowType):
1703 self._emit_window(hdr, emit_type)
1704 elif isinstance(emit_type, types.TypeAlias):
1705 self._emit_alias(hdr, emit_type)
1706 except (ValueError, NotImplementedError) as e:
1707 sys.stderr.write(f"Warning: skipping type '{emit_type}': {e}\n")
1708 hdr.write(f"// Unsupported type '{emit_type}': {e}\n\n")
1709
1710 hdr.write(textwrap.dedent(f"""
1711 }} // namespace {system_name}
1712 """))
1713
1714
1715def run(generator: Type[Generator] = CppGenerator,
1716 cmdline_args=sys.argv) -> int:
1717 """Create and run a generator reading options from the command line."""
1718
1719 argparser = argparse.ArgumentParser(
1720 description=f"Generate {generator.language} headers from an ESI manifest",
1721 formatter_class=argparse.RawDescriptionHelpFormatter,
1722 epilog=textwrap.dedent("""
1723 Can read the manifest from either a file OR a running accelerator.
1724
1725 Usage examples:
1726 # To read the manifest from a file:
1727 esi-cppgen --file /path/to/manifest.json
1728
1729 # To read the manifest from a running accelerator:
1730 esi-cppgen --platform cosim --connection localhost:1234
1731 """))
1732
1733 argparser.add_argument("--file",
1734 type=str,
1735 default=None,
1736 help="Path to the manifest file.")
1737 argparser.add_argument(
1738 "--platform",
1739 type=str,
1740 help="Name of platform for live accelerator connection.")
1741 argparser.add_argument(
1742 "--connection",
1743 type=str,
1744 help="Connection string for live accelerator connection.")
1745 argparser.add_argument(
1746 "--output-dir",
1747 type=str,
1748 default="esi",
1749 help="Output directory for generated files. Recommend adding either `esi`"
1750 " or the system name to the end of the path so as to avoid header name"
1751 "conflicts. Defaults to `esi`")
1752 argparser.add_argument(
1753 "--system-name",
1754 type=str,
1755 default="esi_system",
1756 help="Name of the ESI system. For C++, this will be the namespace.")
1757
1758 if (len(cmdline_args) <= 1):
1759 argparser.print_help()
1760 return 1
1761 args = argparser.parse_args(cmdline_args[1:])
1762
1763 if args.file is not None and args.platform is not None:
1764 print("Cannot specify both --file and --platform")
1765 return 1
1766
1767 conn: AcceleratorConnection
1768 if args.file is not None:
1769 # Use os.pathsep (';' on Windows, ':' on Unix) to avoid conflicts with
1770 # drive letters.
1771 conn = Context.default().connect("trace", f"-{os.pathsep}{args.file}")
1772 elif args.platform is not None:
1773 if args.connection is None:
1774 print("Must specify --connection with --platform")
1775 return 1
1776 conn = Context.default().connect(args.platform, args.connection)
1777 else:
1778 print("Must specify either --file or --platform")
1779 return 1
1780
1781 output_dir = Path(args.output_dir)
1782 if output_dir.exists() and not output_dir.is_dir():
1783 print(f"Output directory {output_dir} is not a directory")
1784 return 1
1785 if not output_dir.exists():
1786 output_dir.mkdir(parents=True)
1787
1788 gen = generator(conn)
1789 gen.generate(output_dir, args.system_name)
1790 return 0
1791
1792
1793if __name__ == '__main__':
1794 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:1306
str _format_window_field_decl(self, str field_name, types.ESIType field_type)
Definition codegen.py:1152
_analyze_window(self, types.WindowType window_type)
Definition codegen.py:1219
None _emit_size_assert(self, TextIO hdr, str type_name, Optional[int] expected_bytes, str indent="")
Definition codegen.py:1203
None _emit_window_deserializer(self, TextIO hdr, info)
Definition codegen.py:1607
None _emit_window_data_accessors(self, TextIO hdr, info)
Definition codegen.py:1546
str _storage_type(self, int bit_width, bool signed)
Definition codegen.py:1053
str _std_array_type(self, types.ArrayType array_type)
Definition codegen.py:1090
str _get_bitvector_str(self, types.ESIType type)
Definition codegen.py:1046
None write_header(self, Path output_dir, str system_name)
Definition codegen.py:1664
str _format_window_ctor_param(self, str field_name, types.ESIType field_type)
Definition codegen.py:1166
None _emit_alias(self, TextIO hdr, types.TypeAlias alias_type)
Definition codegen.py:1652
None __init__(self, CppTypePlanner planner)
Definition codegen.py:1032
int _field_byte_width(self, types.ESIType field_type)
Definition codegen.py:1183
Tuple[str, List[int]] _array_base_and_dims(self, types.ArrayType array_type)
Definition codegen.py:1080
int _type_byte_width(self, types.ESIType wrapped)
Definition codegen.py:1073
types.ESIType _unwrap_aliases(self, types.ESIType wrapped)
Definition codegen.py:1145
str type_identifier(self, types.ESIType type)
Definition codegen.py:1037
None _emit_union(self, TextIO hdr, types.UnionType union_type)
Definition codegen.py:1367
str _cpp_string_literal(self, str value)
Definition codegen.py:1041
None _emit_window(self, TextIO hdr, types.WindowType window_type)
Definition codegen.py:1425
Optional[int] _safe_byte_width(self, types.ESIType esi_type)
Definition codegen.py:1187
None _emit_window_field_copy(self, TextIO hdr, str dest_expr, str src_expr, types.ESIType field_type)
Definition codegen.py:1179
str _cpp_type(self, types.ESIType wrapped)
Definition codegen.py:1104
str _auto_window_name(self, types.WindowType window_type)
Definition codegen.py:764
bool _contains_window(self, types.ESIType esi_type)
Definition codegen.py:937
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:956
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:1716
"AcceleratorConnection" connect(str platform, str connection_str)
Definition __init__.py:27