# # This file is distributed under the MIT License. See LICENSE.md for details. # import graphlib from collections import defaultdict from typing import Dict, Optional from jinja2 import Environment from tuple_tree_generator.schema import Definition, EnumDefinition, ReferenceDefinition from tuple_tree_generator.schema import ScalarDefinition, Schema, SequenceDefinition from tuple_tree_generator.schema import SequenceStructField, SimpleStructField, StructDefinition from tuple_tree_generator.schema import StructField, UpcastableDefinition from .jinja_utils import int_re, loader class CppGenerator: def __init__( self, schema: Schema, base_namespace: str, emit_tracking: bool, string_types=[], user_include_path: Optional[str] = None, ): self.schema = schema self.string_types = string_types self.emit_tracking = emit_tracking self.base_namespace = base_namespace self.generated_namespace = f"{base_namespace}::generated" if not user_include_path: user_include_path = "" elif not user_include_path.endswith("/"): user_include_path = user_include_path + "/" self.environment = Environment( block_start_string="/**", block_end_string="**/", variable_start_string="/*=", variable_end_string="=*/", comment_start_string="/*#", comment_end_string="#*/", loader=loader, ) # More convenient than escaping the double braces self.environment.filters["docstring"] = self.render_docstring self.environment.filters["field_type"] = self.field_type self.environment.filters["get_default_value"] = self.get_default_value self.environment.filters["fullname"] = self.fullname self.environment.filters["user_fullname"] = self.user_fullname self.environment.filters["is_struct_field"] = self.is_struct_field self.environment.filters["enum_underlying_type"] = self.enum_underlying_type self.enum_template = self.environment.get_template("enum.h.tpl") self.struct_template = self.environment.get_template("struct.h.tpl") self.struct_late_template = self.environment.get_template("struct_late.h.tpl") self.struct_impl_template = self.environment.get_template("struct_impl.cpp.tpl") self.struct_forward_decls_template = self.environment.get_template( "struct_forward_decls.h.tpl" ) self.class_forward_decls_template = self.environment.get_template( "class_forward_decls.h.tpl" ) # Path where user-provided headers are assumed to be located # Prepended to include statements (e.g. #include "/Class.h") self.user_include_path = user_include_path self.depgraph = self._build_depgraph() self._inverse_depgraph = self._build_inverse_depgraph() def emit(self) -> Dict[str, str]: sources = { "ForwardDecls.h": self._emit_forward_decls(), } early_definitions = self._emit_early_type_definitions() late_definitions = self._emit_late_type_definitions() impl_definitions = self._emit_impl() sources.update(early_definitions) sources.update(late_definitions) sources.update(impl_definitions) return sources def _build_depgraph(self): depgraph = {} for definition in self.schema.definitions.values(): depgraph[definition.name] = set() for dependency_name in definition.dependencies: dependency_type = self.schema.get_definition_for(dependency_name) if dependency_type: depgraph[definition.name].add(dependency_type.name) return depgraph def _build_inverse_depgraph(self): inverse_depgraph = defaultdict(set) for definition in self.schema.definitions.values(): for dependency in definition.dependencies: inverse_depgraph[dependency].add(definition.name) return inverse_depgraph def _compute_includes(self, type_to_emit): # Register which headers we need # We assume that the headers will always be in the 'Generated' # folder, relative to the user_include_path includes = set() includes.add("Generated/ForwardDecls.h") for dep in type_to_emit.dependencies: dep_definition = self.schema.get_definition_for(dep) if dep_definition and not isinstance(dep_definition, ScalarDefinition): if not dep_definition.autogenerated: includes.add(f"{dep_definition.name}.h") else: includes.add(f"Generated/Early/{dep_definition.name}.h") return includes def _emit_early_type_definitions(self): definitions = {} toposorter = graphlib.TopologicalSorter(self.depgraph) order = list(toposorter.static_order()) for type_name_to_emit in order: type_to_emit = self.schema.get_definition_for(type_name_to_emit) if not type_to_emit: # Should we emit a warning here? continue filename = f"Early/{type_to_emit.name}.h" assert filename not in definitions if isinstance(type_to_emit, StructDefinition): upcastable_types = self.schema.get_upcastable_types(type_to_emit) includes = self._compute_includes(type_to_emit) definition = self.struct_template.render( struct=type_to_emit, base_namespace=self.base_namespace, upcastable=upcastable_types, user_include_path=self.user_include_path, includes=includes, emit_tracking=self.emit_tracking, root_type=self.schema.root_type, version=self.schema.version, ) elif isinstance(type_to_emit, EnumDefinition): definition = self.enum_template.render( enum=type_to_emit, base_namespace=self.base_namespace ) elif isinstance(type_to_emit, ScalarDefinition): definition = None else: raise ValueError() if definition is not None: definitions[filename] = definition return definitions def _emit_late_type_definitions(self): all_known_types = set() for struct in self.schema.struct_definitions(): all_known_types.add(self.user_fullname(struct)) for field in struct.fields: all_known_types.add(self._cpp_type(field.resolved_type)) if isinstance(field, SequenceStructField): all_known_types.add(self._cpp_type(field.resolved_element_type)) definitions = {} for type_to_emit in self.schema.definitions.values(): filename = f"Late/{type_to_emit.name}.h" assert filename not in definitions if isinstance(type_to_emit, StructDefinition): upcastable_types = self.schema.get_upcastable_types(type_to_emit) definition = self.struct_late_template.render( struct=type_to_emit, upcastable=upcastable_types, schema=self.schema, user_include_path=self.user_include_path, root_type=self.schema.root_type, namespace=self.generated_namespace, all_types=all_known_types, base_namespace=self.base_namespace, emit_tracking=self.emit_tracking, ) elif isinstance(type_to_emit, EnumDefinition): definition = "" elif isinstance(type_to_emit, ScalarDefinition): definition = None else: raise ValueError() if definition is not None: definitions[filename] = definition return definitions def _emit_impl(self): definition = "" for type_to_emit in self.schema.definitions.values(): if isinstance(type_to_emit, StructDefinition): upcastable_types = self.schema.get_upcastable_types(type_to_emit) definition += ( self.struct_impl_template.render( struct=type_to_emit, upcastable=upcastable_types, user_include_path=self.user_include_path, schema=self.schema, root_type=self.schema.root_type, base_namespace=self.base_namespace, emit_tracking=self.emit_tracking, ) + "\n" ) elif isinstance(type_to_emit, (EnumDefinition, ScalarDefinition)): pass else: raise ValueError() return {"Impl.cpp": definition} def _emit_forward_decls(self): generated_ns_to_names = defaultdict(set) user_ns_to_names = defaultdict(set) generated_ns_to_names = defaultdict(set) for definition in self.schema.struct_definitions(): # Forward declaration for autogenerated class generated_ns_to_names[self.generated_namespace].add(definition.name) # Forward declaration for user-defined derived class user_ns_to_names[self.base_namespace].add(definition.name) abstract_names = [ definition.name for definition in self.schema.struct_definitions() if definition.abstract ] return "\n".join( [ self.struct_forward_decls_template.render(ns_to_names=generated_ns_to_names), self.class_forward_decls_template.render( ns_to_names=user_ns_to_names, abstract_names=abstract_names ), ] ) def _cpp_type(self, definition: Definition): assert isinstance(definition, Definition) if isinstance(definition, StructDefinition): return f"{self.base_namespace}::{definition.name}" elif isinstance(definition, SequenceDefinition): if definition.sequence_type == "SortedVector" and self.emit_tracking: return f"TrackingSortedVector<{self._cpp_type(definition.element_type)}>" if definition.sequence_type == "MutableSet" and self.emit_tracking: return f"TrackingMutableSet<{self._cpp_type(definition.element_type)}>" return f"{definition.sequence_type}<{self._cpp_type(definition.element_type)}>" elif isinstance(definition, EnumDefinition): return f"{self.base_namespace}::{definition.name}::Values" elif isinstance(definition, ScalarDefinition): if definition.name == "string": return "std::string" return definition.name elif isinstance(definition, ReferenceDefinition): root_type = self._cpp_type(definition.root) return f"TupleTreeReference<{self._cpp_type(definition.pointee)}, {root_type}>" elif isinstance(definition, UpcastableDefinition): return f"UpcastablePointer<{self._cpp_type(definition.base)}>" else: assert False def field_type(self, field: StructField): assert field.resolved_type is not None return self._cpp_type(field.resolved_type) def fullname(self, resolved_type: Definition): if isinstance(resolved_type, StructDefinition): return f"{self.generated_namespace}::{resolved_type.name}" elif isinstance(resolved_type, EnumDefinition): return f"{self.base_namespace}::{resolved_type.name}::Values" else: raise ValueError(resolved_type) def user_fullname(self, resolved_type: Definition): if isinstance(resolved_type, StructDefinition): return f"{self.base_namespace}::{resolved_type.name}" elif isinstance(resolved_type, EnumDefinition): return f"{self.base_namespace}::{resolved_type.name}::Values" else: raise ValueError(resolved_type) @staticmethod def is_struct_field(field: StructField): return isinstance(field, StructField) @staticmethod def render_docstring(docstr: str): if not docstr: return "" rendered_docstring = "\n".join(f"/// {line}" for line in docstr.splitlines()) if not rendered_docstring.endswith("\n"): rendered_docstring = rendered_docstring + "\n" return rendered_docstring def get_default_value(self, field: StructField): if isinstance(field.resolved_type, SequenceDefinition): return f"{self.field_type(field)}()" elif isinstance(field.resolved_type, ReferenceDefinition): return "{}" elif isinstance(field, SimpleStructField): if field.type == "string" and field.default: assert isinstance(field.default, str) return f'"{field.default}"' elif field.type in self.string_types and field.default: assert isinstance(field.default, str) return f'{self.field_type(field)}::fromString("{field.default}")' elif field.type == "bool": assert not field.default or isinstance(field.default, bool) return "true" if field.default else "false" elif int_re.match(field.type): assert not field.default or isinstance(field.default, int) return f"{field.default if field.default else 0}" elif isinstance(field.resolved_type, EnumDefinition): assert not field.default or isinstance(field.default, str) return ( f"{self.base_namespace}::{field.resolved_type.name}::" f"{field.default if field.default else "Invalid"}" ) assert not field.default, ( "Currently `default:` is only allowed on simple types: " "integers, booleans, strings and enums." ) if isinstance(field, SimpleStructField): return f"{self.field_type(field)}()" else: raise ValueError() @staticmethod def enum_underlying_type(enum: EnumDefinition): # The 2 accounts for `Invalid` and `Count` total_members = len(enum.members) + 2 for size in (8, 16, 32, 64): if total_members <= 2**size: return f"uint{size}_t" else: raise ValueError("Enum too big")