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]
1153 count_field_name = f
"{list_field_name}_count"
1154 count_width = header_field.bulk_count_width
1156 for field
in header_frame.fields:
1157 if field.name == list_field_name:
1158 header_fields.append((count_field_name,
None))
1159 header_bits += count_width
1161 field_type = field_map[field.name]
1162 header_fields.append((field.name, field_type))
1163 header_bits += field_type.bit_width
1167 for field
in data_frame.fields:
1168 if field.name == list_field_name:
1169 data_fields.append((list_field_name, list_type.element_type))
1170 data_bits += list_type.element_type.bit_width
1172 field_type = field_map[field.name]
1173 data_fields.append((field.name, field_type))
1174 data_bits += field_type.bit_width
1179 frame_bits = max(header_bits, data_bits)
1180 frame_bytes = (frame_bits + 7) // 8
1183 "ctor_params": ctor_params,
1184 "count_cpp": count_cpp,
1185 "count_field_name": count_field_name,
1186 "count_width": count_width,
1187 "data_fields": data_fields,
1188 "element_cpp": self.
_cpp_type(list_type.element_type),
1189 "frame_bits": frame_bits,
1190 "frame_bytes": frame_bytes,
1191 "header_fields": header_fields,
1192 "list_field_name": list_field_name,
1197 self, struct_type: types.StructType) -> Tuple[Dict[str, int], int]:
1198 """Return `(bit_offset_by_name, total_bits)` for non-void fields.
1200 Fields are laid out LSB-first in wire order, which is reversed manifest
1201 order when `cpp_type.reverse` is set. Caller already knows each field's
1202 type/width from `struct_type.fields`; we only need the per-name offset
1203 and the resulting total bit width to size the storage array.
1205 declared = struct_type.fields
1206 if struct_type.cpp_type.reverse:
1207 declared = list(reversed(declared))
1208 bit_offsets: Dict[str, int] = {}
1210 for field_name, field_type
in declared:
1211 if self.
_cpp_type(field_type) ==
"void":
1213 bit_offsets[field_name] = bit_offset
1214 bit_offset += field_type.bit_width
1215 return bit_offsets, bit_offset
1218 """True if the field is a signed integer (only `IntType`, not `UIntType`
1225 off_expr: str, width: int) ->
None:
1226 """Emit a loop writing the low `width` bits of the view `src` into
1227 `raw_member`, with bit `b` landing at wire bit `off_expr + b`. Shared by
1228 the scalar view-class field setter and the per-element view-array setter.
1230 w.line(f
"const std::size_t n = "
1231 f
"std::min<std::size_t>({src}.width(), {width});")
1232 with w.block(f
"for (std::size_t b = 0; b < {width}; ++b)"):
1233 w.line(f
"const std::size_t g = {off_expr} + b;")
1234 w.line(f
"const bool val = (b < n) && {src}.getBit(b);")
1237 w.line(f
"{raw_member}[g / 8] |= "
1238 f
"static_cast<uint8_t>(uint8_t{{1}} << (g % 8));")
1241 w.line(f
"{raw_member}[g / 8] &= "
1242 f
"static_cast<uint8_t>(~(uint8_t{{1}} << (g % 8)));")
1245 self_type: str, field_name: str,
1247 bit_width: int) ->
None:
1248 """Emit a getter/setter pair that reads/writes `field_name` out of the
1249 raw bytes member `raw_member` at `bit_offset` / `bit_width`.
1251 Setters share the field's name (no `set_` prefix) and return
1252 `self_type &` so the caller can chain calls (e.g.
1253 `Foo{}.a(1).b(2).inner(x)`).
1255 For integer fields the emitter picks between three inline strategies:
1257 * Native ints (<= 64 bits). Delegate to the compile-time
1258 `esi::detail::{read,write}{Un,}signedBits` helpers from
1259 `esi/BitAccess.h`, which loop over the constituent bits at the
1260 field's constant offset/width. The non-type template parameters
1261 let the optimiser fully unroll the loop, so a byte-aligned
1262 standard-width field still collapses to a single load/store on
1263 -O1+ -- no special-cased `memcpy` / per-byte fast path required.
1264 * `bool` (a single bit). Same helpers, specialised to read/write one
1265 bit and hand back a `bool`.
1266 * View-class fields. Triggered when `_is_value_class_type` is true --
1267 currently widths above 64 bits, for any of `BitsType`, signed, or
1268 unsigned. Returns a non-owning
1269 `esi::BitVector` / `esi::IntView` / `esi::UIntView` view *into*
1270 the parent struct's `_bytes` -- zero allocation, no copy. The
1271 setter accepts any `esi::BitVector` (so views and owning
1272 subclasses both work) and writes back bit-by-bit. The returned
1273 view dangles when the parent buffer dies; see the lifetime warning
1274 at the top of the generated header.
1277 if field_cpp ==
"void":
1287 byte_offset = bit_offset // 8
1288 sub_bit_index = bit_offset % 8
1293 span_bytes = (sub_bit_index + bit_width + 7) // 8
1294 with w.block(f
"{field_cpp} {field_name}() const"):
1295 w.line(f
"return {field_cpp}(")
1296 w.line(f
" std::span<const uint8_t>("
1297 f
"{raw_member}.data() + {byte_offset}, {span_bytes}),")
1298 w.line(f
" static_cast<std::size_t>({bit_width}),")
1299 w.line(f
" static_cast<uint8_t>({sub_bit_index}));")
1300 with w.block(f
"{self_type} &{field_name}(const {field_cpp} &v)"):
1304 w.line(
"return *this;")
1309 if bit_width == 1
and field_cpp ==
"bool":
1310 with w.block(f
"bool {field_name}() const"):
1311 w.line(f
"return esi::detail::readUnsignedBits<uint8_t, "
1312 f
"{bit_offset}, 1>({raw_member}.data()) != 0;")
1313 with w.block(f
"{self_type} &{field_name}(bool v)"):
1314 w.line(f
"esi::detail::writeUnsignedBits<uint8_t, "
1315 f
"{bit_offset}, 1>({raw_member}.data(), "
1316 f
"static_cast<uint8_t>(v) & uint8_t{{1}});")
1317 w.line(
"return *this;")
1321 kind =
"Signed" if signed
else "Unsigned"
1332 with w.block(f
"{field_cpp} {field_name}() const"):
1333 w.line(f
"return esi::detail::read{kind}Bits<{field_cpp}, "
1334 f
"{bit_offset}, {bit_width}>({raw_member}.data());")
1335 with w.block(f
"{self_type} &{field_name}({field_cpp} v)"):
1336 w.line(f
"esi::detail::write{kind}Bits<{field_cpp}, "
1337 f
"{bit_offset}, {bit_width}>({raw_member}.data(), v);")
1338 w.line(
"return *this;")
1350 assert bit_width >= 0, (
1351 f
"field '{field_name}': unbounded aggregate field reached the "
1352 f
"emitter; the planner should have excluded the parent struct "
1353 f
"via `_contains_unbounded`")
1366 byte_aligned = (bit_offset % 8 == 0
and bit_width % 8 == 0)
1367 byte_offset = bit_offset // 8
1368 byte_width = (bit_width + 7) // 8
1370 if not is_view_array:
1375 with w.block(f
"{field_cpp} {field_name}() const"):
1376 w.line(f
"{field_cpp} out{{}};")
1377 w.line(f
"for (std::size_t i = 0; i < {byte_width}; ++i)")
1379 w.line(f
"reinterpret_cast<uint8_t *>(&out)[i] = "
1380 f
"{raw_member}[{byte_offset} + i];")
1381 w.line(
"return out;")
1382 with w.block(f
"{self_type} &{field_name}(const {field_cpp} &v)"):
1383 w.line(f
"for (std::size_t i = 0; i < {byte_width}; ++i)")
1385 w.line(f
"{raw_member}[{byte_offset} + i] = "
1386 f
"reinterpret_cast<const uint8_t *>(&v)[i];")
1387 w.line(
"return *this;")
1392 with w.block(f
"{field_cpp} {field_name}() const"):
1393 w.line(f
"{field_cpp} out{{}};")
1394 w.line(f
"esi::detail::copyBitsIn<{bit_offset}, "
1395 f
"{bit_width}>({raw_member}.data(), "
1396 f
"reinterpret_cast<uint8_t *>(&out));")
1397 w.line(
"return out;")
1398 with w.block(f
"{self_type} &{field_name}(const {field_cpp} &v)"):
1399 w.line(f
"esi::detail::copyBitsOut<{bit_offset}, "
1400 f
"{bit_width}>({raw_member}.data(), "
1401 f
"reinterpret_cast<const uint8_t *>(&v));")
1402 w.line(
"return *this;")
1425 elem_type = wrapped.element_type
1427 elem_width = elem_type.bit_width
1428 elem_count = wrapped.size
1429 with w.block(f
"{elem_cpp} {field_name}(std::size_t i) const"):
1430 w.line(f
"const std::size_t bit_off = {bit_offset} + i * {elem_width};")
1431 w.line(f
"return {elem_cpp}(")
1432 w.line(f
" std::span<const uint8_t>("
1433 f
"{raw_member}.data() + bit_off / 8,")
1435 f
"(bit_off % 8 + {elem_width} + 7) / 8),")
1436 w.line(f
" static_cast<std::size_t>({elem_width}),")
1437 w.line(f
" static_cast<uint8_t>(bit_off % 8));")
1438 with w.block(f
"{self_type} &{field_name}(std::size_t i, "
1439 f
"const {elem_cpp} &v)"):
1440 w.line(f
"const std::size_t bit_off = {bit_offset} + i * {elem_width};")
1442 w.line(
"return *this;")
1446 with w.block(f
"auto {field_name}() const"):
1447 w.line(f
"return std::views::iota("
1448 f
"std::size_t{{0}}, std::size_t{{{elem_count}}}) |")
1449 w.line(
" std::views::transform([this](std::size_t i) {")
1450 w.line(f
" return this->{field_name}(i);")
1454 with w.block(f
"{self_type} &{field_name}("
1455 f
"const std::array<{elem_cpp}, {elem_count}> &v)"):
1456 w.line(f
"for (std::size_t i = 0; i < {elem_count}; ++i)")
1458 w.line(f
"{field_name}(i, v[i]);")
1459 w.line(
"return *this;")
1462 self_type: str, field_name: str,
1464 bit_offset: int) ->
None:
1465 """Emit the accessor set for a fixed-size array field.
1467 Four accessors are emitted, matching the existing array API:
1469 * `Elem field(std::size_t i)` / `Self &field(std::size_t i, const Elem &)`
1470 -- per-element get/set, where `Elem` is the (possibly itself an
1471 array) element type.
1472 * `Whole field()` / `Self &field(const Whole &)` -- whole-array
1473 get/set, expressed in terms of the per-element accessors.
1475 The per-element get/set bodies are produced by `_emit_unpack` /
1476 `_emit_pack`, which recurse to the scalar leaves and place each leaf at
1477 its true wire offset `bit_offset + i * elemWidth (+ ...)`. This is what
1478 lets sub-byte, odd-width, sub-byte-aggregate, and *arbitrarily nested*
1479 array elements round-trip even when the C++ element layout does not
1480 match the wire layout.
1482 elem_type = array_type.element_type
1484 elem_width = elem_type.bit_width
1485 elem_count = array_type.size
1487 elem_off = f
"{bit_offset} + i * {elem_width}"
1489 with w.block(f
"{elem_cpp} {field_name}(std::size_t i) const"):
1490 w.line(f
"{elem_cpp} out{{}};")
1491 self.
_emit_unpack(w, raw_member,
"out", elem_off, elem_type, 1)
1492 w.line(
"return out;")
1494 with w.block(f
"{self_type} &{field_name}(std::size_t i, "
1495 f
"const {elem_cpp} &v)"):
1496 self.
_emit_pack(w, raw_member,
"v", elem_off, elem_type, 1)
1497 w.line(
"return *this;")
1499 with w.block(f
"{whole_cpp} {field_name}() const"):
1500 w.line(f
"{whole_cpp} out{{}};")
1501 w.line(f
"for (std::size_t i = 0; i < {elem_count}; ++i)")
1503 w.line(f
"out[i] = {field_name}(i);")
1504 w.line(
"return out;")
1506 with w.block(f
"{self_type} &{field_name}(const {whole_cpp} &v)"):
1507 w.line(f
"for (std::size_t i = 0; i < {elem_count}; ++i)")
1509 w.line(f
"{field_name}(i, v[i]);")
1510 w.line(
"return *this;")
1512 def _emit_unpack(self, w: IndentedWriter, raw_member: str, dst: str, off: str,
1514 """Emit statements that read a value of `esi_type` out of `raw_member`
1515 at wire bit offset `off` (a C++ `size_t` expression) into the
1516 already-declared, zero-initialised C++ lvalue `dst`.
1518 Recurses through arrays (one loop per dimension) down to scalar / struct
1519 / union leaves, which are read with the runtime-offset bit helpers.
1524 ew = wrapped.element_type.bit_width
1525 with w.block(f
"for (std::size_t {idx} = 0; {idx} < "
1526 f
"{wrapped.size}; ++{idx})"):
1528 f
"{off} + {idx} * {ew}", wrapped.element_type,
1535 w.line(f
"esi::detail::copyBitsInDyn({raw_member}.data(), {off},")
1536 w.line(f
" reinterpret_cast<uint8_t *>(&{dst}), {wrapped.bit_width});")
1540 raise NotImplementedError(
1541 "arrays of integers wider than 64 bits are not supported")
1544 w.line(f
"{dst} = esi::detail::readUnsignedBitsDyn<uint8_t>("
1545 f
"{raw_member}.data(), {off}, 1) != 0;")
1547 w.line(f
"{dst} = esi::detail::readSignedBitsDyn<{cpp}>("
1548 f
"{raw_member}.data(), {off}, {wrapped.bit_width});")
1550 w.line(f
"{dst} = esi::detail::readUnsignedBitsDyn<{cpp}>("
1551 f
"{raw_member}.data(), {off}, {wrapped.bit_width});")
1553 raise NotImplementedError(
1554 f
"unsupported array element type '{wrapped}' for C++ generation")
1556 def _emit_pack(self, w: IndentedWriter, raw_member: str, src: str, off: str,
1558 """Inverse of `_emit_unpack`: write the C++ value `src` of `esi_type`
1559 into `raw_member` at wire bit offset `off`."""
1563 ew = wrapped.element_type.bit_width
1564 with w.block(f
"for (std::size_t {idx} = 0; {idx} < "
1565 f
"{wrapped.size}; ++{idx})"):
1566 self.
_emit_pack(w, raw_member, f
"{src}[{idx}]", f
"{off} + {idx} * {ew}",
1567 wrapped.element_type, depth + 1)
1570 w.line(f
"esi::detail::copyBitsOutDyn({raw_member}.data(), {off},")
1571 w.line(f
" reinterpret_cast<const uint8_t *>(&{src}), "
1572 f
"{wrapped.bit_width});")
1576 raise NotImplementedError(
1577 "arrays of integers wider than 64 bits are not supported")
1580 w.line(f
"esi::detail::writeUnsignedBitsDyn<uint8_t>("
1581 f
"{raw_member}.data(), "
1582 f
"static_cast<uint8_t>({src}) & uint8_t{{1}}, {off}, 1);")
1584 w.line(f
"esi::detail::writeSignedBitsDyn<{cpp}>("
1585 f
"{raw_member}.data(), {src}, {off}, {wrapped.bit_width});")
1587 w.line(f
"esi::detail::writeUnsignedBitsDyn<{cpp}>("
1588 f
"{raw_member}.data(), {src}, {off}, {wrapped.bit_width});")
1590 raise NotImplementedError(
1591 f
"unsupported array element type '{wrapped}' for C++ generation")
1594 """Return the C++ constructor-parameter type for a setter call."""
1599 return f
"const {field_cpp} &"
1603 """Emit a packed struct as a raw byte buffer with bit-precise accessors.
1605 The struct's storage is a single `std::array<uint8_t, N>` (where `N`
1606 is the byte width derived from the manifest) whose layout matches the
1607 on-wire bit-packing exactly. Per-field accessors (generated below)
1608 read/write the bits so the wire format does not depend on C++ bit-field
1609 allocation rules, which differ between the Itanium ABI (GCC/Clang) and MSVC.
1613 if struct_type.bit_width == 0:
1617 total_bytes = (total_bits + 7) // 8
1621 logical_fields = [(name, ftype)
1622 for name, ftype
in struct_type.fields
1630 if not logical_fields:
1633 with w.block(f
"struct {struct_name}", tail=
";"):
1638 w.access(
"private:")
1639 w.line(f
"std::array<uint8_t, {max(total_bytes, 1)}> _bytes{{}};")
1642 w.line(f
"{struct_name}() = default;")
1643 ctor_params =
", ".join(f
"{self._ctor_param_type(ftype)} {name}"
1644 for name, ftype
in logical_fields)
1645 with w.block(f
"{struct_name}({ctor_params})"):
1648 chain =
".".join(f
"{name}({name})" for name, _
in logical_fields)
1649 w.line(f
"this->{chain};")
1654 for name, ftype
in logical_fields:
1656 bit_offsets[name], ftype.bit_width)
1659 w.line(f
"static constexpr std::string_view _ESI_ID = "
1660 f
"{self._cpp_string_literal(struct_type.id)};")
1662 if expected_bytes
is not None and expected_bytes > 0:
1667 """Emit a union as a raw byte buffer with per-variant accessors.
1669 The union's storage is a single `std::array<uint8_t, N>` sized to the
1670 widest variant. Each variant lives at the MSB end of the buffer
1671 (matching the existing SV-style packed union layout where padding
1672 occupies the lower bytes), so the byte offset for a variant of size
1673 V is `union_bytes - V`. Sub-byte integer variants are byte-padded to
1674 full bytes within that region, matching the Python runtime's union
1679 if union_type.bit_width == 0:
1684 with w.block(f
"struct {union_name}", tail=
";"):
1686 w.access(
"private:")
1687 w.line(f
"std::array<uint8_t, {union_bytes}> _bytes{{}};")
1690 w.line(f
"{union_name}() = default;")
1693 for field_name, field_type
in union_type.fields:
1695 if field_cpp ==
"void":
1698 byte_offset = union_bytes - field_bytes
1699 bit_offset = byte_offset * 8
1700 bit_width = field_type.bit_width
1705 field_type, bit_offset, bit_width)
1708 w.line(f
"static constexpr std::string_view _ESI_ID = "
1709 f
"{self._cpp_string_literal(union_type.id)};")
1715 self, fields: List[Tuple[str, Optional[
types.ESIType]]], frame_bits: int,
1718 """Compute (name, type, bit_offset, bit_width) for each window frame field.
1720 Fields are listed in declared (MSB-first) order and laid out MSB-aligned
1721 within `frame_bits`, matching how CIRCT lowers the serial-window frame
1722 union: the first field occupies the highest bits, each subsequent field
1723 sits immediately below it with no inter-field padding, and any slack
1724 (`frame_bits - content_bits`) is left as zero padding at the LSB end.
1725 The returned `bit_offset` is the field's LSB position within the frame's
1726 little-endian `_bytes` array, ready to hand straight to
1727 `_emit_field_accessor`.
1729 The sentinel `(name, None)` count field is materialised as
1730 `count_type_synth` so it can flow through the standard
1731 `_emit_field_accessor` path.
1733 Raises `ValueError` if a field is unbounded (`bit_width < 0`) or if the
1734 accumulated content overflows `frame_bits` -- either would produce bogus
1735 offsets, so fail fast rather than emit silently-wrong accessors.
1738 bit_top = frame_bits
1739 for name, ftype
in fields:
1740 actual_type = count_type_synth
if ftype
is None else ftype
1741 bit_width = actual_type.bit_width
1744 f
"window frame field '{name}' has an unbounded width; window "
1745 f
"codegen requires every frame field to be fixed-width")
1746 bit_top -= bit_width
1749 f
"window frame field '{name}' overflows the {frame_bits}-bit frame "
1750 f
"(content is wider than the frame width)")
1751 layout.append((name, actual_type, bit_top, bit_width))
1755 self, w: IndentedWriter, frame_name: str, frame_bytes: int,
1757 """Emit a window header/data frame as a raw-bytes struct with accessors.
1759 Nested inside the window helper class, it uses the same accessor pattern
1760 as top-level structs/unions: a private `_bytes` array plus per-field
1761 getter/setter pairs returning `frame_name &` to allow chaining.
1763 with w.block(f
"struct {frame_name}", tail=
";"):
1764 w.access(
"private:")
1765 w.line(f
"std::array<uint8_t, {frame_bytes}> _bytes{{}};")
1768 for name, ftype, bit_offset, bit_width
in layout:
1770 bit_offset, bit_width)
1776 """Emit a SegmentedMessageData helper for a serial list window.
1778 This emitter builds its own pre-indented text, so it uses the writer
1779 purely as a verbatim `hdr.write(...)` sink. The nested frame structs,
1780 however, are produced by the line-based `_emit_window_frame`, so their
1781 calls are wrapped in `hdr.indented()` to place them at the window
1782 class's member indent.
1787 for name, field_type
in info[
"ctor_params"]
1789 value_ctor_params = list(ctor_params)
1790 value_ctor_params.append(
1791 f
"const std::vector<value_type> &{info['list_field_name']}")
1792 value_ctor_signature =
", ".join(value_ctor_params)
1793 frame_ctor_params = list(ctor_params)
1794 frame_ctor_params.append(
"std::vector<data_frame> frames")
1795 frame_ctor_signature =
", ".join(frame_ctor_params)
1796 helper_args =
", ".join(name
for name, _
in info[
"ctor_params"])
1797 helper_call = f
"{helper_args}, std::move(frames)" if helper_args
else "std::move(frames)"
1802 count_type_synth =
types.UIntType(f
"_count_ui{info['count_width']}",
1803 info[
"count_width"])
1817 max_batch_val = (1 << info[
"count_width"]) - 1
1820 f
"struct {info['window_name']} : public esi::SegmentedMessageData {{\n")
1821 hdr.write(
"public:\n")
1822 hdr.write(f
" using value_type = {info['element_cpp']};\n")
1823 hdr.write(f
" using count_type = {info['count_cpp']};\n\n")
1824 with hdr.indented():
1828 hdr.write(
"private:\n")
1829 with hdr.indented():
1833 hdr.write(
" // One header per burst (bursts 2..N are continuations whose\n"
1834 " // only meaningful field is the count); the data frames of\n"
1835 " // every burst are stored contiguously in `data_frames` and\n"
1836 " // sliced per burst by `segment()`.\n")
1837 hdr.write(
" std::vector<header_frame> headers;\n")
1838 hdr.write(
" std::vector<data_frame> data_frames;\n")
1839 hdr.write(
" header_frame footer{};\n\n")
1840 hdr.write(
" // Largest list length encodable in one burst's count field.\n"
1841 " // Longer lists are chunked across multiple bursts.\n")
1842 hdr.write(f
" static constexpr size_t _kMaxBatch = {max_batch_val}ULL;\n\n")
1843 hdr.write(f
" void construct({frame_ctor_signature}) {{\n")
1844 hdr.write(
" if (frames.empty())\n")
1846 f
" throw std::invalid_argument(\"{info['window_name']}: bulk windowed lists cannot be empty\");\n"
1848 hdr.write(
" data_frames = std::move(frames);\n")
1849 hdr.write(
" // Split the list into bursts no larger than the count\n"
1850 " // field can encode. The read side reassembles the bursts\n"
1851 " // back into a single list.\n")
1852 hdr.write(
" const size_t total = data_frames.size();\n")
1854 " const size_t numBatches = (total + _kMaxBatch - 1) / _kMaxBatch;\n"
1856 hdr.write(
" headers.assign(numBatches, header_frame{});\n")
1857 hdr.write(
" for (size_t b = 0; b < numBatches; ++b) {\n")
1859 " const size_t batchSize =\n"
1860 " std::min<size_t>(_kMaxBatch, total - b * _kMaxBatch);\n")
1862 f
" headers[b].{info['count_field_name']}(static_cast<count_type>(batchSize));\n"
1866 " // Only the first burst's header carries the static fields.\n")
1867 for name, _
in info[
"ctor_params"]:
1868 hdr.write(f
" headers.front().{name}({name});\n")
1869 hdr.write(f
" footer.{info['count_field_name']}(0);\n")
1871 hdr.write(
"public:\n")
1872 hdr.write(f
" {info['window_name']}({frame_ctor_signature}) {{\n")
1873 hdr.write(f
" construct({helper_call});\n")
1875 hdr.write(f
" {info['window_name']}({value_ctor_signature}) {{\n")
1876 hdr.write(
" std::vector<data_frame> frames;\n")
1877 hdr.write(f
" frames.reserve({info['list_field_name']}.size());\n")
1878 hdr.write(f
" for (const auto &element : {info['list_field_name']}) {{\n")
1879 hdr.write(
" auto &frame = frames.emplace_back();\n")
1880 hdr.write(f
" frame.{info['list_field_name']}(element);\n")
1882 hdr.write(f
" construct({helper_call});\n")
1884 hdr.write(
" size_t numSegments() const override {\n"
1885 " return 2 * headers.size() + 1;\n"
1887 hdr.write(
" esi::Segment segment(size_t idx) const override {\n")
1888 hdr.write(
" const size_t footerIdx = 2 * headers.size();\n")
1889 hdr.write(
" if (idx == footerIdx)\n")
1891 " return {reinterpret_cast<const uint8_t *>(&footer), sizeof(footer)};\n"
1893 hdr.write(
" if (idx > footerIdx)\n")
1895 f
" throw std::out_of_range(\"{info['window_name']}: invalid segment index\");\n"
1897 hdr.write(
" const size_t b = idx / 2;\n")
1898 hdr.write(
" if (idx % 2 == 0)\n")
1899 hdr.write(
" return {reinterpret_cast<const uint8_t *>(&headers[b]),\n"
1900 " sizeof(header_frame)};\n")
1901 hdr.write(
" const size_t start = b * _kMaxBatch;\n")
1903 " const size_t batchSize =\n"
1904 " std::min<size_t>(_kMaxBatch, data_frames.size() - start);\n")
1906 " return {reinterpret_cast<const uint8_t *>(data_frames.data() + start),\n"
1907 " batchSize * sizeof(data_frame)};\n")
1910 f
" static constexpr std::string_view _ESI_ID = {self._cpp_string_literal(self._unwrap_aliases(window_type.into_type).id)};\n"
1916 f
" static constexpr std::string_view _ESI_WINDOW_ID = {self._cpp_string_literal(window_type.id)};\n"
1923 info: Dict[str, Any]) ->
None:
1924 """Emit accessors for the header and data fields of a window helper.
1926 Exposes each static header field as a scalar accessor, the count of data
1927 frames, and one vector-valued accessor per data field so decoded values
1928 are easy to inspect on the read side.
1930 list_field_name = info[
"list_field_name"]
1932 for field_name, field_type
in info[
"header_fields"]:
1935 if field_type
is None:
1943 f
" {cpp} {field_name}() const {{ return headers.front().{field_name}(); }}\n"
1946 f
" size_t {list_field_name}_count() const {{ return data_frames.size(); }}\n"
1948 for field_name, field_type
in info[
"data_fields"]:
1949 if field_name == list_field_name:
1950 elem_cpp =
"value_type"
1966 projection = (f
"[](const data_frame &f) -> {elem_cpp} "
1967 f
"{{ return f.{field_name}(); }}")
1969 f
" auto {field_name}() const {{\n"
1970 f
" return std::views::transform(data_frames, {projection});\n"
1972 hdr.write(f
" std::vector<{elem_cpp}> {field_name}_vector() const {{\n"
1973 f
" std::vector<{elem_cpp}> out;\n"
1974 f
" out.reserve(data_frames.size());\n"
1975 f
" for (const auto &frame : data_frames)\n"
1976 f
" out.push_back(frame.{field_name}());\n"
1981 info: Dict[str, Any]) ->
None:
1982 """Emit a few bridge helpers + a `TypeDeserializer` alias.
1984 The actual decoder lives in `esi::SerialListTypeDeserializer<T>`, which
1985 walks the header/data/footer burst protocol generically. Each window
1986 helper only has to expose:
1988 - `_headerCount(const header_frame &)` -> `count_type`
1989 - `_fromFrames(const header_frame &, std::vector<data_frame> &&)`
1990 -> `std::unique_ptr<T>`
1992 plus a `friend class esi::SerialListTypeDeserializer<T>;` so the template
1993 can reach the (private) `header_frame` definition.
1995 window_name = info[
"window_name"]
1996 count_field_name = info[
"count_field_name"]
1997 ctor_args =
", ".join(f
"h.{name}()" for name, _
in info[
"ctor_params"])
1999 ctor_args = f
"{ctor_args}, std::move(frames)"
2001 ctor_args =
"std::move(frames)"
2004 hdr.write(
"private:\n")
2006 " // Bridge helpers used by esi::SerialListTypeDeserializer<T>; the\n")
2008 " // template walks the serial-list burst protocol generically and\n")
2010 " // reaches into `header_frame` via the friend declaration below.\n")
2011 hdr.write(
" static count_type _headerCount(const header_frame &h) {\n")
2012 hdr.write(f
" return h.{count_field_name}();\n")
2014 hdr.write(f
" static std::unique_ptr<{window_name}> _fromFrames(\n")
2016 " const header_frame &h, std::vector<data_frame> &&frames) {\n")
2017 hdr.write(f
" return std::make_unique<{window_name}>({ctor_args});\n")
2020 f
" friend class esi::SerialListTypeDeserializer<{window_name}>;\n\n")
2021 hdr.write(
"public:\n")
2023 f
" using TypeDeserializer = esi::SerialListTypeDeserializer<{window_name}>;\n"
2027 """Emit a using alias when the alias targets a different C++ type."""
2028 inner_wrapped = alias_type.inner_type
2031 if inner_wrapped
is not None:
2032 inner_cpp = self.
_cpp_type(inner_wrapped)
2033 if inner_cpp
is None:
2035 if inner_cpp != alias_name:
2036 w.line(f
"using {alias_name} = {inner_cpp};")
2040 """Emit the fully ordered types.h header into the output directory."""
2042 sys.stderr.write(
"Warning: cyclic type dependencies detected.\n")
2043 sys.stderr.write(
" Logically this should not be possible.\n")
2045 " Emitted code may fail to compile due to ordering issues.\n")
2047 hdr_file = output_dir /
"types.h"
2048 with open(hdr_file,
"w", encoding=
"utf-8")
as hdr:
2051 textwrap.dedent(f
"""
2052 // Generated header for {system_name} types.
2054 // Lifetime note: accessors that return `esi::BitVector` (for
2055 // `Bits<N>` fields), `esi::IntView` (for `Int<N>`, N > 64), or
2056 // `esi::UIntView` (for `UInt<N>`, N > 64) hand back non-owning
2057 // views into the parent struct's bytes. The view is only valid
2058 // while the parent is alive. Bind the parent to a named local
2059 // first, or construct an owning `esi::Int` / `esi::UInt` /
2060 // `esi::MutableBitVector` from the view if you need the value
2061 // to outlive its source. See `esi/Values.h` for details.
2067 #include <algorithm>
2072 #include <stdexcept>
2073 #include <string_view>
2077 #include "esi/BitAccess.h"
2078 #include "esi/Common.h"
2079 #include "esi/TypedPorts.h"
2080 #include "esi/Values.h"
2082 namespace {system_name} {{
2087 w.write(f
"// Unsupported type '{skipped_type}': {reason}\n\n")
2103 w.write(textwrap.dedent(f
"""
2104 }} // namespace {system_name}
2108def run(generator: Type[Generator] = CppGenerator,
2109 cmdline_args: List[str] = sys.argv) -> int:
2110 """Create and run a generator reading options from the command line."""
2112 argparser = argparse.ArgumentParser(
2113 description=f
"Generate {generator.language} headers from an ESI manifest",
2114 formatter_class=argparse.RawDescriptionHelpFormatter,
2115 epilog=textwrap.dedent(
"""
2116 Can read the manifest from either a file OR a running accelerator.
2119 # To read the manifest from a file:
2120 esi-cppgen --file /path/to/manifest.json
2122 # To read the manifest from a running accelerator:
2123 esi-cppgen --platform cosim --connection localhost:1234
2126 argparser.add_argument(
"--file",
2129 help=
"Path to the manifest file.")
2130 argparser.add_argument(
2133 help=
"Name of platform for live accelerator connection.")
2134 argparser.add_argument(
2137 help=
"Connection string for live accelerator connection.")
2138 argparser.add_argument(
2142 help=
"Output directory for generated files. Recommend adding either `esi`"
2143 " or the system name to the end of the path so as to avoid header name"
2144 "conflicts. Defaults to `esi`")
2145 argparser.add_argument(
2148 default=
"esi_system",
2149 help=
"Name of the ESI system. For C++, this will be the namespace.")
2151 if (len(cmdline_args) <= 1):
2152 argparser.print_help()
2154 args = argparser.parse_args(cmdline_args[1:])
2156 if args.file
is not None and args.platform
is not None:
2157 print(
"Cannot specify both --file and --platform")
2160 conn: AcceleratorConnection
2161 if args.file
is not None:
2164 conn = Context.default().
connect(
"trace", f
"-{os.pathsep}{args.file}")
2165 elif args.platform
is not None:
2166 if args.connection
is None:
2167 print(
"Must specify --connection with --platform")
2169 conn = Context.default().
connect(args.platform, args.connection)
2171 print(
"Must specify either --file or --platform")
2174 output_dir = Path(args.output_dir)
2175 if output_dir.exists()
and not output_dir.is_dir():
2176 print(f
"Output directory {output_dir} is not a directory")
2178 if not output_dir.exists():
2179 output_dir.mkdir(parents=
True)
2181 gen = generator(conn)
2182 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)