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