4"""Code generation from ESI manifests to source code.
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.
14from typing
import Any, Callable, Dict, List, Set, TextIO, Tuple, Type, Optional, cast
15from ..accelerator
import AcceleratorConnection, Context, Instance
16from ..esiCppAccel
import AppID, ModuleInfo
23from pathlib
import Path
25from .indented_writer
import IndentedWriter
26from .ports
import CppPortGroup, CppPortKind, cpp_port_kind
30 """Base class for all generators."""
32 language: Optional[str] =
None
34 def __init__(self, conn: AcceleratorConnection) ->
None:
37 def generate(self, output_dir: Path, system_name: str) ->
None:
38 raise NotImplementedError(
"Generator.generate() must be overridden")
42 """Generate C++ headers from an ESI manifest."""
46 def __init__(self, conn: AcceleratorConnection) ->
None:
53 """Get the C++ code for a constant in a module."""
54 const_strs: List[str] = [
55 f
"static constexpr {self.type_emitter.type_identifier(const.type)} "
56 f
"{name} = 0x{const.value:x};"
57 for name, const
in module_info.constants.items()
59 return "\n".join(const_strs)
67 """Return a C++-safe identifier from an AppID name."""
70 result.append(ch
if (ch.isalnum()
or ch ==
"_")
else "_")
75 return "".join(result)
78 """Walk the live hierarchy and return {module_name: first Instance}."""
79 accel = self.
_conn.build_accelerator()
80 result: Dict[str, Instance] = {}
81 queue = list(accel.children.values())
84 info = inst.cpp_hwmodule.info
87 if name
is not None and name
not in result:
89 queue.extend(inst.children.values())
94 """Return (alias_name, type_id) pairs to emit as `using` declarations at
95 module-class scope for the typed-port template parameters."""
96 if kind.alias_kind ==
"func":
100 arg = self.
type_emitter.type_identifier(func_port.arg_window_type
or
102 res = self.
type_emitter.type_identifier(func_port.result_window_type
or
103 func_port.result_type)
105 (f
"{alias_prefix}Args", arg),
106 (f
"{alias_prefix}Result", res),
108 if kind.alias_kind ==
"chan":
112 data = self.
type_emitter.type_identifier(chan_port.data_window_type
or
114 return [(f
"{alias_prefix}Data", data)]
119 """Return `esi::AppID(...)` expression for an AppID."""
123 return f
'esi::AppID("{name}")'
124 return f
'esi::AppID("{name}", {idx})'
127 appid: AppID) -> CppPortGroup:
128 """Build a CppPortGroup for a single scalar (non-indexed) port."""
129 kind = cpp_port_kind(port)
131 alias_prefix = member_name
if aliases
else None
132 member_type = kind.member_type(alias_prefix=alias_prefix)
133 is_ref = member_type.endswith(
" &")
134 param_name = f
"{member_name}{kind.param_suffix}"
137 member_decl = f
"{member_type}{member_name};"
139 member_decl = f
"{member_type} {member_name};"
143 post = f
"connected->{member_name}.connect();"
146 member_decl=member_decl,
147 ctor_params=[f
"{kind.param_type} {param_name}"],
148 init_entry=f
"{member_name}({param_name})",
149 find_code=kind.scalar_find_code(param_name, self.
_appid_expr(appid)),
150 make_unique_args=[param_name],
152 using_aliases=aliases,
156 self, member_name: str, appid_name: str,
158 """Build a CppPortGroup for a same-name, same-type indexed port array."""
160 first_port = port_list[0][1]
161 kind = cpp_port_kind(first_port)
163 alias_prefix = member_name
if aliases
else None
164 elem_type = kind.indexed_elem_type(alias_prefix=alias_prefix)
165 indexed_type = f
"esi::IndexedPorts<{elem_type}>"
166 map_var = f
"{member_name}_backing"
167 map_type = f
"std::map<int, {elem_type}>"
168 indexed_var = f
"{member_name}_map"
176 f
"{map_type} {map_var};",
177 f
"for (uint32_t idx : esi::findPortIndices(rawModule, "
178 f
"\"{appid_name}\")) {{",
180 appid_expr = f
'esi::AppID("{appid_name}", idx)'
181 find_parts.append(kind.indexed_find_code(map_var, appid_expr))
182 find_parts.append(
"}")
183 find_parts.append(f
"{indexed_type} {indexed_var}(std::move({map_var}));")
188 post = (f
"for (auto &[idx, port] : connected->{member_name})\n"
192 member_decl=f
"{indexed_type} {member_name};",
193 ctor_params=[f
"{indexed_type} {indexed_var}"],
194 init_entry=f
"{member_name}(std::move({indexed_var}))",
195 find_code=
"\n".join(find_parts),
196 make_unique_args=[f
"std::move({indexed_var})"],
198 using_aliases=aliases,
202 self, member_name: str, appid_name: str,
204 """Build a CppPortGroup for a same-name, mixed-type indexed port group."""
205 struct_name = f
"{member_name}_ports"
206 sub_member_decls: List[str] = []
207 ctor_params: List[str] = []
208 init_args: List[str] = []
209 find_parts: List[str] = []
210 make_args: List[str] = []
211 post_parts: List[str] = []
212 using_aliases: List[Tuple[str, str]] = []
214 for appid, port
in port_list:
215 kind = cpp_port_kind(port)
216 idx = appid.idx
if appid.idx
is not None else 0
218 sub_alias_prefix = f
"{member_name}_{idx}"
220 using_aliases.extend(sub_aliases)
221 alias_prefix = sub_alias_prefix
if sub_aliases
else None
222 member_type = kind.member_type(alias_prefix=alias_prefix)
223 is_ref = member_type.endswith(
" &")
225 sub_member_decls.append(f
"{member_type}{sub_name};")
227 sub_member_decls.append(f
"{member_type} {sub_name};")
229 param_name = f
"{member_name}_{idx}{kind.param_suffix}"
230 ctor_params.append(f
"{kind.param_type} {param_name}")
231 init_args.append(param_name)
234 find_local = f
"{param_name}{kind.param_suffix}"
236 kind.scalar_find_code(find_local, self.
_appid_expr(appid)))
237 make_args.append(find_local)
239 post_parts.append(f
"connected->{member_name}.{sub_name}.connect();")
241 struct_decl = (f
"struct {struct_name} {{\n" +
242 "".join(f
" {d}\n" for d
in sub_member_decls) +
" };")
243 init_entry = f
"{member_name}{{{', '.join(init_args)}}}"
246 struct_decls=[struct_decl],
247 member_decl=f
"{struct_name} {member_name};",
248 ctor_params=ctor_params,
249 init_entry=init_entry,
250 find_code=
"\n".join(find_parts),
251 make_unique_args=make_args,
252 post_connect=
"\n".join(post_parts),
253 using_aliases=using_aliases,
258 """Group `ports` (AppID → BundlePort) into CppPortGroup list."""
260 groups_by_name: Dict[str, list] = {}
261 for appid, port
in ports.items():
263 if n
not in groups_by_name:
264 groups_by_name[n] = []
265 groups_by_name[n].
append((appid, port))
267 result: List[CppPortGroup] = []
268 for appid_name, port_list
in groups_by_name.items():
271 port_list.sort(key=
lambda x: x[0].idx
if x[0].idx
is not None else -1)
273 if len(port_list) == 1:
274 appid, port = port_list[0]
275 if appid.idx
is None:
283 all_indexed = all(a.idx
is not None for a, _
in port_list)
291 all_same_type = len({type(p)
for _, p
in port_list}) == 1
302 module_info: ModuleInfo,
303 port_groups: List[CppPortGroup], out: TextIO) ->
None:
304 """Emit the full module class to `out`."""
305 out.write(f
"/// Generated header for {system_name} module {name}.\n"
307 '#include "types.h"\n'
308 '#include "esi/TypedPorts.h"\n'
312 "#include <optional>\n"
313 "#include <string>\n"
315 f
"namespace {system_name} {{\n"
319 metadata_lines: List[str] = []
320 summary = getattr(module_info,
"summary",
None)
322 for line
in summary.splitlines():
323 metadata_lines.append(line)
324 for label, attr
in ((
"Version",
"version"), (
"Repository",
"repo"),
325 (
"Commit",
"commit_hash")):
326 val = getattr(module_info, attr,
None)
328 metadata_lines.append(f
"{label}: {val}")
331 for line
in metadata_lines:
332 out.write(f
"/// {line}\n" if line
else "///\n")
335 out.write(f
"class {name} {{\n"
340 out.write(
" // Module constants.\n")
341 out.write(f
" {consts}\n\n")
345 aliases = [a
for grp
in port_groups
for a
in grp.using_aliases]
347 for alias_name, alias_type
in aliases:
348 out.write(f
" using {alias_name} = {alias_type};\n")
353 " /// Holds the resolved, typed ports for this module instance.\n"
354 " /// Returned by `connect()`.\n"
355 " class Connected {\n public:\n")
358 for grp
in port_groups:
359 for decl
in grp.struct_decls:
360 out.write(f
" {decl}\n")
361 if any(grp.struct_decls
for grp
in port_groups):
365 for grp
in port_groups:
366 out.write(f
" {grp.member_decl}\n")
370 all_params = [p
for grp
in port_groups
for p
in grp.ctor_params]
371 out.write(
" Connected(\n")
372 for i, param
in enumerate(all_params):
373 comma =
"," if i < len(all_params) - 1
else ""
374 out.write(f
" {param}{comma}\n")
376 inits = [grp.init_entry
for grp
in port_groups]
377 out.write(
",\n ".join(inits))
378 out.write(
" {}\n };\n\n")
382 f
" {name}(esi::HWModule *rawModule) : rawModule(rawModule) {{}}\n\n")
388 " /// The connected module's name as reported by the manifest, or\n"
389 " /// std::nullopt if the module has no metadata.\n"
390 " std::optional<std::string> name() const {\n"
391 " auto info = rawModule->getInfo();\n"
392 " return info ? info->name : std::nullopt;\n"
394 " /// The connected module's summary string, if any.\n"
395 " std::optional<std::string> summary() const {\n"
396 " auto info = rawModule->getInfo();\n"
397 " return info ? info->summary : std::nullopt;\n"
399 " /// The connected module's version string, if any.\n"
400 " std::optional<std::string> version() const {\n"
401 " auto info = rawModule->getInfo();\n"
402 " return info ? info->version : std::nullopt;\n"
404 " /// The connected module's source repository, if any.\n"
405 " std::optional<std::string> repo() const {\n"
406 " auto info = rawModule->getInfo();\n"
407 " return info ? info->repo : std::nullopt;\n"
409 " /// The connected module's source commit hash, if any.\n"
410 " std::optional<std::string> commitHash() const {\n"
411 " auto info = rawModule->getInfo();\n"
412 " return info ? info->commitHash : std::nullopt;\n"
414 " /// Designer-specified constants for the connected module.\n"
415 " /// Returns an empty map if the module has no metadata.\n"
416 " std::map<std::string, esi::Constant> constants() const {\n"
417 " auto info = rawModule->getInfo();\n"
418 " return info ? info->constants\n"
419 " : std::map<std::string, esi::Constant>{};\n"
421 " /// Free-form designer-supplied metadata for the connected module.\n"
422 " /// Returns an empty map if the module has no metadata.\n"
423 " std::map<std::string, std::any> extra() const {\n"
424 " auto info = rawModule->getInfo();\n"
425 " return info ? info->extra : std::map<std::string, std::any>{};\n"
429 out.write(
" std::unique_ptr<Connected> connect() {\n")
432 for grp
in port_groups:
434 for line
in grp.find_code.splitlines():
435 out.write(f
" {line}\n")
439 all_args = [a
for grp
in port_groups
for a
in grp.make_unique_args]
440 out.write(
" auto connected = std::make_unique<Connected>(\n")
441 for i, arg
in enumerate(all_args):
442 comma =
"," if i < len(all_args) - 1
else ""
443 out.write(f
" {arg}{comma}\n")
447 for grp
in port_groups:
449 for line
in grp.post_connect.splitlines():
450 out.write(f
" {line}\n")
452 out.write(
" return connected;\n }\n\n")
454 out.write(
"private:\n esi::HWModule *rawModule;\n};\n\n")
455 out.write(f
"}} // namespace {system_name}\n")
458 """Write the C++ header. One for each module in the manifest."""
461 for module_info
in self.
manifest.module_infos:
462 if module_info.name
is None:
464 name = module_info.name
465 instance = module_instances.get(name)
467 if instance
is not None:
471 except (NotImplementedError, ValueError)
as e:
472 hdr_file = output_dir / f
"{name}.h"
473 with open(hdr_file,
"w", encoding=
"utf-8")
as hdr:
474 hdr.write(f
"// Skipped: {e}\n")
477 hdr_file = output_dir / f
"{name}.h"
478 with open(hdr_file,
"w", encoding=
"utf-8")
as hdr:
482 def generate(self, output_dir: Path, system_name: str) ->
None:
488 """Plan C++ type naming and ordering from an ESI manifest."""
491 """Initialize the generator with the manifest and target namespace."""
495 self.used_names: Dict[str, bool] = {}
497 self.alias_base_names: Set[str] = set()
507 """Name the types and prepare for emission by registering all reachable
508 types and assigning."""
509 visited: Set[str] = set()
524 """Create a C++-safe identifier from the manifest-provided name."""
525 name = name.replace(
"::",
"_")
526 if name.startswith(
"@"):
530 if ch.isalnum()
or ch ==
"_":
533 sanitized.append(
"_")
537 sanitized.insert(0,
"_")
538 return "".join(sanitized)
541 """Reserve a globally unique identifier using the sanitized base name."""
543 if is_alias
and base
in self.alias_base_names:
545 f
"Warning: duplicate alias name '{base}' detected; disambiguating.\n")
547 self.alias_base_names.add(base)
550 while name
in self.used_names:
551 name = f
"{base}_{idx}"
553 self.used_names[name] = is_alias
557 """Derive a deterministic name for anonymous structs from their fields."""
559 for field_name, field_type
in struct_type.fields:
560 parts.append(field_name)
565 """Derive a deterministic name for anonymous unions from their fields."""
567 for field_name, field_type
in union_type.fields:
568 parts.append(field_name)
573 """Derive a deterministic name for generated window helpers.
575 Two distinct windows can wrap the same `into` struct (e.g. serial and
576 parallel encodings of the same payload), so the helper name must be
577 derived from BOTH the inner type's name and the window's own name/id.
580 into_name = self.type_id_map.get(into_type)
581 window_part = (window_type.name
584 base = f
"{into_name}_{window_part}"
585 elif window_type.name:
588 base = f
"_window_{window_part}"
593 wrapped = wrapped.inner_type
606 for field_name, field_type
in into_type.fields:
608 list_fields.append(field_name)
609 if len(list_fields) != 1:
612 list_field_name = list_fields[0]
617 for frame
in current_type.frames:
618 for field
in frame.fields:
619 if field.name != list_field_name:
621 if field.bulk_count_width > 0:
622 if header_field
is not None:
625 elif field.num_items > 0:
626 if data_field
is not None:
629 return (header_field
is not None and data_field
is not None and
630 data_field.num_items == 1)
633 """Return child types in a stable order for traversal."""
635 return [t.inner_type]
if t.inner_type
is not None else []
637 return [channel.type
for channel
in t.channels]
641 return [field_type
for _, field_type
in t.fields]
643 return [field_type
for _, field_type
in t.fields]
645 return [t.element_type]
649 return [t.element_type]
654 """Traverse types with alphabetical child ordering in post-order."""
656 raise TypeError(f
"Expected ESIType, got {type(t)}")
662 for child
in children:
667 """Scan for aliases and reserve their names (recursive)."""
670 def visit(alias_type: types.ESIType) ->
None:
673 if alias_type
not in self.type_id_map:
674 alias_name = self.
_reserve_name(alias_type.name, is_alias=
True)
675 self.type_id_map[alias_type] = alias_name
680 """Scan for structs/unions needing auto-names and reserve them."""
683 def visit(current_type: types.ESIType) ->
None:
684 if current_type
in self.type_id_map:
694 """Scan for supported window types and reserve helper names."""
696 def visit(current_type: types.ESIType) ->
None:
700 if current_type
in self.type_id_map:
708 """Collect types that require top-level declarations for a given type."""
712 def visit(current: types.ESIType) ->
None:
734 """Collect only the declarations referenced by a generated window helper."""
740 for _, field_type
in into_type.fields:
749 """Return True if `esi_type` is or transitively contains a WindowType.
751 Structs (and aliases/unions that reference them) which embed a window
752 cannot be emitted as C++ packed structs because the C++ window helper
753 is a variable-size multi-frame container. This helper is used to
754 exclude such types from the emission list entirely.
768 """Return True if `esi_type` is or transitively contains a type with
769 no bounded bit width (e.g. `!esi.any`, or a list that the window
772 A struct can't be emitted as a fixed-size raw-bytes buffer if any
773 field's width is unbounded — there's no `std::array<uint8_t, N>` size
774 that would match the wire layout — so the planner excludes such
775 structs (and aliases/unions/arrays that reach one) from the emission
776 list, the same way `_contains_window` does for nested windows.
780 if unwrapped.bit_width < 0:
793 """Collect and order types for deterministic emission."""
795 for esi_type
in self.type_id_map.keys():
805 for esi_type
in self.type_id_map.keys():
806 if isinstance(esi_type,
808 skip_set.add(esi_type)
809 self.skipped_types.
append(
811 "into-type of a windowed helper; subsumed by the helper class"))
814 inner = esi_type.inner_type
816 inner)
in window_into_types:
817 skip_set.add(esi_type)
818 self.skipped_types.
append(
819 (esi_type,
"alias of a windowed helper's into-type"))
826 skip_set.add(esi_type)
827 self.skipped_types.
append((esi_type,
"contains a windowed sub-type"))
836 skip_set.add(esi_type)
837 self.skipped_types.
append(
838 (esi_type,
"contains an unbounded sub-type (e.g. !esi.any)"))
842 for esi_type
in self.type_id_map.keys():
843 if esi_type
in skip_set:
845 if (isinstance(esi_type,
848 emit_types.append(esi_type)
851 name_to_type = {self.type_id_map[t]: t
for t
in emit_types}
852 sorted_names = sorted(name_to_type.keys(),
854 (0
if self.used_names.get(name,
False)
else 1, name))
865 def visit(current: types.ESIType) ->
None:
867 if current
in visited:
869 if current
in visiting:
872 visiting.add(current)
876 inner = current.inner_type
877 if inner
is not None:
880 for _, field_type
in current.fields:
885 for dep
in sorted(deps, key=
lambda dep: self.type_id_map[dep]):
890 visiting.remove(current)
892 ordered.append(current)
894 for name
in sorted_names:
895 visit(name_to_type[name])
897 return ordered, has_cycle
901 """Emit C++ headers from precomputed type ordering."""
903 def __init__(self, planner: CppTypePlanner) ->
None:
910 """Get the C++ type string for an ESI type."""
914 """Escape a Python string for use as a C++ string literal."""
915 escaped = value.replace(
"\\",
"\\\\").replace(
'"',
'\\"')
916 return f
'"{escaped}"'
919 """Get the textual code for the C++ type used to represent an integer
920 field's value at the API boundary.
922 Integers up to 64 bits map to the native `int{N}_t` / `uint{N}_t`
923 (or `bool` for a single bit) storage types so common scalars stay
924 zero-overhead, behave identically to plain C ints, and stay valid as
925 template parameters for the existing `TypedReadPort<T>` /
926 `TypedWritePort<T>` / `TypedFunction<...>` machinery.
928 Wider integers (signed > 64 bits, unsigned > 64 bits, or any
929 `BitsType` > 64 bits) fall back to the non-owning view classes from
930 `esi/Values.h`: `esi::BitVector` for `BitsType`, `esi::IntView` for
931 signed integers, `esi::UIntView` for unsigned. All three are
932 non-owning views over the parent struct's bytes, so generated
933 getters are zero-allocation; see the lifetime note on `BitVector`
934 and at the top of the generated header.
938 if type.bit_width > 64:
940 return "esi::BitVector"
942 return "esi::UIntView"
943 return "esi::IntView"
949 """Get the textual code for a native byte-addressable integer storage
950 type. Only valid for widths 1..64; wider integers are handled by
951 `_get_bitvector_str` via the `esi::IntView` / `esi::UIntView` view
959 elif bit_width <= 16:
961 elif bit_width <= 32:
963 elif bit_width <= 64:
966 raise ValueError(f
"Unsupported native integer width: {bit_width}")
969 return f
"uint{storage_width}_t"
970 return f
"int{storage_width}_t"
973 """True if the C++ representation of this integer field is one of
974 the `esi::{BitVector,IntView,UIntView}` view classes from
975 `esi/Values.h` rather than a native integer.
980 return wrapped.bit_width > 64
983 self, array_type: types.ArrayType) -> Tuple[str, List[int]]:
984 """Return the base C++ type and outer-to-inner dimensions of a nested array."""
988 dims.append(inner.size)
989 inner = inner.element_type
991 return base_cpp, dims
994 """Return the equivalent nested `std::array<...>` type for an array.
996 `std::array<T, N>` is layout-compatible in practice with `T[N]` on every
997 major implementation (and identical under `#pragma pack(1)`), so the
998 generator uses it everywhere a fixed-size array would appear. This keeps
999 field/value/ctor types storable in `std::vector` and assignable with `=`.
1003 for size
in reversed(dims):
1004 result = f
"std::array<{result}, {size}>"
1008 """Resolve an ESI type to its C++ identifier."""
1011 if isinstance(wrapped,
1018 if wrapped.bit_width == 0:
1026 raise ValueError(
"List types require a generated window wrapper")
1034 if wrapped.bit_width == 0:
1040 if wrapped.bit_width == 0:
1045 raise NotImplementedError(
1046 f
"Type '{wrapped}' not supported for C++ generation")
1049 """Strip alias wrappers to reach the underlying type."""
1051 wrapped = wrapped.inner_type
1056 """Emit a constructor parameter for generated window helpers.
1058 Small scalar header fields are cheaper to pass by value than by reference.
1059 Larger aggregates stay as const references.
1065 return f
"const {field_cpp} &{field_name}"
1066 return f
"{field_cpp} {field_name}"
1067 return f
"const {field_cpp} &{field_name}"
1070 """Compute the byte width of a field type, rounding up to full bytes."""
1071 return (field_type.bit_width + 7) // 8
1074 """Return the bounded byte width of `esi_type`, or `None` if it has no
1075 well-defined static size (e.g. unbounded `!esi.any` or recursive types).
1078 bit_width = esi_type.bit_width
1081 if bit_width
is None or bit_width < 0:
1083 return (bit_width + 7) // 8
1086 expected_bytes: Optional[int]) ->
None:
1087 """Emit a `static_assert` that pins the C++ `sizeof` of a packed type to
1088 the byte width derived from the manifest.
1090 `std::array` and bit-field layout are technically implementation-defined,
1091 so this assertion is the safety net that catches a toolchain that lays
1092 them out differently from the wire format. Skipped silently for types
1093 without a bounded static size.
1095 if expected_bytes
is None:
1097 w.line(f
"static_assert(sizeof({type_name}) == {expected_bytes},")
1098 w.line(f
' "{type_name}: packed layout does not match '
1099 f
'manifest size");')
1102 """Extract the metadata needed to emit a bulk list window wrapper."""
1105 raise ValueError(
"window codegen currently requires a struct into-type")
1107 field_map = {name: field_type
for name, field_type
in into_type.fields}
1110 for name, field_type
in into_type.fields
1113 if len(list_fields) != 1:
1114 raise ValueError(
"window codegen currently supports exactly one list")
1116 list_field_name, list_type = list_fields[0]
1123 for frame
in window_type.frames:
1124 for field
in frame.fields:
1125 if field.name != list_field_name:
1127 if field.bulk_count_width > 0:
1128 header_frame = frame
1129 header_field = field
1130 elif field.num_items > 0:
1134 if header_frame
is None or header_field
is None:
1135 raise ValueError(
"window codegen requires a bulk-count header frame")
1136 if data_frame
is None or data_field
is None:
1137 raise ValueError(
"window codegen requires a data frame for the list")
1138 if data_field.num_items != 1:
1139 raise ValueError(
"window codegen currently supports numItems == 1")
1141 ctor_params = [(name, field_type)
1142 for name, field_type
in into_type.fields
1143 if name != list_field_name]
1150 count_field_name = f
"{list_field_name}_count"
1151 count_width = header_field.bulk_count_width
1153 for field
in header_frame.fields:
1154 if field.name == list_field_name:
1155 header_fields.append((count_field_name,
None))
1156 header_bits += count_width
1158 field_type = field_map[field.name]
1159 header_fields.append((field.name, field_type))
1160 header_bits += field_type.bit_width
1164 for field
in data_frame.fields:
1165 if field.name == list_field_name:
1166 data_fields.append((list_field_name, list_type.element_type))
1167 data_bits += list_type.element_type.bit_width
1169 field_type = field_map[field.name]
1170 data_fields.append((field.name, field_type))
1171 data_bits += field_type.bit_width
1176 frame_bits = max(header_bits, data_bits)
1177 frame_bytes = (frame_bits + 7) // 8
1180 "ctor_params": ctor_params,
1181 "count_cpp": count_cpp,
1182 "count_field_name": count_field_name,
1183 "count_width": count_width,
1184 "data_fields": data_fields,
1185 "element_cpp": self.
_cpp_type(list_type.element_type),
1186 "frame_bits": frame_bits,
1187 "frame_bytes": frame_bytes,
1188 "header_fields": header_fields,
1189 "list_field_name": list_field_name,
1194 self, struct_type: types.StructType) -> Tuple[Dict[str, int], int]:
1195 """Return `(bit_offset_by_name, total_bits)` for non-void fields.
1197 Fields are laid out LSB-first in wire order, which is reversed manifest
1198 order when `cpp_type.reverse` is set. Caller already knows each field's
1199 type/width from `struct_type.fields`; we only need the per-name offset
1200 and the resulting total bit width to size the storage array.
1202 declared = struct_type.fields
1203 if struct_type.cpp_type.reverse:
1204 declared = list(reversed(declared))
1205 bit_offsets: Dict[str, int] = {}
1207 for field_name, field_type
in declared:
1208 if self.
_cpp_type(field_type) ==
"void":
1210 bit_offsets[field_name] = bit_offset
1211 bit_offset += field_type.bit_width
1212 return bit_offsets, bit_offset
1215 """True if the field is a signed integer (only `IntType`, not `UIntType`
1222 off_expr: str, width: int) ->
None:
1223 """Emit a loop writing the low `width` bits of the view `src` into
1224 `raw_member`, with bit `b` landing at wire bit `off_expr + b`. Shared by
1225 the scalar view-class field setter and the per-element view-array setter.
1227 w.line(f
"const std::size_t n = "
1228 f
"std::min<std::size_t>({src}.width(), {width});")
1229 with w.block(f
"for (std::size_t b = 0; b < {width}; ++b)"):
1230 w.line(f
"const std::size_t g = {off_expr} + b;")
1231 w.line(f
"const bool val = (b < n) && {src}.getBit(b);")
1234 w.line(f
"{raw_member}[g / 8] |= "
1235 f
"static_cast<uint8_t>(uint8_t{{1}} << (g % 8));")
1238 w.line(f
"{raw_member}[g / 8] &= "
1239 f
"static_cast<uint8_t>(~(uint8_t{{1}} << (g % 8)));")
1242 self_type: str, field_name: str,
1244 bit_width: int) ->
None:
1245 """Emit a getter/setter pair that reads/writes `field_name` out of the
1246 raw bytes member `raw_member` at `bit_offset` / `bit_width`.
1248 Setters share the field's name (no `set_` prefix) and return
1249 `self_type &` so the caller can chain calls (e.g.
1250 `Foo{}.a(1).b(2).inner(x)`).
1252 For integer fields the emitter picks between three inline strategies:
1254 * Native ints (<= 64 bits). Delegate to the compile-time
1255 `esi::detail::{read,write}{Un,}signedBits` helpers from
1256 `esi/BitAccess.h`, which loop over the constituent bits at the
1257 field's constant offset/width. The non-type template parameters
1258 let the optimiser fully unroll the loop, so a byte-aligned
1259 standard-width field still collapses to a single load/store on
1260 -O1+ -- no special-cased `memcpy` / per-byte fast path required.
1261 * `bool` (a single bit). Same helpers, specialised to read/write one
1262 bit and hand back a `bool`.
1263 * View-class fields. Triggered when `_is_value_class_type` is true --
1264 currently widths above 64 bits, for any of `BitsType`, signed, or
1265 unsigned. Returns a non-owning
1266 `esi::BitVector` / `esi::IntView` / `esi::UIntView` view *into*
1267 the parent struct's `_bytes` -- zero allocation, no copy. The
1268 setter accepts any `esi::BitVector` (so views and owning
1269 subclasses both work) and writes back bit-by-bit. The returned
1270 view dangles when the parent buffer dies; see the lifetime warning
1271 at the top of the generated header.
1274 if field_cpp ==
"void":
1284 byte_offset = bit_offset // 8
1285 sub_bit_index = bit_offset % 8
1290 span_bytes = (sub_bit_index + bit_width + 7) // 8
1291 with w.block(f
"{field_cpp} {field_name}() const"):
1292 w.line(f
"return {field_cpp}(")
1293 w.line(f
" std::span<const uint8_t>("
1294 f
"{raw_member}.data() + {byte_offset}, {span_bytes}),")
1295 w.line(f
" static_cast<std::size_t>({bit_width}),")
1296 w.line(f
" static_cast<uint8_t>({sub_bit_index}));")
1297 with w.block(f
"{self_type} &{field_name}(const {field_cpp} &v)"):
1301 w.line(
"return *this;")
1306 if bit_width == 1
and field_cpp ==
"bool":
1307 with w.block(f
"bool {field_name}() const"):
1308 w.line(f
"return esi::detail::readUnsignedBits<uint8_t, "
1309 f
"{bit_offset}, 1>({raw_member}.data()) != 0;")
1310 with w.block(f
"{self_type} &{field_name}(bool v)"):
1311 w.line(f
"esi::detail::writeUnsignedBits<uint8_t, "
1312 f
"{bit_offset}, 1>({raw_member}.data(), "
1313 f
"static_cast<uint8_t>(v) & uint8_t{{1}});")
1314 w.line(
"return *this;")
1318 kind =
"Signed" if signed
else "Unsigned"
1329 with w.block(f
"{field_cpp} {field_name}() const"):
1330 w.line(f
"return esi::detail::read{kind}Bits<{field_cpp}, "
1331 f
"{bit_offset}, {bit_width}>({raw_member}.data());")
1332 with w.block(f
"{self_type} &{field_name}({field_cpp} v)"):
1333 w.line(f
"esi::detail::write{kind}Bits<{field_cpp}, "
1334 f
"{bit_offset}, {bit_width}>({raw_member}.data(), v);")
1335 w.line(
"return *this;")
1347 assert bit_width >= 0, (
1348 f
"field '{field_name}': unbounded aggregate field reached the "
1349 f
"emitter; the planner should have excluded the parent struct "
1350 f
"via `_contains_unbounded`")
1363 byte_aligned = (bit_offset % 8 == 0
and bit_width % 8 == 0)
1364 byte_offset = bit_offset // 8
1365 byte_width = (bit_width + 7) // 8
1367 if not is_view_array:
1372 with w.block(f
"{field_cpp} {field_name}() const"):
1373 w.line(f
"{field_cpp} out{{}};")
1374 w.line(f
"for (std::size_t i = 0; i < {byte_width}; ++i)")
1376 w.line(f
"reinterpret_cast<uint8_t *>(&out)[i] = "
1377 f
"{raw_member}[{byte_offset} + i];")
1378 w.line(
"return out;")
1379 with w.block(f
"{self_type} &{field_name}(const {field_cpp} &v)"):
1380 w.line(f
"for (std::size_t i = 0; i < {byte_width}; ++i)")
1382 w.line(f
"{raw_member}[{byte_offset} + i] = "
1383 f
"reinterpret_cast<const uint8_t *>(&v)[i];")
1384 w.line(
"return *this;")
1389 with w.block(f
"{field_cpp} {field_name}() const"):
1390 w.line(f
"{field_cpp} out{{}};")
1391 w.line(f
"esi::detail::copyBitsIn<{bit_offset}, "
1392 f
"{bit_width}>({raw_member}.data(), "
1393 f
"reinterpret_cast<uint8_t *>(&out));")
1394 w.line(
"return out;")
1395 with w.block(f
"{self_type} &{field_name}(const {field_cpp} &v)"):
1396 w.line(f
"esi::detail::copyBitsOut<{bit_offset}, "
1397 f
"{bit_width}>({raw_member}.data(), "
1398 f
"reinterpret_cast<const uint8_t *>(&v));")
1399 w.line(
"return *this;")
1422 elem_type = wrapped.element_type
1424 elem_width = elem_type.bit_width
1425 elem_count = wrapped.size
1426 with w.block(f
"{elem_cpp} {field_name}(std::size_t i) const"):
1427 w.line(f
"const std::size_t bit_off = {bit_offset} + i * {elem_width};")
1428 w.line(f
"return {elem_cpp}(")
1429 w.line(f
" std::span<const uint8_t>("
1430 f
"{raw_member}.data() + bit_off / 8,")
1432 f
"(bit_off % 8 + {elem_width} + 7) / 8),")
1433 w.line(f
" static_cast<std::size_t>({elem_width}),")
1434 w.line(f
" static_cast<uint8_t>(bit_off % 8));")
1435 with w.block(f
"{self_type} &{field_name}(std::size_t i, "
1436 f
"const {elem_cpp} &v)"):
1437 w.line(f
"const std::size_t bit_off = {bit_offset} + i * {elem_width};")
1439 w.line(
"return *this;")
1443 with w.block(f
"auto {field_name}() const"):
1444 w.line(f
"return std::views::iota("
1445 f
"std::size_t{{0}}, std::size_t{{{elem_count}}}) |")
1446 w.line(
" std::views::transform([this](std::size_t i) {")
1447 w.line(f
" return this->{field_name}(i);")
1451 with w.block(f
"{self_type} &{field_name}("
1452 f
"const std::array<{elem_cpp}, {elem_count}> &v)"):
1453 w.line(f
"for (std::size_t i = 0; i < {elem_count}; ++i)")
1455 w.line(f
"{field_name}(i, v[i]);")
1456 w.line(
"return *this;")
1459 self_type: str, field_name: str,
1461 bit_offset: int) ->
None:
1462 """Emit the accessor set for a fixed-size array field.
1464 Four accessors are emitted, matching the existing array API:
1466 * `Elem field(std::size_t i)` / `Self &field(std::size_t i, const Elem &)`
1467 -- per-element get/set, where `Elem` is the (possibly itself an
1468 array) element type.
1469 * `Whole field()` / `Self &field(const Whole &)` -- whole-array
1470 get/set, expressed in terms of the per-element accessors.
1472 The per-element get/set bodies are produced by `_emit_unpack` /
1473 `_emit_pack`, which recurse to the scalar leaves and place each leaf at
1474 its true wire offset `bit_offset + i * elemWidth (+ ...)`. This is what
1475 lets sub-byte, odd-width, sub-byte-aggregate, and *arbitrarily nested*
1476 array elements round-trip even when the C++ element layout does not
1477 match the wire layout.
1479 elem_type = array_type.element_type
1481 elem_width = elem_type.bit_width
1482 elem_count = array_type.size
1484 elem_off = f
"{bit_offset} + i * {elem_width}"
1486 with w.block(f
"{elem_cpp} {field_name}(std::size_t i) const"):
1487 w.line(f
"{elem_cpp} out{{}};")
1488 self.
_emit_unpack(w, raw_member,
"out", elem_off, elem_type, 1)
1489 w.line(
"return out;")
1491 with w.block(f
"{self_type} &{field_name}(std::size_t i, "
1492 f
"const {elem_cpp} &v)"):
1493 self.
_emit_pack(w, raw_member,
"v", elem_off, elem_type, 1)
1494 w.line(
"return *this;")
1496 with w.block(f
"{whole_cpp} {field_name}() const"):
1497 w.line(f
"{whole_cpp} out{{}};")
1498 w.line(f
"for (std::size_t i = 0; i < {elem_count}; ++i)")
1500 w.line(f
"out[i] = {field_name}(i);")
1501 w.line(
"return out;")
1503 with w.block(f
"{self_type} &{field_name}(const {whole_cpp} &v)"):
1504 w.line(f
"for (std::size_t i = 0; i < {elem_count}; ++i)")
1506 w.line(f
"{field_name}(i, v[i]);")
1507 w.line(
"return *this;")
1509 def _emit_unpack(self, w: IndentedWriter, raw_member: str, dst: str, off: str,
1511 """Emit statements that read a value of `esi_type` out of `raw_member`
1512 at wire bit offset `off` (a C++ `size_t` expression) into the
1513 already-declared, zero-initialised C++ lvalue `dst`.
1515 Recurses through arrays (one loop per dimension) down to scalar / struct
1516 / union leaves, which are read with the runtime-offset bit helpers.
1521 ew = wrapped.element_type.bit_width
1522 with w.block(f
"for (std::size_t {idx} = 0; {idx} < "
1523 f
"{wrapped.size}; ++{idx})"):
1525 f
"{off} + {idx} * {ew}", wrapped.element_type,
1532 w.line(f
"esi::detail::copyBitsInDyn({raw_member}.data(), {off},")
1533 w.line(f
" reinterpret_cast<uint8_t *>(&{dst}), {wrapped.bit_width});")
1537 raise NotImplementedError(
1538 "arrays of integers wider than 64 bits are not supported")
1541 w.line(f
"{dst} = esi::detail::readUnsignedBitsDyn<uint8_t>("
1542 f
"{raw_member}.data(), {off}, 1) != 0;")
1544 w.line(f
"{dst} = esi::detail::readSignedBitsDyn<{cpp}>("
1545 f
"{raw_member}.data(), {off}, {wrapped.bit_width});")
1547 w.line(f
"{dst} = esi::detail::readUnsignedBitsDyn<{cpp}>("
1548 f
"{raw_member}.data(), {off}, {wrapped.bit_width});")
1550 raise NotImplementedError(
1551 f
"unsupported array element type '{wrapped}' for C++ generation")
1553 def _emit_pack(self, w: IndentedWriter, raw_member: str, src: str, off: str,
1555 """Inverse of `_emit_unpack`: write the C++ value `src` of `esi_type`
1556 into `raw_member` at wire bit offset `off`."""
1560 ew = wrapped.element_type.bit_width
1561 with w.block(f
"for (std::size_t {idx} = 0; {idx} < "
1562 f
"{wrapped.size}; ++{idx})"):
1563 self.
_emit_pack(w, raw_member, f
"{src}[{idx}]", f
"{off} + {idx} * {ew}",
1564 wrapped.element_type, depth + 1)
1567 w.line(f
"esi::detail::copyBitsOutDyn({raw_member}.data(), {off},")
1568 w.line(f
" reinterpret_cast<const uint8_t *>(&{src}), "
1569 f
"{wrapped.bit_width});")
1573 raise NotImplementedError(
1574 "arrays of integers wider than 64 bits are not supported")
1577 w.line(f
"esi::detail::writeUnsignedBitsDyn<uint8_t>("
1578 f
"{raw_member}.data(), "
1579 f
"static_cast<uint8_t>({src}) & uint8_t{{1}}, {off}, 1);")
1581 w.line(f
"esi::detail::writeSignedBitsDyn<{cpp}>("
1582 f
"{raw_member}.data(), {src}, {off}, {wrapped.bit_width});")
1584 w.line(f
"esi::detail::writeUnsignedBitsDyn<{cpp}>("
1585 f
"{raw_member}.data(), {src}, {off}, {wrapped.bit_width});")
1587 raise NotImplementedError(
1588 f
"unsupported array element type '{wrapped}' for C++ generation")
1591 """Return the C++ constructor-parameter type for a setter call."""
1596 return f
"const {field_cpp} &"
1600 """Emit a packed struct as a raw byte buffer with bit-precise accessors.
1602 The struct's storage is a single `std::array<uint8_t, N>` (where `N`
1603 is the byte width derived from the manifest) whose layout matches the
1604 on-wire bit-packing exactly. Per-field accessors (generated below)
1605 read/write the bits so the wire format does not depend on C++ bit-field
1606 allocation rules, which differ between the Itanium ABI (GCC/Clang) and MSVC.
1610 if struct_type.bit_width == 0:
1614 total_bytes = (total_bits + 7) // 8
1618 logical_fields = [(name, ftype)
1619 for name, ftype
in struct_type.fields
1627 if not logical_fields:
1630 with w.block(f
"struct {struct_name}", tail=
";"):
1635 w.access(
"private:")
1636 w.line(f
"std::array<uint8_t, {max(total_bytes, 1)}> _bytes{{}};")
1639 w.line(f
"{struct_name}() = default;")
1640 ctor_params =
", ".join(f
"{self._ctor_param_type(ftype)} {name}"
1641 for name, ftype
in logical_fields)
1642 with w.block(f
"{struct_name}({ctor_params})"):
1645 chain =
".".join(f
"{name}({name})" for name, _
in logical_fields)
1646 w.line(f
"this->{chain};")
1651 for name, ftype
in logical_fields:
1653 bit_offsets[name], ftype.bit_width)
1656 w.line(f
"static constexpr std::string_view _ESI_ID = "
1657 f
"{self._cpp_string_literal(struct_type.id)};")
1659 if expected_bytes
is not None and expected_bytes > 0:
1664 """Emit a union as a raw byte buffer with per-variant accessors.
1666 The union's storage is a single `std::array<uint8_t, N>` sized to the
1667 widest variant. Each variant starts at bit zero, matching the hw.union
1668 bitcast layout. Accessors use the exact member bit width and preserve
1669 bits outside that member.
1673 if union_type.bit_width == 0:
1678 with w.block(f
"struct {union_name}", tail=
";"):
1680 w.access(
"private:")
1681 w.line(f
"std::array<uint8_t, {union_bytes}> _bytes{{}};")
1684 w.line(f
"{union_name}() = default;")
1687 for field_name, field_type
in union_type.fields:
1689 if field_cpp ==
"void":
1692 field_type, 0, field_type.bit_width)
1695 w.line(f
"static constexpr std::string_view _ESI_ID = "
1696 f
"{self._cpp_string_literal(union_type.id)};")
1702 self, fields: List[Tuple[str, Optional[
types.ESIType]]], frame_bits: int,
1705 """Compute (name, type, bit_offset, bit_width) for each window frame field.
1707 Fields are listed in declared (MSB-first) struct order. The last field
1708 starts at bit zero, matching how CIRCT lowers the serial-window frame
1709 union. Earlier fields sit immediately above it with no inter-field
1710 padding, and any slack (`frame_bits - content_bits`) is at the MSB end.
1711 The returned `bit_offset` is the field's LSB position within the frame's
1712 little-endian `_bytes` array, ready to hand straight to
1713 `_emit_field_accessor`.
1715 The sentinel `(name, None)` count field is materialised as
1716 `count_type_synth` so it can flow through the standard
1717 `_emit_field_accessor` path.
1719 Raises `ValueError` if a field is unbounded (`bit_width < 0`) or if the
1720 accumulated content overflows `frame_bits` -- either would produce bogus
1721 offsets, so fail fast rather than emit silently-wrong accessors.
1725 for name, ftype
in reversed(fields):
1726 actual_type = count_type_synth
if ftype
is None else ftype
1727 bit_width = actual_type.bit_width
1730 f
"window frame field '{name}' has an unbounded width; window "
1731 f
"codegen requires every frame field to be fixed-width")
1732 if bit_offset + bit_width > frame_bits:
1734 f
"window frame field '{name}' overflows the {frame_bits}-bit frame "
1735 f
"(content is wider than the frame width)")
1736 layout.append((name, actual_type, bit_offset, bit_width))
1737 bit_offset += bit_width
1742 self, w: IndentedWriter, frame_name: str, frame_bytes: int,
1744 """Emit a window header/data frame as a raw-bytes struct with accessors.
1746 Nested inside the window helper class, it uses the same accessor pattern
1747 as top-level structs/unions: a private `_bytes` array plus per-field
1748 getter/setter pairs returning `frame_name &` to allow chaining.
1750 with w.block(f
"struct {frame_name}", tail=
";"):
1751 w.access(
"private:")
1752 w.line(f
"std::array<uint8_t, {frame_bytes}> _bytes{{}};")
1755 for name, ftype, bit_offset, bit_width
in layout:
1757 bit_offset, bit_width)
1763 """Emit a SegmentedMessageData helper for a serial list window.
1765 This emitter builds its own pre-indented text, so it uses the writer
1766 purely as a verbatim `hdr.write(...)` sink. The nested frame structs,
1767 however, are produced by the line-based `_emit_window_frame`, so their
1768 calls are wrapped in `hdr.indented()` to place them at the window
1769 class's member indent.
1774 for name, field_type
in info[
"ctor_params"]
1776 value_ctor_params = list(ctor_params)
1777 value_ctor_params.append(
1778 f
"const std::vector<value_type> &{info['list_field_name']}")
1779 value_ctor_signature =
", ".join(value_ctor_params)
1780 frame_ctor_params = list(ctor_params)
1781 frame_ctor_params.append(
"std::vector<data_frame> frames")
1782 frame_ctor_signature =
", ".join(frame_ctor_params)
1783 helper_args =
", ".join(name
for name, _
in info[
"ctor_params"])
1784 helper_call = f
"{helper_args}, std::move(frames)" if helper_args
else "std::move(frames)"
1789 count_type_synth =
types.UIntType(f
"_count_ui{info['count_width']}",
1790 info[
"count_width"])
1804 max_batch_val = (1 << info[
"count_width"]) - 1
1807 f
"struct {info['window_name']} : public esi::SegmentedMessageData {{\n")
1808 hdr.write(
"public:\n")
1809 hdr.write(f
" using value_type = {info['element_cpp']};\n")
1810 hdr.write(f
" using count_type = {info['count_cpp']};\n\n")
1811 with hdr.indented():
1815 hdr.write(
"private:\n")
1816 with hdr.indented():
1820 hdr.write(
" // One header per burst (bursts 2..N are continuations whose\n"
1821 " // only meaningful field is the count); the data frames of\n"
1822 " // every burst are stored contiguously in `data_frames` and\n"
1823 " // sliced per burst by `segment()`.\n")
1824 hdr.write(
" std::vector<header_frame> headers;\n")
1825 hdr.write(
" std::vector<data_frame> data_frames;\n")
1826 hdr.write(
" header_frame footer{};\n\n")
1827 hdr.write(
" // Largest list length encodable in one burst's count field.\n"
1828 " // Longer lists are chunked across multiple bursts.\n")
1829 hdr.write(f
" static constexpr size_t _kMaxBatch = {max_batch_val}ULL;\n\n")
1830 hdr.write(f
" void construct({frame_ctor_signature}) {{\n")
1831 hdr.write(
" if (frames.empty())\n")
1833 f
" throw std::invalid_argument(\"{info['window_name']}: bulk windowed lists cannot be empty\");\n"
1835 hdr.write(
" data_frames = std::move(frames);\n")
1836 hdr.write(
" // Split the list into bursts no larger than the count\n"
1837 " // field can encode. The read side reassembles the bursts\n"
1838 " // back into a single list.\n")
1839 hdr.write(
" const size_t total = data_frames.size();\n")
1841 " const size_t numBatches = (total + _kMaxBatch - 1) / _kMaxBatch;\n"
1843 hdr.write(
" headers.assign(numBatches, header_frame{});\n")
1844 hdr.write(
" for (size_t b = 0; b < numBatches; ++b) {\n")
1846 " const size_t batchSize =\n"
1847 " std::min<size_t>(_kMaxBatch, total - b * _kMaxBatch);\n")
1849 f
" headers[b].{info['count_field_name']}(static_cast<count_type>(batchSize));\n"
1853 " // Only the first burst's header carries the static fields.\n")
1854 for name, _
in info[
"ctor_params"]:
1855 hdr.write(f
" headers.front().{name}({name});\n")
1856 hdr.write(f
" footer.{info['count_field_name']}(0);\n")
1858 hdr.write(
"public:\n")
1859 hdr.write(f
" {info['window_name']}({frame_ctor_signature}) {{\n")
1860 hdr.write(f
" construct({helper_call});\n")
1862 hdr.write(f
" {info['window_name']}({value_ctor_signature}) {{\n")
1863 hdr.write(
" std::vector<data_frame> frames;\n")
1864 hdr.write(f
" frames.reserve({info['list_field_name']}.size());\n")
1865 hdr.write(f
" for (const auto &element : {info['list_field_name']}) {{\n")
1866 hdr.write(
" auto &frame = frames.emplace_back();\n")
1867 hdr.write(f
" frame.{info['list_field_name']}(element);\n")
1869 hdr.write(f
" construct({helper_call});\n")
1871 hdr.write(
" size_t numSegments() const override {\n"
1872 " return 2 * headers.size() + 1;\n"
1874 hdr.write(
" esi::Segment segment(size_t idx) const override {\n")
1875 hdr.write(
" const size_t footerIdx = 2 * headers.size();\n")
1876 hdr.write(
" if (idx == footerIdx)\n")
1878 " return {reinterpret_cast<const uint8_t *>(&footer), sizeof(footer)};\n"
1880 hdr.write(
" if (idx > footerIdx)\n")
1882 f
" throw std::out_of_range(\"{info['window_name']}: invalid segment index\");\n"
1884 hdr.write(
" const size_t b = idx / 2;\n")
1885 hdr.write(
" if (idx % 2 == 0)\n")
1886 hdr.write(
" return {reinterpret_cast<const uint8_t *>(&headers[b]),\n"
1887 " sizeof(header_frame)};\n")
1888 hdr.write(
" const size_t start = b * _kMaxBatch;\n")
1890 " const size_t batchSize =\n"
1891 " std::min<size_t>(_kMaxBatch, data_frames.size() - start);\n")
1893 " return {reinterpret_cast<const uint8_t *>(data_frames.data() + start),\n"
1894 " batchSize * sizeof(data_frame)};\n")
1897 f
" static constexpr std::string_view _ESI_ID = {self._cpp_string_literal(self._unwrap_aliases(window_type.into_type).id)};\n"
1903 f
" static constexpr std::string_view _ESI_WINDOW_ID = {self._cpp_string_literal(window_type.id)};\n"
1910 info: Dict[str, Any]) ->
None:
1911 """Emit accessors for the header and data fields of a window helper.
1913 Exposes each static header field as a scalar accessor, the count of data
1914 frames, and one vector-valued accessor per data field so decoded values
1915 are easy to inspect on the read side.
1917 list_field_name = info[
"list_field_name"]
1919 for field_name, field_type
in info[
"header_fields"]:
1922 if field_type
is None:
1930 f
" {cpp} {field_name}() const {{ return headers.front().{field_name}(); }}\n"
1933 f
" size_t {list_field_name}_count() const {{ return data_frames.size(); }}\n"
1935 for field_name, field_type
in info[
"data_fields"]:
1936 if field_name == list_field_name:
1937 elem_cpp =
"value_type"
1953 projection = (f
"[](const data_frame &f) -> {elem_cpp} "
1954 f
"{{ return f.{field_name}(); }}")
1956 f
" auto {field_name}() const {{\n"
1957 f
" return std::views::transform(data_frames, {projection});\n"
1959 hdr.write(f
" std::vector<{elem_cpp}> {field_name}_vector() const {{\n"
1960 f
" std::vector<{elem_cpp}> out;\n"
1961 f
" out.reserve(data_frames.size());\n"
1962 f
" for (const auto &frame : data_frames)\n"
1963 f
" out.push_back(frame.{field_name}());\n"
1968 info: Dict[str, Any]) ->
None:
1969 """Emit a few bridge helpers + a `TypeDeserializer` alias.
1971 The actual decoder lives in `esi::SerialListTypeDeserializer<T>`, which
1972 walks the header/data/footer burst protocol generically. Each window
1973 helper only has to expose:
1975 - `_headerCount(const header_frame &)` -> `count_type`
1976 - `_fromFrames(const header_frame &, std::vector<data_frame> &&)`
1977 -> `std::unique_ptr<T>`
1979 plus a `friend class esi::SerialListTypeDeserializer<T>;` so the template
1980 can reach the (private) `header_frame` definition.
1982 window_name = info[
"window_name"]
1983 count_field_name = info[
"count_field_name"]
1984 ctor_args =
", ".join(f
"h.{name}()" for name, _
in info[
"ctor_params"])
1986 ctor_args = f
"{ctor_args}, std::move(frames)"
1988 ctor_args =
"std::move(frames)"
1991 hdr.write(
"private:\n")
1993 " // Bridge helpers used by esi::SerialListTypeDeserializer<T>; the\n")
1995 " // template walks the serial-list burst protocol generically and\n")
1997 " // reaches into `header_frame` via the friend declaration below.\n")
1998 hdr.write(
" static count_type _headerCount(const header_frame &h) {\n")
1999 hdr.write(f
" return h.{count_field_name}();\n")
2001 hdr.write(f
" static std::unique_ptr<{window_name}> _fromFrames(\n")
2003 " const header_frame &h, std::vector<data_frame> &&frames) {\n")
2004 hdr.write(f
" return std::make_unique<{window_name}>({ctor_args});\n")
2007 f
" friend class esi::SerialListTypeDeserializer<{window_name}>;\n\n")
2008 hdr.write(
"public:\n")
2010 f
" using TypeDeserializer = esi::SerialListTypeDeserializer<{window_name}>;\n"
2014 """Emit a using alias when the alias targets a different C++ type."""
2015 inner_wrapped = alias_type.inner_type
2018 if inner_wrapped
is not None:
2019 inner_cpp = self.
_cpp_type(inner_wrapped)
2020 if inner_cpp
is None:
2022 if inner_cpp != alias_name:
2023 w.line(f
"using {alias_name} = {inner_cpp};")
2027 """Emit the fully ordered types.h header into the output directory."""
2029 sys.stderr.write(
"Warning: cyclic type dependencies detected.\n")
2030 sys.stderr.write(
" Logically this should not be possible.\n")
2032 " Emitted code may fail to compile due to ordering issues.\n")
2034 hdr_file = output_dir /
"types.h"
2035 with open(hdr_file,
"w", encoding=
"utf-8")
as hdr:
2038 textwrap.dedent(f
"""
2039 // Generated header for {system_name} types.
2041 // Lifetime note: accessors that return `esi::BitVector` (for
2042 // `Bits<N>` fields), `esi::IntView` (for `Int<N>`, N > 64), or
2043 // `esi::UIntView` (for `UInt<N>`, N > 64) hand back non-owning
2044 // views into the parent struct's bytes. The view is only valid
2045 // while the parent is alive. Bind the parent to a named local
2046 // first, or construct an owning `esi::Int` / `esi::UInt` /
2047 // `esi::MutableBitVector` from the view if you need the value
2048 // to outlive its source. See `esi/Values.h` for details.
2054 #include <algorithm>
2059 #include <stdexcept>
2060 #include <string_view>
2064 #include "esi/BitAccess.h"
2065 #include "esi/Common.h"
2066 #include "esi/TypedPorts.h"
2067 #include "esi/Values.h"
2069 namespace {system_name} {{
2074 w.write(f
"// Unsupported type '{skipped_type}': {reason}\n\n")
2090 w.write(textwrap.dedent(f
"""
2091 }} // namespace {system_name}
2095def run(generator: Type[Generator] = CppGenerator,
2096 cmdline_args: List[str] = sys.argv) -> int:
2097 """Create and run a generator reading options from the command line."""
2099 argparser = argparse.ArgumentParser(
2100 description=f
"Generate {generator.language} headers from an ESI manifest",
2101 formatter_class=argparse.RawDescriptionHelpFormatter,
2102 epilog=textwrap.dedent(
"""
2103 Can read the manifest from either a file OR a running accelerator.
2106 # To read the manifest from a file:
2107 esi-cppgen --file /path/to/manifest.json
2109 # To read the manifest from a running accelerator:
2110 esi-cppgen --platform cosim --connection localhost:1234
2113 argparser.add_argument(
"--file",
2116 help=
"Path to the manifest file.")
2117 argparser.add_argument(
2120 help=
"Name of platform for live accelerator connection.")
2121 argparser.add_argument(
2124 help=
"Connection string for live accelerator connection.")
2125 argparser.add_argument(
2129 help=
"Output directory for generated files. Recommend adding either `esi`"
2130 " or the system name to the end of the path so as to avoid header name"
2131 "conflicts. Defaults to `esi`")
2132 argparser.add_argument(
2135 default=
"esi_system",
2136 help=
"Name of the ESI system. For C++, this will be the namespace.")
2138 if (len(cmdline_args) <= 1):
2139 argparser.print_help()
2141 args = argparser.parse_args(cmdline_args[1:])
2143 if args.file
is not None and args.platform
is not None:
2144 print(
"Cannot specify both --file and --platform")
2147 conn: AcceleratorConnection
2148 if args.file
is not None:
2151 conn = Context.default().
connect(
"trace", f
"-{os.pathsep}{args.file}")
2152 elif args.platform
is not None:
2153 if args.connection
is None:
2154 print(
"Must specify --connection with --platform")
2156 conn = Context.default().
connect(args.platform, args.connection)
2158 print(
"Must specify either --file or --platform")
2161 output_dir = Path(args.output_dir)
2162 if output_dir.exists()
and not output_dir.is_dir():
2163 print(f
"Output directory {output_dir} is not a directory")
2165 if not output_dir.exists():
2166 output_dir.mkdir(parents=
True)
2168 gen = generator(conn)
2169 gen.generate(output_dir, args.system_name)
static void print(TypedAttr val, llvm::raw_ostream &os)
static StringAttr append(StringAttr base, const Twine &suffix)
Return a attribute with the specified suffix appended.
CppPortGroup _scalar_port_group(self, str member_name, types.BundlePort port, AppID appid)
None generate(self, Path output_dir, str system_name)
None _emit_module_class(self, str name, str system_name, ModuleInfo module_info, List[CppPortGroup] port_groups, TextIO out)
List[Tuple[str, str]] _port_using_aliases(self, CppPortKind kind, str alias_prefix, types.BundlePort port)
str get_consts_str(self, ModuleInfo module_info)
Dict[str, Instance] _build_module_instance_map(self)
str _sanitize_id(str name)
CppPortGroup _indexed_ports_group(self, str member_name, str appid_name, List[Tuple[AppID, types.BundlePort]] port_list)
CppPortGroup _mixed_struct_group(self, str member_name, str appid_name, List[Tuple[AppID, types.BundlePort]] port_list)
None write_modules(self, Path output_dir, str system_name)
str _appid_expr(AppID appid)
None __init__(self, AcceleratorConnection conn)
List[CppPortGroup] _collect_port_groups(self, Dict[AppID, types.BundlePort] ports)
None _emit_field_accessor(self, IndentedWriter w, str raw_member, str self_type, str field_name, types.ESIType field_type, int bit_offset, int bit_width)
Tuple[Dict[str, int], int] _compute_field_bit_offsets(self, types.StructType struct_type)
None _emit_window(self, IndentedWriter hdr, types.WindowType window_type)
None _emit_window_frame(self, IndentedWriter w, str frame_name, int frame_bytes, List[Tuple[str, types.ESIType, int, int]] layout)
Tuple[str, List[int]] _array_base_and_dims(self, types.ArrayType array_type)
None _emit_unpack(self, IndentedWriter w, str raw_member, str dst, str off, types.ESIType esi_type, int depth)
str _format_window_ctor_param(self, str field_name, types.ESIType field_type)
Dict[str, Any] _analyze_window(self, types.WindowType window_type)
None _emit_alias(self, IndentedWriter w, types.TypeAlias alias_type)
str type_identifier(self, types.ESIType type)
None _emit_view_store(self, IndentedWriter w, str raw_member, str src, str off_expr, int width)
str _cpp_type(self, types.ESIType wrapped)
None _emit_window_data_accessors(self, IndentedWriter hdr, Dict[str, Any] info)
str _get_bitvector_str(self, types.ESIType type)
None _emit_pack(self, IndentedWriter w, str raw_member, str src, str off, types.ESIType esi_type, int depth)
None _emit_union(self, IndentedWriter w, types.UnionType union_type)
str _cpp_string_literal(self, str value)
None __init__(self, CppTypePlanner planner)
bool _is_value_class_type(self, types.ESIType field_type)
str _ctor_param_type(self, types.ESIType field_type)
None _emit_size_assert(self, IndentedWriter w, str type_name, Optional[int] expected_bytes)
None _emit_array_accessor(self, IndentedWriter w, str raw_member, str self_type, str field_name, types.ArrayType array_type, int bit_offset)
None _emit_window_deserializer(self, IndentedWriter hdr, Dict[str, Any] info)
Optional[int] _safe_byte_width(self, types.ESIType esi_type)
bool _is_signed_int_field(self, types.ESIType field_type)
None write_header(self, Path output_dir, str system_name)
int _field_byte_width(self, types.ESIType field_type)
str _storage_type(self, int bit_width, bool signed)
None _emit_struct(self, IndentedWriter w, types.StructType struct_type)
types.ESIType _unwrap_aliases(self, types.ESIType wrapped)
List[Tuple[str, types.ESIType, int, int]] _compute_window_frame_layout(self, List[Tuple[str, Optional[types.ESIType]]] fields, int frame_bits, types.ESIType count_type_synth)
str _std_array_type(self, types.ArrayType array_type)
Set[types.ESIType] _collect_decls_from_type(self, types.ESIType wrapped)
bool _contains_unbounded(self, types.ESIType esi_type)
Tuple[List[types.ESIType], bool] _ordered_emit_types(self)
types.ESIType _unwrap_aliases(self, types.ESIType wrapped)
str _sanitize_name(self, str name)
None _prepare_types(self, List[types.ESIType] type_table)
str _auto_union_name(self, types.UnionType union_type)
None _collect_structs(self, types.ESIType t, Set[str] visited)
str _auto_window_name(self, types.WindowType window_type)
None _collect_aliases(self, types.ESIType t, Set[str] visited)
List[types.ESIType] _iter_type_children(self, types.ESIType t)
None _collect_windows(self, types.ESIType t, Set[str] visited)
bool _contains_window(self, types.ESIType esi_type)
str _auto_struct_name(self, types.StructType struct_type)
str _reserve_name(self, str base, bool is_alias)
Set[types.ESIType] _collect_decls_from_window(self, types.WindowType window_type)
None __init__(self, List[types.ESIType] type_table)
None _visit_types(self, types.ESIType t, Set[str] visited, Callable[[types.ESIType], None] visit_fn)
bool _is_supported_window(self, types.ESIType current_type)
None generate(self, Path output_dir, str system_name)
None __init__(self, AcceleratorConnection conn)
"AcceleratorConnection" connect(str platform, str connection_str)