3
0
Fork 0
mirror of https://github.com/YosysHQ/yosys synced 2025-10-09 01:11:58 +00:00
yosys/pyosys/generator.py
Mohamed Gaber 447a6cb3f0
misc: WITH_PYTHON -> YOSYS_ENABLE_PYTHON
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.
2025-10-03 11:54:44 +03:00

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()