mirror of
				https://github.com/YosysHQ/yosys
				synced 2025-10-31 11:42:30 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			788 lines
		
	
	
	
		
			27 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			788 lines
		
	
	
	
		
			27 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| #!/usr/bin/env python3
 | |
| #  yosys -- Yosys Open SYnthesis Suite
 | |
| #
 | |
| #  Copyright (C) 2012  Claire Xenia Wolf <claire@yosyshq.com>
 | |
| #
 | |
| #  Permission to use, copy, modify, and/or distribute this software for any
 | |
| #  purpose with or without fee is hereby granted, provided that the above
 | |
| #  copyright notice and this permission notice appear in all copies.
 | |
| #
 | |
| #  THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 | |
| #  WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 | |
| #  MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 | |
| #  ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 | |
| #  WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 | |
| #  ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 | |
| #  OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 | |
| #
 | |
| #  Written by Mohamed Gaber <me@donn.website>
 | |
| #
 | |
| #  Inspired by py_wrap_generator.py by Benedikt Tutzer
 | |
| """
 | |
| This generates:
 | |
| - Wrapper calls for opaque container types
 | |
| - Full wrappers for select classes and all enums, global functions and global
 | |
|   variables
 | |
| 
 | |
| Jump to "MARK: Inclusion and Exclusion" to control the above.
 | |
| 
 | |
| Please run ruff on this file in particular to make sure it parses with Python
 | |
| <= 3.12. There is so much f-string use here that the outer quote thing
 | |
| is a common problem. ``ruff check pyosys/generator.py`` suffices.
 | |
| """
 | |
| import os
 | |
| import io
 | |
| import shutil
 | |
| import argparse
 | |
| from pathlib import Path
 | |
| from sysconfig import get_paths
 | |
| from dataclasses import dataclass, field
 | |
| from typing import Any, Dict, FrozenSet, Iterable, Tuple, Union, Optional, List
 | |
| 
 | |
| import pybind11
 | |
| from cxxheaderparser.simple import parse_file, ClassScope, NamespaceScope, EnumDecl
 | |
| from cxxheaderparser.options import ParserOptions
 | |
| from cxxheaderparser.preprocessor import make_gcc_preprocessor
 | |
| from cxxheaderparser.types import (
 | |
|     PQName,
 | |
|     Type,
 | |
|     Pointer,
 | |
|     Reference,
 | |
|     MoveReference,
 | |
|     AnonymousName,
 | |
|     Method,
 | |
|     Function,
 | |
|     Field,
 | |
|     Variable,
 | |
|     Array,
 | |
|     FundamentalSpecifier,
 | |
| )
 | |
| 
 | |
| __file_dir__ = Path(__file__).absolute().parent
 | |
| __yosys_root__ = __file_dir__.parent
 | |
| 
 | |
| 
 | |
| @dataclass
 | |
| class PyosysClass:
 | |
|     """
 | |
|     Metadata about classes or structs intended to be wrapped using Pyosys.
 | |
| 
 | |
|     :param name: The base name of the class (without extra qualifiers)
 | |
|     :param ref_only: Whether this class can be copied to Python, or only
 | |
|         referenced.
 | |
|     :param string_expr:
 | |
|         A C++ expression that will be used for the ``__str__`` method in Python.
 | |
| 
 | |
|         The object will be available as a const reference with the identifier
 | |
|         `s`.
 | |
|     :param hash_expr:
 | |
|         A C++ expression that will be fed to ``run_hash`` to declare a
 | |
|         ``__hash__`` method for Python.
 | |
| 
 | |
|         The object will be vailable as a const reference with the identifier
 | |
|         `s`.
 | |
|     :param denylist: If specified, one or more methods can be excluded from
 | |
|         wrapping.
 | |
|     """
 | |
| 
 | |
|     name: str
 | |
|     ref_only: bool = False
 | |
| 
 | |
|     # in the format s.(method or property) (or just s)
 | |
|     string_expr: Optional[str] = None
 | |
|     hash_expr: Optional[str] = None
 | |
| 
 | |
|     denylist: FrozenSet[str] = frozenset({})
 | |
| 
 | |
| 
 | |
| @dataclass
 | |
| class PyosysHeader:
 | |
|     """
 | |
|     :param name: The name of the header, i.e., its relative path to the Yosys root
 | |
|     :param classes: A list of ``PyosysClass`` classes to be wrapped
 | |
|     :param enums: A list of enums to be wrapped
 | |
|     """
 | |
| 
 | |
|     name: str
 | |
|     classes: List[PyosysClass] = field(default_factory=lambda: [])
 | |
| 
 | |
|     def __post_init__(self):
 | |
|         self.classes_by_name = {}
 | |
|         if classes := self.classes:
 | |
|             for cls in classes:
 | |
|                 self.classes_by_name[cls.name] = cls
 | |
| 
 | |
| 
 | |
| # MARK: Inclusion and Exclusion
 | |
| global_denylist = frozenset(
 | |
|     {
 | |
|         # deprecated
 | |
|         "builtin_ff_cell_types",
 | |
|         "logv_file_error",
 | |
|         # no implementation
 | |
|         "set_verific_logging",
 | |
|         # can't bridge to python cleanly
 | |
|         ## std::regex
 | |
|         "log_warn_regexes",
 | |
|         "log_nowarn_regexes",
 | |
|         "log_werror_regexes",
 | |
|         ## function pointers
 | |
|         "log_error_atexit",
 | |
|         "log_verific_callback",
 | |
|     }
 | |
| )
 | |
| pyosys_headers = [
 | |
|     # Headers for incomplete types
 | |
|     PyosysHeader("kernel/binding.h"),
 | |
|     PyosysHeader("libs/sha1/sha1.h"),
 | |
|     # Headers for globals
 | |
|     PyosysHeader("kernel/log.h"),
 | |
|     PyosysHeader("kernel/yosys.h"),
 | |
|     PyosysHeader("kernel/cost.h"),
 | |
|     # Headers with classes
 | |
|     PyosysHeader(
 | |
|         "kernel/celltypes.h",
 | |
|         [PyosysClass("CellType", hash_expr="s.type"), PyosysClass("CellTypes")],
 | |
|     ),
 | |
|     PyosysHeader("kernel/consteval.h", [PyosysClass("ConstEval")]),
 | |
|     PyosysHeader(
 | |
|         "kernel/register.h",
 | |
|         [
 | |
|             # PyosysClass("Pass") # Virtual methods, manually bridged
 | |
|         ],
 | |
|     ),
 | |
|     PyosysHeader(
 | |
|         "kernel/rtlil.h",
 | |
|         [
 | |
|             PyosysClass(
 | |
|                 "IdString",
 | |
|                 string_expr="s.str()",
 | |
|                 hash_expr="s.str()",
 | |
|                 denylist=frozenset(
 | |
|                     # shouldn't be messed with from python in general
 | |
|                     {
 | |
|                         "global_id_storage_",
 | |
|                         "global_id_index_",
 | |
|                         "global_refcount_storage_",
 | |
|                         "global_free_idx_list_",
 | |
|                         "last_created_idx_ptr_",
 | |
|                         "last_created_idx_",
 | |
|                         "builtin_ff_cell_types",
 | |
|                     }
 | |
|                 ),
 | |
|             ),
 | |
|             PyosysClass(
 | |
|                 "Const",
 | |
|                 string_expr="s.as_string()",
 | |
|                 denylist=frozenset({"bits", "bitvectorize"}),
 | |
|             ),
 | |
|             PyosysClass("AttrObject", denylist=frozenset({"get_blackbox_attribute"})),
 | |
|             PyosysClass("NamedObject", denylist=frozenset({"get_blackbox_attribute"})),
 | |
|             PyosysClass("Selection"),
 | |
|             # PyosysClass("Monitor"), # Virtual methods, manually bridged
 | |
|             PyosysClass("CaseRule", denylist=frozenset({"get_blackbox_attribute"})),
 | |
|             PyosysClass("SwitchRule", denylist=frozenset({"get_blackbox_attribute"})),
 | |
|             PyosysClass("SyncRule"),
 | |
|             PyosysClass(
 | |
|                 "Process",
 | |
|                 ref_only=True,
 | |
|                 string_expr="s.name.c_str()",
 | |
|                 hash_expr="s.name",
 | |
|             ),
 | |
|             PyosysClass("SigChunk"),
 | |
|             PyosysClass("SigBit", hash_expr="s"),
 | |
|             PyosysClass("SigSpec", hash_expr="s"),
 | |
|             PyosysClass(
 | |
|                 "Cell",
 | |
|                 ref_only=True,
 | |
|                 string_expr="s.name.c_str()",
 | |
|                 hash_expr="s",
 | |
|             ),
 | |
|             PyosysClass(
 | |
|                 "Wire",
 | |
|                 ref_only=True,
 | |
|                 string_expr="s.name.c_str()",
 | |
|                 hash_expr="s",
 | |
|             ),
 | |
|             PyosysClass(
 | |
|                 "Memory",
 | |
|                 ref_only=True,
 | |
|                 string_expr="s.name.c_str()",
 | |
|                 hash_expr="s",
 | |
|             ),
 | |
|             PyosysClass(
 | |
|                 "Module",
 | |
|                 ref_only=True,
 | |
|                 string_expr="s.name.c_str()",
 | |
|                 hash_expr="s",
 | |
|                 denylist=frozenset({"Pow"}),  # has no implementation
 | |
|             ),
 | |
|             PyosysClass(
 | |
|                 "Design",
 | |
|                 string_expr="s.hashidx_",
 | |
|                 hash_expr="s",
 | |
|                 denylist=frozenset({"selected_whole_modules"}),  # deprecated
 | |
|             ),
 | |
|         ],
 | |
|     ),
 | |
| ]
 | |
| 
 | |
| 
 | |
| @dataclass(frozen=True)  # hashable
 | |
| class PyosysType:
 | |
|     """
 | |
|     Bit of a hacky object all-around: this is more or less used to encapsulate
 | |
|     container types so they can be later made opaque using pybind.
 | |
|     """
 | |
| 
 | |
|     base: str
 | |
|     specialization: Tuple["PyosysType", ...]
 | |
|     const: bool = False
 | |
| 
 | |
|     @classmethod
 | |
|     def from_type(Self, type_obj, drop_const=False) -> "PyosysType":
 | |
|         const = type_obj.const and not drop_const
 | |
|         if isinstance(type_obj, Pointer):
 | |
|             ptr_to = Self.from_type(type_obj.ptr_to)
 | |
|             return Self("ptr", (ptr_to,), const)
 | |
|         elif isinstance(type_obj, Reference):
 | |
|             ref_to = Self.from_type(type_obj.ref_to)
 | |
|             return Self("ref", (ref_to,), const)
 | |
|         assert isinstance(
 | |
|             type_obj, Type
 | |
|         ), f"unexpected c++ type object of type {type(type_obj)}"
 | |
|         last_segment = type_obj.typename.segments[-1]
 | |
|         base = last_segment.name
 | |
|         specialization = tuple()
 | |
|         if (
 | |
|             hasattr(last_segment, "specialization")
 | |
|             and last_segment.specialization is not None
 | |
|         ):
 | |
|             for template_arg in last_segment.specialization.args:
 | |
|                 specialization = (*specialization, Self.from_type(template_arg.arg))
 | |
|         return Self(base, specialization, const)
 | |
| 
 | |
|     def generate_identifier(self):
 | |
|         title = self.base.title()
 | |
|         if len(self.specialization) == 0:
 | |
|             return title
 | |
| 
 | |
|         if title == "Dict":
 | |
|             key, value = self.specialization
 | |
|             return f"{key.generate_identifier()}To{value.generate_identifier()}{title}"
 | |
| 
 | |
|         return (
 | |
|             "".join(spec.generate_identifier() for spec in self.specialization) + title
 | |
|         )
 | |
| 
 | |
|     def generate_cpp_name(self):
 | |
|         const_prefix = "const " * self.const
 | |
|         if len(self.specialization) == 0:
 | |
|             return const_prefix + self.base
 | |
|         elif self.base == "ptr":
 | |
|             return const_prefix + f"{self.specialization[0].generate_cpp_name()} *"
 | |
|         elif self.base == "ref":
 | |
|             return const_prefix + f"{self.specialization[0].generate_cpp_name()} &"
 | |
|         else:
 | |
|             return (
 | |
|                 const_prefix
 | |
|                 + f"{self.base}<{', '.join(spec.generate_cpp_name() for spec in self.specialization)}>"
 | |
|             )
 | |
| 
 | |
| 
 | |
| class PyosysWrapperGenerator(object):
 | |
|     def __init__(
 | |
|         self,
 | |
|         from_headers: Iterable[PyosysHeader],
 | |
|         wrapper_stream: io.TextIOWrapper,
 | |
|         header_stream: io.TextIOWrapper,
 | |
|     ):
 | |
|         self.headers = from_headers
 | |
|         self.f = wrapper_stream
 | |
|         self.f_inc = header_stream
 | |
|         self.found_containers: Dict[PyosysType, Any] = {}
 | |
|         self.class_registry: Dict[str, ClassScope] = {}
 | |
| 
 | |
|     # entry point
 | |
|     def generate(self):
 | |
|         tpl = __file_dir__ / "wrappers_tpl.cc"
 | |
|         preprocessor_opts = self.make_preprocessor_options()
 | |
|         with open(tpl, encoding="utf8") as f:
 | |
|             do_line_directive = True
 | |
|             for i, line in enumerate(f):
 | |
|                 if do_line_directive:
 | |
|                     self.f.write(f'#line {i + 1} "{tpl}"\n')
 | |
|                     do_line_directive = False
 | |
|                 if "<!-- generated includes -->" in line:
 | |
|                     for header in self.headers:
 | |
|                         self.f.write(f'#include "{header.name}"\n')
 | |
|                     do_line_directive = True
 | |
|                 elif "<!-- generated pymod-level code -->" in line:
 | |
|                     for header in self.headers:
 | |
|                         header_path = __yosys_root__ / header.name
 | |
|                         parsed = parse_file(header_path, options=preprocessor_opts)
 | |
|                         global_namespace = parsed.namespace
 | |
|                         self.process_namespace(header, global_namespace)
 | |
|                 else:
 | |
|                     self.f.write(line)
 | |
| 
 | |
|         for container, _ in self.found_containers.items():
 | |
|             identifier = container.generate_identifier()
 | |
|             print(
 | |
|                 f"using {identifier} = {container.generate_cpp_name()};",
 | |
|                 file=self.f_inc,
 | |
|             )
 | |
|             print(f"PYBIND11_MAKE_OPAQUE({identifier})", file=self.f_inc)
 | |
| 
 | |
|         print(
 | |
|             f"static void bind_autogenerated_opaque_containers(py::module &m) {{",
 | |
|             file=self.f_inc,
 | |
|         )
 | |
|         for container, _ in self.found_containers.items():
 | |
|             identifier = container.generate_identifier()
 | |
|             cxx = container.generate_cpp_name()
 | |
|             tpl_args = [cxx]
 | |
|             for spec in container.specialization:
 | |
|                 tpl_args.append(spec.generate_cpp_name())
 | |
|             print(
 | |
|                 f'\tpy::hashlib::bind_{container.base}<{", ".join(tpl_args)}>(m, "{container.generate_identifier()}");',
 | |
|                 file=self.f_inc,
 | |
|             )
 | |
|             print(
 | |
|                 f"\tpy::implicitly_convertible<py::iterable, {identifier}>();",
 | |
|                 file=self.f_inc,
 | |
|             )
 | |
|         print(f"}}", file=self.f_inc)
 | |
| 
 | |
|     # helpers
 | |
|     def make_preprocessor_options(self):
 | |
|         py_include = get_paths()["include"]
 | |
|         preprocessor_bin = shutil.which("clang++") or "g++"
 | |
|         cxx_std = os.getenv("CXX_STD", "c++17")
 | |
|         return ParserOptions(
 | |
|             preprocessor=make_gcc_preprocessor(
 | |
|                 defines=["_YOSYS_", "YOSYS_ENABLE_PYTHON"],
 | |
|                 gcc_args=[preprocessor_bin, "-fsyntax-only", f"-std={cxx_std}"],
 | |
|                 include_paths=[str(__yosys_root__), py_include, pybind11.get_include()],
 | |
|             ),
 | |
|         )
 | |
| 
 | |
|     @staticmethod
 | |
|     def find_containers(
 | |
|         containers: Iterable[str], type_info: Any
 | |
|     ) -> Dict[PyosysType, Any]:
 | |
|         if isinstance(type_info, Pointer):
 | |
|             return PyosysWrapperGenerator.find_containers(containers, type_info.ptr_to)
 | |
|         if isinstance(type_info, MoveReference):
 | |
|             return PyosysWrapperGenerator.find_containers(
 | |
|                 containers, type_info.moveref_to
 | |
|             )
 | |
|         if isinstance(type_info, Reference):
 | |
|             return PyosysWrapperGenerator.find_containers(containers, type_info.ref_to)
 | |
|         if not isinstance(type_info, Type):
 | |
|             return ()
 | |
|         segments = type_info.typename.segments
 | |
|         containers_found = {}
 | |
|         for segment in segments:
 | |
|             if isinstance(segment, FundamentalSpecifier):
 | |
|                 continue
 | |
|             if segment.name in containers:
 | |
|                 containers_found.update(
 | |
|                     {PyosysType.from_type(type_info, drop_const=True): type_info}
 | |
|                 )
 | |
|             if segment.specialization is not None:
 | |
|                 for arg in segment.specialization.args:
 | |
|                     sub_containers = PyosysWrapperGenerator.find_containers(
 | |
|                         containers, arg.arg
 | |
|                     )
 | |
|                     containers_found.update(sub_containers)
 | |
|         return containers_found
 | |
| 
 | |
|     @staticmethod
 | |
|     def find_anonymous_union(cls: ClassScope):
 | |
|         if cls.class_decl.typename.classkey != "union":
 | |
|             return None
 | |
|         for s in cls.class_decl.typename.segments:
 | |
|             if isinstance(s, AnonymousName):
 | |
|                 return s
 | |
|         return None  # named union
 | |
| 
 | |
|     @staticmethod
 | |
|     def get_parameter_types(function: Function) -> str:
 | |
|         return ", ".join(p.type.format() for p in function.parameters)
 | |
| 
 | |
|     def register_containers(self, target: Union[Function, Field, Variable]):
 | |
|         supported = ("dict", "idict", "pool", "set", "vector")
 | |
|         if isinstance(target, Function):
 | |
|             self.found_containers.update(
 | |
|                 self.find_containers(supported, target.return_type)
 | |
|             )
 | |
| 
 | |
|             for parameter in target.parameters:
 | |
|                 self.found_containers.update(
 | |
|                     self.find_containers(supported, parameter.type)
 | |
|                 )
 | |
|         else:
 | |
|             self.found_containers.update(self.find_containers(supported, target.type))
 | |
| 
 | |
|     # processors
 | |
|     def get_overload_cast(
 | |
|         self, function: Function, class_basename: Optional[str]
 | |
|     ) -> str:
 | |
|         is_method = isinstance(function, Method)
 | |
|         function_return_type = function.return_type.format()
 | |
|         if class_basename == "Const" and function_return_type in {
 | |
|             "iterator",
 | |
|             "const_iterator",
 | |
|         }:
 | |
|             # HACK: qualify Const's iterators
 | |
|             function_return_type = f"{class_basename}::{function_return_type}"
 | |
| 
 | |
|         pointer_kind = (
 | |
|             f"{class_basename}::*" if (is_method and not function.static) else "*"
 | |
|         )
 | |
| 
 | |
|         retval = f"static_cast <"
 | |
|         retval += function_return_type
 | |
|         retval += f"({pointer_kind})"
 | |
|         retval += f"({self.get_parameter_types(function)})"
 | |
|         if is_method and function.const:
 | |
|             retval += " const"
 | |
|         retval += ">"
 | |
|         retval += "(&"
 | |
|         if is_method:
 | |
|             retval += f"{class_basename}::"
 | |
|         retval += function.name.segments[-1].format()
 | |
|         retval += ")"
 | |
|         return retval
 | |
| 
 | |
|     def get_definition_args(
 | |
|         self,
 | |
|         function: Function,
 | |
|         class_basename: Optional[str],
 | |
|         python_name_override: Optional[str] = None,
 | |
|     ) -> List[str]:
 | |
|         function_basename = function.name.segments[-1].format()
 | |
| 
 | |
|         python_function_basename = python_name_override or keyword_aliases.get(
 | |
|             function_basename, function_basename
 | |
|         )
 | |
| 
 | |
|         def_args = [f'"{python_function_basename}"']
 | |
|         def_args.append(self.get_overload_cast(function, class_basename))
 | |
|         for parameter in function.parameters:
 | |
|             # ASSUMPTION: there are no unnamed parameters in the yosys codebase
 | |
|             parameter_arg = f'py::arg("{parameter.name}")'
 | |
|             if parameter.default is not None:
 | |
|                 parameter_arg += f" = {parameter.default.format()}"
 | |
|             def_args.append(parameter_arg)
 | |
| 
 | |
|         return def_args
 | |
| 
 | |
|     def process_method(self, metadata: PyosysClass, function: Method):
 | |
|         if (
 | |
|             function.deleted
 | |
|             or function.template
 | |
|             or function.vararg
 | |
|             or function.access != "public"
 | |
|             or function.pure_virtual
 | |
|             or function.destructor
 | |
|         ):
 | |
|             return
 | |
| 
 | |
|         if any(isinstance(p.type, MoveReference) for p in function.parameters):
 | |
|             # skip move constructors
 | |
|             return
 | |
| 
 | |
|         if len(function.name.segments) > 1:
 | |
|             # can't handle, skip
 | |
|             return
 | |
| 
 | |
|         if function.constructor:
 | |
|             if (
 | |
|                 not metadata.ref_only
 | |
|             ):  # ref-only classes should not be constructed from python
 | |
|                 print(
 | |
|                     f"\t\t\t.def(py::init<{self.get_parameter_types(function)}>())",
 | |
|                     file=self.f,
 | |
|                 )
 | |
|             return
 | |
| 
 | |
|         python_name_override = None
 | |
|         if function.operator is not None:
 | |
|             if function.operator == "==":
 | |
|                 python_name_override = "__eq__"
 | |
|             elif function.operator == "!=":
 | |
|                 python_name_override = "__ne__"
 | |
|             elif function.operator == "<":
 | |
|                 python_name_override = "__lt__"
 | |
|             else:
 | |
|                 return
 | |
| 
 | |
|         self.register_containers(function)
 | |
| 
 | |
|         definition_fn = "def"
 | |
|         if function.static:
 | |
|             definition_fn = "def_static"
 | |
| 
 | |
|         print(
 | |
|             f"\t\t\t.{definition_fn}({', '.join(self.get_definition_args(function, metadata.name, python_name_override))})",
 | |
|             file=self.f,
 | |
|         )
 | |
| 
 | |
|     def process_function(self, function: Function):
 | |
|         if function.deleted or function.template or function.vararg or function.static:
 | |
|             return
 | |
| 
 | |
|         if function.operator is not None:
 | |
|             # Python doesn't do global operators
 | |
|             return
 | |
| 
 | |
|         if function.name.segments[-1].format() in global_denylist:
 | |
|             return
 | |
| 
 | |
|         self.register_containers(function)
 | |
| 
 | |
|         print(
 | |
|             f"\t\t\tm.def({', '.join(self.get_definition_args(function, None))});",
 | |
|             file=self.f,
 | |
|         )
 | |
| 
 | |
|     def process_field(self, metadata: PyosysClass, field: Field):
 | |
|         if field.access != "public":
 | |
|             return
 | |
| 
 | |
|         if field.name is None:
 | |
|             # anonymous structs and unions
 | |
|             # unions handled in calling function
 | |
|             # ASSUMPTION: No anonymous structs in the yosys codebase
 | |
|             # 			  (they're not in the C++ standard anyway)
 | |
|             return
 | |
| 
 | |
|         unique_ptrs = self.find_containers(("unique_ptr",), field.type)
 | |
|         if len(unique_ptrs):
 | |
|             # TODO: figure out how to bridge unique pointers maybe does anyone
 | |
|             # care
 | |
|             return
 | |
| 
 | |
|         self.register_containers(field)
 | |
| 
 | |
|         definition_fn = f"def_{'readonly' if field.type.const else 'readwrite'}"
 | |
|         if field.static:
 | |
|             definition_fn += "_static"
 | |
| 
 | |
|         field_python_basename = keyword_aliases.get(field.name, field.name)
 | |
| 
 | |
|         print(
 | |
|             f'\t\t\t.{definition_fn}("{field_python_basename}", &{metadata.name}::{field.name})',
 | |
|             file=self.f,
 | |
|         )
 | |
| 
 | |
|     def process_variable(self, variable: Variable):
 | |
|         if isinstance(variable.type, Array):
 | |
|             return
 | |
| 
 | |
|         variable_basename = variable.name.segments[-1].format()
 | |
|         if variable_basename in global_denylist:
 | |
|             return
 | |
| 
 | |
|         self.register_containers(variable)
 | |
| 
 | |
|         definition_fn = (
 | |
|             f"def_{'readonly' if variable.type.const else 'readwrite'}_static"
 | |
|         )
 | |
| 
 | |
|         variable_python_basename = keyword_aliases.get(
 | |
|             variable_basename, variable_basename
 | |
|         )
 | |
|         variable_name = variable.name.format()
 | |
| 
 | |
|         print(
 | |
|             f'\t\t\tglobal_variables.{definition_fn}("{variable_python_basename}", &{variable_name});',
 | |
|             file=self.f,
 | |
|         )
 | |
| 
 | |
|     def process_class_members(
 | |
|         self, metadata: PyosysClass, cls: ClassScope, basename: str
 | |
|     ):
 | |
|         for method in cls.methods:
 | |
|             if method.name.segments[-1].name in metadata.denylist:
 | |
|                 continue
 | |
|             self.process_method(metadata, method)
 | |
| 
 | |
|         visited_anonymous_unions = set()
 | |
|         for field_ in cls.fields:
 | |
|             if field_.name in metadata.denylist:
 | |
|                 continue
 | |
|             self.process_field(metadata, field_)
 | |
| 
 | |
|             # Handle anonymous unions
 | |
|             for subclass in cls.classes:
 | |
|                 if subclass.class_decl.access != "public":
 | |
|                     continue
 | |
|                 if au := self.find_anonymous_union(subclass):
 | |
|                     if au.id in visited_anonymous_unions:
 | |
|                         continue
 | |
|                     visited_anonymous_unions.add(au.id)
 | |
|                     for subfield in subclass.fields:
 | |
|                         self.process_field(metadata, subfield)
 | |
| 
 | |
|     def process_class(
 | |
|         self,
 | |
|         metadata: PyosysClass,
 | |
|         cls: ClassScope,
 | |
|         namespace_components: Tuple[str, ...],
 | |
|     ):
 | |
|         pqname: PQName = cls.class_decl.typename
 | |
|         full_path = list(namespace_components) + [
 | |
|             segment.format() for segment in pqname.segments
 | |
|         ]
 | |
|         basename = full_path.pop()
 | |
|         self.class_registry[basename] = cls
 | |
| 
 | |
|         declaration_namespace = "::".join(full_path)
 | |
|         tpl_args = [basename]
 | |
|         if metadata.ref_only:
 | |
|             tpl_args.append(f"std::unique_ptr<{basename}, py::nodelete>")
 | |
|         print(
 | |
|             f'\t\t{{using namespace {declaration_namespace}; py::class_<{", ".join(tpl_args)}>(m, "{basename}")',
 | |
|             file=self.f,
 | |
|         )
 | |
| 
 | |
|         self.process_class_members(metadata, cls, basename)
 | |
|         for base in cls.class_decl.bases:
 | |
|             if base.access != "public":
 | |
|                 continue
 | |
|             name = base.typename.segments[-1].format()
 | |
|             if base_scope := self.class_registry.get(name):
 | |
|                 self.process_class_members(metadata, base_scope, basename)
 | |
| 
 | |
|         if expr := metadata.string_expr:
 | |
|             print(
 | |
|                 f'\t\t.def("__str__", [](const {basename} &s) {{ return {expr}; }})',
 | |
|                 file=self.f,
 | |
|             )
 | |
| 
 | |
|         if expr := metadata.hash_expr:
 | |
|             print(
 | |
|                 f'\t\t.def("__hash__", [](const {basename} &s) {{ return run_hash({expr}); }})',
 | |
|                 file=self.f,
 | |
|             )
 | |
| 
 | |
|         print(f"\t\t;}}", file=self.f)
 | |
| 
 | |
|     def process_enum(
 | |
|         self,
 | |
|         enum: EnumDecl,
 | |
|         namespace_components: Tuple[str, ...],
 | |
|     ):
 | |
|         pqname: PQName = enum.typename
 | |
|         full_path = list(namespace_components) + [
 | |
|             segment.format() for segment in pqname.segments
 | |
|         ]
 | |
|         basename = full_path.pop()
 | |
| 
 | |
|         declaration_namespace = "::".join(full_path)
 | |
|         print(
 | |
|             f'\t\t{{using namespace {declaration_namespace}; py::native_enum<{basename}>(m, "{basename}", "enum.Enum")',
 | |
|             file=self.f,
 | |
|         )
 | |
|         enum_class = enum.typename.classkey == "enum class"
 | |
|         for value in enum.values:
 | |
|             enum_class_qualifier = f"{basename}::" * enum_class
 | |
|             print(
 | |
|                 f'\t\t\t.value("{value.name}", {enum_class_qualifier}{value.name})',
 | |
|                 file=self.f,
 | |
|             )
 | |
|         print(f"\t\t\t.finalize();}}", file=self.f)
 | |
| 
 | |
|     def process_namespace(
 | |
|         self,
 | |
|         header: PyosysHeader,
 | |
|         ns: NamespaceScope,
 | |
|         namespace_components: Tuple[str, ...] = (),
 | |
|     ):
 | |
|         for name, subns in ns.namespaces.items():
 | |
|             self.process_namespace(header, subns, (*namespace_components, name))
 | |
|         if len(namespace_components) and (len(ns.functions) + len(ns.variables)):
 | |
|             # TODO: Not essential but maybe move namespace usage into
 | |
|             # process_function for consistency?
 | |
|             print(
 | |
|                 f"\t\t{{ using namespace {'::'.join(namespace_components)};",
 | |
|                 file=self.f,
 | |
|             )
 | |
|             for function in ns.functions:
 | |
|                 self.process_function(function)
 | |
|             for variable in ns.variables:
 | |
|                 self.process_variable(variable)
 | |
|             print(f"\t\t}}", file=self.f)
 | |
|         for enum in ns.enums:
 | |
|             self.process_enum(enum, namespace_components)
 | |
|         for cls in ns.classes:
 | |
|             pqname = cls.class_decl.typename
 | |
|             declared_name_str = pqname.segments[-1].format()
 | |
|             if cls_metadata := header.classes_by_name.get(declared_name_str):
 | |
|                 self.process_class(cls_metadata, cls, namespace_components)
 | |
| 
 | |
| 
 | |
| keyword_aliases = {
 | |
|     "False": "False_",
 | |
|     "None": "None_",
 | |
|     "True": "True_",
 | |
|     "and": "and_",
 | |
|     "as": "as_",
 | |
|     "assert": "assert_",
 | |
|     "break": "break_",
 | |
|     "class": "class_",
 | |
|     "continue": "continue_",
 | |
|     "def": "def_",
 | |
|     "del": "del_",
 | |
|     "elif": "elif_",
 | |
|     "else": "else_",
 | |
|     "except": "except_",
 | |
|     "for": "for_",
 | |
|     "from": "from_",
 | |
|     "global": "global_",
 | |
|     "if": "if_",
 | |
|     "import": "import_",
 | |
|     "in": "in_",
 | |
|     "is": "is_",
 | |
|     "lambda": "lambda_",
 | |
|     "nonlocal": "nonlocal_",
 | |
|     "not": "not_",
 | |
|     "or": "or_",
 | |
|     "pass": "pass_",
 | |
|     "raise": "raise_",
 | |
|     "return": "return_",
 | |
|     "try": "try_",
 | |
|     "while": "while_",
 | |
|     "with": "with_",
 | |
|     "yield": "yield_",
 | |
| }
 | |
| 
 | |
| 
 | |
| def print_includes():
 | |
|     for header in pyosys_headers:
 | |
|         print(header.name)
 | |
| 
 | |
| 
 | |
| if __name__ == "__main__":
 | |
|     ap = argparse.ArgumentParser()
 | |
|     ap.add_argument("--debug", default=0, type=int)
 | |
|     group = ap.add_mutually_exclusive_group(required=True)
 | |
|     group.add_argument("--print-includes", action="store_true")
 | |
|     group.add_argument("output", nargs="?")
 | |
|     ns = ap.parse_args()
 | |
|     if ns.print_includes:
 | |
|         print_includes()
 | |
|         exit(0)
 | |
| 
 | |
|     out_path = Path(ns.output)
 | |
|     out_inc = out_path.parent / (out_path.stem + ".inc.cc")
 | |
|     with open(out_path, "w", encoding="utf8") as f, open(
 | |
|         out_inc, "w", encoding="utf8"
 | |
|     ) as inc_f:
 | |
|         generator = PyosysWrapperGenerator(
 | |
|             from_headers=pyosys_headers, wrapper_stream=f, header_stream=inc_f
 | |
|         )
 | |
|         generator.generate()
 |