mirror of
https://github.com/YosysHQ/yosys
synced 2025-10-09 01:11:58 +00:00
For consistency. Also trying a new thing: only rebuilding objects that use the pybind11 library. The idea is these are the only objects that include the Python/pybind headers and thus the only ones that depend on the Python ABI in any capacity, so other objects can be reused across wheel builds. This has the potential to cut down build times.
746 lines
26 KiB
Python
746 lines
26 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
|
|
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
|
|
import argparse
|
|
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",
|
|
# 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",
|
|
ref_only=True,
|
|
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"}}", 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, function: Method, class_basename: str):
|
|
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:
|
|
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, class_basename, 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, field: Field, class_basename: str):
|
|
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}", &{class_basename}::{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(method, basename)
|
|
|
|
visited_anonymous_unions = set()
|
|
for field_ in cls.fields:
|
|
if field_.name in metadata.denylist:
|
|
continue
|
|
self.process_field(field_, basename)
|
|
|
|
# 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(subfield, basename)
|
|
|
|
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()
|