CIRCT 23.0.0git
Loading...
Searching...
No Matches
codegen.py
Go to the documentation of this file.
1# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
2# See https://llvm.org/LICENSE.txt for license information.
3# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
4"""Code generation from ESI manifests to source code.
5
6Uses a two-pass approach for C++: first collect and name all reachable types,
7then emit structs/aliases in a dependency-ordered sequence so headers are
8standalone and deterministic.
9"""
10
11# C++ header support included with the runtime, though it is intended to be
12# extensible for other languages.
13
14from typing import Dict, List, Set, TextIO, Tuple, Type, Optional
15from .accelerator import AcceleratorConnection, Context
16from .esiCppAccel import ModuleInfo
17from . import types
18
19import argparse
20from pathlib import Path
21import textwrap
22import sys
23
24_thisdir = Path(__file__).absolute().resolve().parent
25
26
28 """Base class for all generators."""
29
30 language: Optional[str] = None
31
32 def __init__(self, conn: AcceleratorConnection):
33 self.manifest = conn.manifest()
34
35 def generate(self, output_dir: Path, system_name: str):
36 raise NotImplementedError("Generator.generate() must be overridden")
37
38
40 """Generate C++ headers from an ESI manifest."""
41
42 language = "C++"
43
44 def __init__(self, conn: AcceleratorConnection):
45 super().__init__(conn)
46 self.type_planner = CppTypePlanner(self.manifest.type_table)
48
49 def get_consts_str(self, module_info: ModuleInfo) -> str:
50 """Get the C++ code for a constant in a module."""
51 const_strs: List[str] = [
52 f"static constexpr {self.type_emitter.type_identifier(const.type)} "
53 f"{name} = 0x{const.value:x};"
54 for name, const in module_info.constants.items()
55 ]
56 return "\n".join(const_strs)
57
58 def write_modules(self, output_dir: Path, system_name: str):
59 """Write the C++ header. One for each module in the manifest."""
60
61 for module_info in self.manifest.module_infos:
62 s = f"""
63 /// Generated header for {system_name} module {module_info.name}.
64 #pragma once
65 #include "types.h"
66
67 namespace {system_name} {{
68 class {module_info.name} {{
69 public:
70 {self.get_consts_str(module_info)}
71 }};
72 }} // namespace {system_name}
73 """
74
75 hdr_file = output_dir / f"{module_info.name}.h"
76 with open(hdr_file, "w") as hdr:
77 hdr.write(textwrap.dedent(s))
78
79 def generate(self, output_dir: Path, system_name: str):
80 self.type_emitter.write_header(output_dir, system_name)
81 self.write_modules(output_dir, system_name)
82
83
85 """Plan C++ type naming and ordering from an ESI manifest."""
86
87 def __init__(self, type_table) -> None:
88 """Initialize the generator with the manifest and target namespace."""
89 # Map manifest type ids to their preferred C++ names.
90 self.type_id_map: Dict[types.ESIType, str] = {}
91 # Track all names already taken to avoid collisions. True => alias-based.
92 self.used_names: Dict[str, bool] = {}
93 # Track alias base names to warn on collisions.
94 self.alias_base_names: Set[str] = set()
95 self.ordered_types: List[types.ESIType] = []
96 self.has_cycle = False
97 self._prepare_types(type_table)
98
99 def _prepare_types(self, type_table) -> None:
100 """Name the types and prepare for emission by registering all reachable
101 types and assigning."""
102 visited: Set[str] = set()
103 for t in type_table:
104 self._collect_aliases(t, visited)
105
106 visited = set()
107 for t in type_table:
108 self._collect_structs(t, visited)
109
111
112 def _sanitize_name(self, name: str) -> str:
113 """Create a C++-safe identifier from the manifest-provided name."""
114 name = name.replace("::", "_")
115 if name.startswith("@"):
116 name = name[1:]
117 sanitized = []
118 for ch in name:
119 if ch.isalnum() or ch == "_":
120 sanitized.append(ch)
121 else:
122 sanitized.append("_")
123 if not sanitized:
124 return "Type"
125 if sanitized[0].isdigit():
126 sanitized.insert(0, "_")
127 return "".join(sanitized)
128
129 def _reserve_name(self, base: str, is_alias: bool) -> str:
130 """Reserve a globally unique identifier using the sanitized base name."""
131 base = self._sanitize_name(base)
132 if is_alias and base in self.alias_base_names:
133 sys.stderr.write(
134 f"Warning: duplicate alias name '{base}' detected; disambiguating.\n")
135 if is_alias:
136 self.alias_base_names.add(base)
137 name = base
138 idx = 1
139 while name in self.used_names:
140 name = f"{base}_{idx}"
141 idx += 1
142 self.used_names[name] = is_alias
143 return name
144
145 def _auto_struct_name(self, struct_type: types.StructType) -> str:
146 """Derive a deterministic name for anonymous structs from their fields."""
147 parts = ["_struct"]
148 for field_name, field_type in struct_type.fields:
149 parts.append(field_name)
150 parts.append(self._sanitize_name(field_type.id))
151 return self._reserve_name("_".join(parts), is_alias=False)
152
153 def _iter_type_children(self, t: types.ESIType) -> List[types.ESIType]:
154 """Return child types in a stable order for traversal."""
155 if isinstance(t, types.TypeAlias):
156 return [t.inner_type] if t.inner_type is not None else []
157 if isinstance(t, types.BundleType):
158 return [channel.type for channel in t.channels]
159 if isinstance(t, types.ChannelType):
160 return [t.inner]
161 if isinstance(t, types.StructType):
162 return [field_type for _, field_type in t.fields]
163 if isinstance(t, types.ArrayType):
164 return [t.element_type]
165 return []
166
167 def _visit_types(self, t: types.ESIType, visited: Set[str], visit_fn) -> None:
168 """Traverse types with alphabetical child ordering in post-order."""
169 if not isinstance(t, types.ESIType):
170 raise TypeError(f"Expected ESIType, got {type(t)}")
171 tid = t.id
172 if tid in visited:
173 return
174 visited.add(tid)
175 children = sorted(self._iter_type_children(t), key=lambda child: child.id)
176 for child in children:
177 self._visit_types(child, visited, visit_fn)
178 visit_fn(t)
179
180 def _collect_aliases(self, t: types.ESIType, visited: Set[str]) -> None:
181 """Scan for aliases and reserve their names (recursive)."""
182
183 # Visit callback: reserve alias names and map aliases to identifiers.
184 def visit(alias_type: types.ESIType) -> None:
185 if not isinstance(alias_type, types.TypeAlias):
186 return
187 if alias_type not in self.type_id_map:
188 alias_name = self._reserve_name(alias_type.name, is_alias=True)
189 self.type_id_map[alias_type] = alias_name
190
191 self._visit_types(t, visited, visit)
192
193 def _collect_structs(self, t: types.ESIType, visited: Set[str]) -> None:
194 """Scan for structs needing auto-names and reserve them (recursive)."""
195
196 # Visit callback: assign auto-names to unnamed structs.
197 def visit(struct_type: types.ESIType) -> None:
198 if not isinstance(struct_type, types.StructType):
199 return
200 if struct_type in self.type_id_map:
201 return
202 struct_name = self._auto_struct_name(struct_type)
203 self.type_id_map[struct_type] = struct_name
204
205 self._visit_types(t, visited, visit)
206
208 wrapped: types.ESIType) -> Set[types.ESIType]:
209 """Collect types that require top-level declarations for a given type."""
210 deps: Set[types.ESIType] = set()
211
212 # Visit callback: collect structs and non-struct aliases used by a type.
213 def visit(current: types.ESIType) -> None:
214 if isinstance(current, types.TypeAlias):
215 inner = current.inner_type
216 if inner is not None and isinstance(inner, types.StructType):
217 deps.add(inner)
218 else:
219 deps.add(current)
220 elif isinstance(current, types.StructType):
221 deps.add(current)
222
223 self._visit_types(wrapped, set(), visit)
224 return deps
225
226 def _ordered_emit_types(self) -> Tuple[List[types.ESIType], bool]:
227 """Collect and order types for deterministic emission."""
228 emit_types: List[types.ESIType] = []
229 for esi_type in self.type_id_map.keys():
230 if isinstance(esi_type, (types.StructType, types.TypeAlias)):
231 emit_types.append(esi_type)
232
233 # Prefer alias-reserved names first, then lexicographic for determinism.
234 name_to_type = {self.type_id_map[t]: t for t in emit_types}
235 sorted_names = sorted(name_to_type.keys(),
236 key=lambda name:
237 (0 if self.used_names.get(name, False) else 1, name))
238
239 ordered: List[types.ESIType] = []
240 visited: Set[types.ESIType] = set()
241 visiting: Set[types.ESIType] = set()
242 has_cycle = False
243
244 # Visit callback: DFS to emit dependencies before their users.
245 def visit(current: types.ESIType) -> None:
246 nonlocal has_cycle
247 if current in visited:
248 return
249 if current in visiting:
250 has_cycle = True
251 return
252 visiting.add(current)
253
254 deps: Set[types.ESIType] = set()
255 if isinstance(current, types.TypeAlias):
256 inner = current.inner_type
257 if inner is not None:
258 deps.update(self._collect_decls_from_type(inner))
259 elif isinstance(current, types.StructType):
260 for _, field_type in current.fields:
261 deps.update(self._collect_decls_from_type(field_type))
262 for dep in sorted(deps, key=lambda dep: self.type_id_map[dep]):
263 visit(dep)
264
265 visiting.remove(current)
266 visited.add(current)
267 ordered.append(current)
268
269 for name in sorted_names:
270 visit(name_to_type[name])
271
272 return ordered, has_cycle
273
274
276 """Emit C++ headers from precomputed type ordering."""
277
278 def __init__(self, planner: CppTypePlanner) -> None:
279 self.type_id_map = planner.type_id_map
280 self.ordered_types = planner.ordered_types
281 self.has_cycle = planner.has_cycle
282
283 def type_identifier(self, type: types.ESIType) -> str:
284 """Get the C++ type string for an ESI type."""
285 return self._cpp_type(type)
286
287 def _get_bitvector_str(self, type: types.ESIType) -> str:
288 """Get the textual code for the storage class of an integer type."""
289 assert isinstance(type, (types.BitsType, types.IntType))
290
291 if type.bit_width == 1:
292 return "bool"
293 elif type.bit_width <= 8:
294 storage_width = 8
295 elif type.bit_width <= 16:
296 storage_width = 16
297 elif type.bit_width <= 32:
298 storage_width = 32
299 elif type.bit_width <= 64:
300 storage_width = 64
301 else:
302 raise ValueError(f"Unsupported integer width: {type.bit_width}")
303
304 if isinstance(type, (types.BitsType, types.UIntType)):
305 return f"uint{storage_width}_t"
306 return f"int{storage_width}_t"
307
309 array_type: types.ArrayType) -> Tuple[str, str]:
310 """Return the base C++ type and bracket suffix for a nested array."""
311 dims: List[int] = []
312 inner: types.ESIType = array_type
313 while isinstance(inner, types.ArrayType):
314 dims.append(inner.size)
315 inner = inner.element_type
316 base_cpp = self._cpp_type(inner)
317 suffix = "".join([f"[{d}]" for d in dims])
318 return base_cpp, suffix
319
320 def _format_array_type(self, array_type: types.ArrayType) -> str:
321 """Return the C++ type string for a nested array alias."""
322 base_cpp, suffix = self._array_base_and_suffix(array_type)
323 return f"{base_cpp}{suffix}"
324
325 def _cpp_type(self, wrapped: types.ESIType) -> str:
326 """Resolve an ESI type to its C++ identifier."""
327 if isinstance(wrapped, (types.TypeAlias, types.StructType)):
328 return self.type_id_map[wrapped]
329 if isinstance(wrapped, types.BundleType):
330 return "void"
331 if isinstance(wrapped, types.ChannelType):
332 return self._cpp_type(wrapped.inner)
333 if isinstance(wrapped, types.VoidType):
334 return "void"
335 if isinstance(wrapped, types.AnyType):
336 return "std::any"
337 if isinstance(wrapped, (types.BitsType, types.IntType)):
338 return self._get_bitvector_str(wrapped)
339 if isinstance(wrapped, types.ArrayType):
340 return self._format_array_type(wrapped)
341 if type(wrapped) is types.ESIType:
342 return "std::any"
343 raise NotImplementedError(
344 f"Type '{wrapped}' not supported for C++ generation")
345
346 def _unwrap_aliases(self, wrapped: types.ESIType) -> types.ESIType:
347 """Strip alias wrappers to reach the underlying type."""
348 while isinstance(wrapped, types.TypeAlias):
349 wrapped = wrapped.inner_type
350 return wrapped
351
352 def _format_array_decl(self, array_type: types.ArrayType, name: str) -> str:
353 """Emit a field declaration for a multi-dimensional array member.
354
355 The declaration flattens nested arrays into repeated bracketed sizes for C++.
356 """
357 base_cpp, suffix = self._array_base_and_suffix(array_type)
358 return f"{base_cpp} {name}{suffix};"
359
360 def _emit_struct(self, hdr: TextIO, struct_type: types.StructType) -> None:
361 """Emit a packed struct declaration plus its type id string."""
362 fields = list(struct_type.fields)
363 if struct_type.cpp_type.reverse:
364 fields = list(reversed(fields))
365 field_decls: List[str] = []
366 for field_name, field_type in fields:
367 if isinstance(field_type, types.ArrayType):
368 field_decls.append(self._format_array_decl(field_type, field_name))
369 else:
370 field_cpp = self._cpp_type(field_type)
371 wrapped = self._unwrap_aliases(field_type)
372 if isinstance(wrapped, (types.BitsType, types.IntType)):
373 # TODO: Bitfield layout is implementation-defined; consider
374 # byte-aligned storage with explicit pack/unpack helpers.
375 bitfield_width = wrapped.bit_width
376 if bitfield_width >= 0:
377 field_decls.append(f"{field_cpp} {field_name} : {bitfield_width};")
378 else:
379 field_decls.append(f"{field_cpp} {field_name};")
380 else:
381 field_decls.append(f"{field_cpp} {field_name};")
382 hdr.write(f"struct {self.type_id_map[struct_type]} {{\n")
383 for decl in field_decls:
384 hdr.write(f" {decl}\n")
385 hdr.write("\n")
386 hdr.write(
387 f" static constexpr std::string_view _ESI_ID = \"{struct_type.id}\";\n"
388 )
389 hdr.write("};\n\n")
390
391 def _emit_alias(self, hdr: TextIO, alias_type: types.TypeAlias) -> None:
392 """Emit a using alias when the alias targets a different C++ type."""
393 inner_wrapped = alias_type.inner_type
394 alias_name = self.type_id_map[alias_type]
395 inner_cpp = None
396 if inner_wrapped is not None:
397 inner_cpp = self._cpp_type(inner_wrapped)
398 if inner_cpp is None:
399 inner_cpp = self.type_id_map[alias_type]
400 if inner_cpp != alias_name:
401 hdr.write(f"using {alias_name} = {inner_cpp};\n\n")
402
403 def write_header(self, output_dir: Path, system_name: str) -> None:
404 """Emit the fully ordered types.h header into the output directory."""
405 hdr_file = output_dir / "types.h"
406 with open(hdr_file, "w") as hdr:
407 hdr.write(
408 textwrap.dedent(f"""
409 // Generated header for {system_name} types.
410 #pragma once
411
412 #include <cstdint>
413 #include <any>
414 #include <string_view>
415
416 namespace {system_name} {{
417 #pragma pack(push, 1)
418
419 """))
420 if self.has_cycle:
421 sys.stderr.write("Warning: cyclic type dependencies detected.\n")
422 sys.stderr.write(" Logically this should not be possible.\n")
423 sys.stderr.write(
424 " Emitted code may fail to compile due to ordering issues.\n")
425
426 for emit_type in self.ordered_types:
427 if isinstance(emit_type, types.StructType):
428 self._emit_struct(hdr, emit_type)
429 elif isinstance(emit_type, types.TypeAlias):
430 self._emit_alias(hdr, emit_type)
431
432 hdr.write(
433 textwrap.dedent(f"""
434 #pragma pack(pop)
435 }} // namespace {system_name}
436 """))
437
438
439def run(generator: Type[Generator] = CppGenerator,
440 cmdline_args=sys.argv) -> int:
441 """Create and run a generator reading options from the command line."""
442
443 argparser = argparse.ArgumentParser(
444 description=f"Generate {generator.language} headers from an ESI manifest",
445 formatter_class=argparse.RawDescriptionHelpFormatter,
446 epilog=textwrap.dedent("""
447 Can read the manifest from either a file OR a running accelerator.
448
449 Usage examples:
450 # To read the manifest from a file:
451 esi-cppgen --file /path/to/manifest.json
452
453 # To read the manifest from a running accelerator:
454 esi-cppgen --platform cosim --connection localhost:1234
455 """))
456
457 argparser.add_argument("--file",
458 type=str,
459 default=None,
460 help="Path to the manifest file.")
461 argparser.add_argument(
462 "--platform",
463 type=str,
464 help="Name of platform for live accelerator connection.")
465 argparser.add_argument(
466 "--connection",
467 type=str,
468 help="Connection string for live accelerator connection.")
469 argparser.add_argument(
470 "--output-dir",
471 type=str,
472 default="esi",
473 help="Output directory for generated files. Recommend adding either `esi`"
474 " or the system name to the end of the path so as to avoid header name"
475 "conflicts. Defaults to `esi`")
476 argparser.add_argument(
477 "--system-name",
478 type=str,
479 default="esi_system",
480 help="Name of the ESI system. For C++, this will be the namespace.")
481
482 if (len(cmdline_args) <= 1):
483 argparser.print_help()
484 return 1
485 args = argparser.parse_args(cmdline_args[1:])
486
487 if args.file is not None and args.platform is not None:
488 print("Cannot specify both --file and --platform")
489 return 1
490
491 conn: AcceleratorConnection
492 if args.file is not None:
493 conn = Context.default().connect("trace", f"-:{args.file}")
494 elif args.platform is not None:
495 if args.connection is None:
496 print("Must specify --connection with --platform")
497 return 1
498 conn = Context.default().connect(args.platform, args.connection)
499 else:
500 print("Must specify either --file or --platform")
501 return 1
502
503 output_dir = Path(args.output_dir)
504 if output_dir.exists() and not output_dir.is_dir():
505 print(f"Output directory {output_dir} is not a directory")
506 return 1
507 if not output_dir.exists():
508 output_dir.mkdir(parents=True)
509
510 gen = generator(conn)
511 gen.generate(output_dir, args.system_name)
512 return 0
513
514
515if __name__ == '__main__':
516 sys.exit(run())
static void print(TypedAttr val, llvm::raw_ostream &os)
static mlir::Operation * resolve(Context &context, mlir::SymbolRefAttr sym)
#define isdigit(x)
Definition FIRLexer.cpp:26
str get_consts_str(self, ModuleInfo module_info)
Definition codegen.py:49
__init__(self, AcceleratorConnection conn)
Definition codegen.py:44
write_modules(self, Path output_dir, str system_name)
Definition codegen.py:58
generate(self, Path output_dir, str system_name)
Definition codegen.py:79
str _format_array_type(self, types.ArrayType array_type)
Definition codegen.py:320
None _emit_struct(self, TextIO hdr, types.StructType struct_type)
Definition codegen.py:360
str _get_bitvector_str(self, types.ESIType type)
Definition codegen.py:287
None write_header(self, Path output_dir, str system_name)
Definition codegen.py:403
str _format_array_decl(self, types.ArrayType array_type, str name)
Definition codegen.py:352
None _emit_alias(self, TextIO hdr, types.TypeAlias alias_type)
Definition codegen.py:391
Tuple[str, str] _array_base_and_suffix(self, types.ArrayType array_type)
Definition codegen.py:309
None __init__(self, CppTypePlanner planner)
Definition codegen.py:278
types.ESIType _unwrap_aliases(self, types.ESIType wrapped)
Definition codegen.py:346
str type_identifier(self, types.ESIType type)
Definition codegen.py:283
str _cpp_type(self, types.ESIType wrapped)
Definition codegen.py:325
str _sanitize_name(self, str name)
Definition codegen.py:112
None _visit_types(self, types.ESIType t, Set[str] visited, visit_fn)
Definition codegen.py:167
str _reserve_name(self, str base, bool is_alias)
Definition codegen.py:129
Tuple[List[types.ESIType], bool] _ordered_emit_types(self)
Definition codegen.py:226
List[types.ESIType] _iter_type_children(self, types.ESIType t)
Definition codegen.py:153
None __init__(self, type_table)
Definition codegen.py:87
Set[types.ESIType] _collect_decls_from_type(self, types.ESIType wrapped)
Definition codegen.py:208
str _auto_struct_name(self, types.StructType struct_type)
Definition codegen.py:145
None _collect_structs(self, types.ESIType t, Set[str] visited)
Definition codegen.py:193
None _prepare_types(self, type_table)
Definition codegen.py:99
None _collect_aliases(self, types.ESIType t, Set[str] visited)
Definition codegen.py:180
generate(self, Path output_dir, str system_name)
Definition codegen.py:35
__init__(self, AcceleratorConnection conn)
Definition codegen.py:32
int run(Type[Generator] generator=CppGenerator, cmdline_args=sys.argv)
Definition codegen.py:440
"AcceleratorConnection" connect(str platform, str connection_str)
Definition __init__.py:26