mirror of
https://github.com/YosysHQ/yosys
synced 2025-10-08 17:01:57 +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()
|