From 88be7283532237b71c1777a9953bb1aab260321a Mon Sep 17 00:00:00 2001 From: Mohamed Gaber Date: Sun, 21 Sep 2025 22:36:27 +0300 Subject: [PATCH 1/9] pyosys: rewrite using pybind11 - Rewrite all Python features to use the pybind11 library instead of boost::python. Unlike boost::python, pybind11 is a header-only library that is just included by Pyosys code, saving a lot of compile time on wheels. - Factor out as much "translation" code from the generator into proper C++ files - Fix running the embedded interpreter not supporting "from pyosys import libyosys as ys" like wheels - Move Python-related elements to `pyosys` directory at the root of the repo - Slight shift in bridging semantics: - Containers are declared as "opaque types" and are passed by reference to Python - many methods have been implemented to make them feel right at home without the overhead/ambiguity of copying to Python and then copying back after mutation - Monitor/Pass use "trampoline" pattern to support virual methods overridable in Python: virtual methods no longer require `py_` prefix - Create really short test set for pyosys that just exercises basic functionality --- .github/workflows/wheels.yml | 14 +- .github/workflows/wheels/cibw_before_build.sh | 21 - CODEOWNERS | 3 +- Makefile | 30 +- docs/source/yosys_internals/hashing.rst | 2 +- examples/python-api/pass.py | 22 +- kernel/driver.cc | 29 +- kernel/yosys.cc | 25 +- kernel/yosys_common.h | 3 + misc/py_wrap_generator.py | 2549 ----------------- passes/cmds/plugin.cc | 34 +- pyosys/.gitignore | 1 + {misc => pyosys}/__init__.py | 1 + pyosys/generator.py | 2039 +++++++++++++ pyosys/hashlib.h | 275 ++ pyosys/wrappers_tpl.cc | 248 ++ pyproject.toml | 6 + setup.py | 43 +- tests/pyosys/run_tests.py | 39 + tests/pyosys/spm.cut.v.gz | Bin 0 -> 5294 bytes tests/pyosys/test_data_read.py | 45 + tests/pyosys/test_dict.py | 13 + tests/pyosys/test_idict.py | 31 + tests/pyosys/test_import.py | 3 + tests/pyosys/test_monitor.py | 22 + tests/pyosys/test_pass.py | 34 + tests/pyosys/test_script.py | 21 + 27 files changed, 2879 insertions(+), 2674 deletions(-) delete mode 100644 misc/py_wrap_generator.py create mode 100644 pyosys/.gitignore rename {misc => pyosys}/__init__.py (99%) create mode 100644 pyosys/generator.py create mode 100644 pyosys/hashlib.h create mode 100644 pyosys/wrappers_tpl.cc create mode 100644 pyproject.toml create mode 100644 tests/pyosys/run_tests.py create mode 100644 tests/pyosys/spm.cut.v.gz create mode 100644 tests/pyosys/test_data_read.py create mode 100644 tests/pyosys/test_dict.py create mode 100644 tests/pyosys/test_idict.py create mode 100644 tests/pyosys/test_import.py create mode 100644 tests/pyosys/test_monitor.py create mode 100644 tests/pyosys/test_pass.py create mode 100644 tests/pyosys/test_script.py diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 2a5b7e024..b561b5d32 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -55,11 +55,6 @@ jobs: submodules: true persist-credentials: false - uses: actions/setup-python@v5 - - name: Get Boost Source - shell: bash - run: | - mkdir -p boost - curl -L https://github.com/boostorg/boost/releases/download/boost-1.86.0/boost-1.86.0-b2-nodocs.tar.gz | tar --strip-components=1 -xzC boost - name: Get FFI shell: bash run: | @@ -103,21 +98,16 @@ jobs: CIBW_BEFORE_ALL: bash ./.github/workflows/wheels/cibw_before_all.sh CIBW_ENVIRONMENT: > OPTFLAGS=-O3 - CXXFLAGS=-I./boost/pfx/include - LINKFLAGS=-L./boost/pfx/lib PKG_CONFIG_PATH=./ffi/pfx/lib/pkgconfig - makeFlags='BOOST_PYTHON_LIB=./boost/pfx/lib/libboost_python*.a' PATH="$PWD/bison/src:$PATH" CIBW_ENVIRONMENT_MACOS: > OPTFLAGS=-O3 - CXXFLAGS=-I./boost/pfx/include - LINKFLAGS=-L./boost/pfx/lib PKG_CONFIG_PATH=./ffi/pfx/lib/pkgconfig MACOSX_DEPLOYMENT_TARGET=11 - makeFlags='BOOST_PYTHON_LIB=./boost/pfx/lib/libboost_python*.a CONFIG=clang' + makeFlags='CONFIG=clang' PATH="$PWD/bison/src:$PATH" CIBW_BEFORE_BUILD: bash ./.github/workflows/wheels/cibw_before_build.sh - CIBW_TEST_COMMAND: python3 {project}/tests/arch/ecp5/add_sub.py + CIBW_TEST_COMMAND: python3 {project}/tests/pyosys/run_tests.py python3 - uses: actions/upload-artifact@v4 with: name: python-wheels-${{ matrix.os.runner }} diff --git a/.github/workflows/wheels/cibw_before_build.sh b/.github/workflows/wheels/cibw_before_build.sh index 018b0e0df..4e81688d0 100644 --- a/.github/workflows/wheels/cibw_before_build.sh +++ b/.github/workflows/wheels/cibw_before_build.sh @@ -11,24 +11,3 @@ if [ "$(uname)" != "Linux" ]; then fi python3 --version python3-config --includes - -# Build boost -cd ./boost -## Delete the artefacts from previous builds (if any) -rm -rf ./pfx -## Bootstrap bjam -./bootstrap.sh --prefix=./pfx -## Build Boost against current version of Python, only for -## static linkage (Boost is statically linked because system boost packages -## wildly vary in versions, including the libboost_python3 version) -./b2\ - -j$(getconf _NPROCESSORS_ONLN 2>/dev/null || sysctl -n hw.ncpu)\ - --prefix=./pfx\ - --with-filesystem\ - --with-system\ - --with-python\ - cxxflags="$(python3-config --includes) -std=c++17 -fPIC"\ - cflags="$(python3-config --includes) -fPIC"\ - link=static\ - variant=release\ - install diff --git a/CODEOWNERS b/CODEOWNERS index 46d37ad2f..4617c39bb 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -38,7 +38,8 @@ techlibs/gowin/ @pepijndevos techlibs/gatemate/ @pu-cc # pyosys -misc/*.py @btut +pyosys/* @donn +setup.py @donn backends/firrtl @ucbjrl @azidar diff --git a/Makefile b/Makefile index ca589ff91..c9cbd0810 100644 --- a/Makefile +++ b/Makefile @@ -133,10 +133,6 @@ LINKFLAGS += -rdynamic ifneq ($(shell :; command -v brew),) BREW_PREFIX := $(shell brew --prefix)/opt $(info $$BREW_PREFIX is [${BREW_PREFIX}]) -ifeq ($(ENABLE_PYOSYS),1) -CXXFLAGS += -I$(BREW_PREFIX)/boost/include -LINKFLAGS += -L$(BREW_PREFIX)/boost/lib -L$(BREW_PREFIX)/boost-python3/lib -endif CXXFLAGS += -I$(BREW_PREFIX)/readline/include -I$(BREW_PREFIX)/flex/include LINKFLAGS += -L$(BREW_PREFIX)/readline/lib -L$(BREW_PREFIX)/flex/lib PKG_CONFIG_PATH := $(BREW_PREFIX)/libffi/lib/pkgconfig:$(PKG_CONFIG_PATH) @@ -353,26 +349,14 @@ ifeq ($(ENABLE_PYOSYS),1) LINKFLAGS += $(filter-out -l%,$(shell $(PYTHON_CONFIG) --ldflags)) LIBS += $(shell $(PYTHON_CONFIG) --libs) EXE_LIBS += $(filter-out $(LIBS),$(shell $(PYTHON_CONFIG_FOR_EXE) --libs)) +PYBIND11_INCLUDE ?= $(shell $(PYTHON_EXECUTABLE) -m pybind11 --includes) +CXXFLAGS += -I$(PYBIND11_INCLUDE) -DWITH_PYTHON CXXFLAGS += $(shell $(PYTHON_CONFIG) --includes) -DWITH_PYTHON -# Detect name of boost_python library. Some distros use boost_python-py, other boost_python, some only use the major version number, some a concatenation of major and minor version numbers -CHECK_BOOST_PYTHON = (echo "int main(int argc, char ** argv) {return 0;}" | $(CXX) -xc -o /dev/null $(LINKFLAGS) $(EXE_LIBS) $(LIBS) -l$(1) - > /dev/null 2>&1 && echo "-l$(1)") -BOOST_PYTHON_LIB ?= $(shell \ - $(call CHECK_BOOST_PYTHON,boost_python-py$(subst .,,$(PYTHON_VERSION))) || \ - $(call CHECK_BOOST_PYTHON,boost_python-py$(PYTHON_MAJOR_VERSION)) || \ - $(call CHECK_BOOST_PYTHON,boost_python$(subst .,,$(PYTHON_VERSION))) || \ - $(call CHECK_BOOST_PYTHON,boost_python$(PYTHON_MAJOR_VERSION)) \ -) - -ifeq ($(BOOST_PYTHON_LIB),) -$(error BOOST_PYTHON_LIB could not be detected. Please define manually) -endif - -LIBS += $(BOOST_PYTHON_LIB) -lboost_system -lboost_filesystem -PY_WRAPPER_FILE = kernel/python_wrappers +PY_WRAPPER_FILE = pyosys/wrappers OBJS += $(PY_WRAPPER_FILE).o -PY_GEN_SCRIPT= py_wrap_generator -PY_WRAP_INCLUDES := $(shell $(PYTHON_EXECUTABLE) -c "from misc import $(PY_GEN_SCRIPT); $(PY_GEN_SCRIPT).print_includes()") +PY_GEN_SCRIPT = pyosys/generator.py +PY_WRAP_INCLUDES := $(shell $(PYTHON_EXECUTABLE) $(PY_GEN_SCRIPT) --print-includes) endif # ENABLE_PYOSYS ifeq ($(ENABLE_READLINE),1) @@ -779,9 +763,9 @@ endif $(P) cat $< | grep -E -v "#[ ]*(include|error)" | $(CXX) $(CXXFLAGS) -x c++ -o $@ -E -P - ifeq ($(ENABLE_PYOSYS),1) -$(PY_WRAPPER_FILE).cc: misc/$(PY_GEN_SCRIPT).py $(PY_WRAP_INCLUDES) +$(PY_WRAPPER_FILE).cc: $(PY_GEN_SCRIPT) pyosys/wrappers_tpl.cc $(PY_WRAP_INCLUDES) pyosys/hashlib.h $(Q) mkdir -p $(dir $@) - $(P) $(PYTHON_EXECUTABLE) -c "from misc import $(PY_GEN_SCRIPT); $(PY_GEN_SCRIPT).gen_wrappers(\"$(PY_WRAPPER_FILE).cc\")" + $(P) $(PYTHON_EXECUTABLE) $(PY_GEN_SCRIPT) $(PY_WRAPPER_FILE).cc endif %.o: %.cpp diff --git a/docs/source/yosys_internals/hashing.rst b/docs/source/yosys_internals/hashing.rst index 1993e617a..1255f6f7c 100644 --- a/docs/source/yosys_internals/hashing.rst +++ b/docs/source/yosys_internals/hashing.rst @@ -36,7 +36,7 @@ The main characteristics are: all compilers, standard libraries and architectures. In addition to ``dict`` and ``pool`` there is also an ``idict`` that -creates a bijective map from ``K`` to the integers. For example: +creates a bijective map from ``K`` to incrementing integers. For example: :: diff --git a/examples/python-api/pass.py b/examples/python-api/pass.py index dbef0a13f..d87e09078 100755 --- a/examples/python-api/pass.py +++ b/examples/python-api/pass.py @@ -1,22 +1,25 @@ #!/usr/bin/python3 -import libyosys as ys +from pyosys import libyosys as ys + +from pathlib import Path import matplotlib.pyplot as plt -import numpy as np + +__file_dir__ = Path(__file__).absolute().parent class CellStatsPass(ys.Pass): def __init__(self): super().__init__("cell_stats", "Shows cell stats as plot") - def py_help(self): + def help(self): ys.log("This pass uses the matplotlib library to display cell stats\n") - def py_execute(self, args, design): + def execute(self, args, design): ys.log_header(design, "Plotting cell stats\n") cell_stats = {} - for module in design.selected_whole_modules_warn(): + for module in design.all_selected_whole_modules(): for cell in module.selected_cells(): if cell.type.str() in cell_stats: cell_stats[cell.type.str()] += 1 @@ -29,4 +32,11 @@ class CellStatsPass(ys.Pass): def py_clear_flags(self): ys.log("Clear Flags - CellStatsPass\n") -p = CellStatsPass() +p = CellStatsPass() # register + +if __name__ == "__main__": + design = ys.Design() + ys.run_pass(f"read_verilog {__file_dir__.parents[1] / 'tests' / 'simple' / 'fiedler-cooley.v'}", design) + ys.run_pass("prep", design) + ys.run_pass("opt -full", design) + ys.run_pass("cell_stats", design) diff --git a/kernel/driver.cc b/kernel/driver.cc index bbe4e46f3..a27c0a00f 100644 --- a/kernel/driver.cc +++ b/kernel/driver.cc @@ -92,8 +92,9 @@ int main(int argc, char **argv) yosys_banner(); yosys_setup(); #ifdef WITH_PYTHON - PyRun_SimpleString(("sys.path.append(\""+proc_self_dirname()+"\")").c_str()); - PyRun_SimpleString(("sys.path.append(\""+proc_share_dirname()+"plugins\")").c_str()); + py::object sys = py::module_::import("sys"); + sys.attr("path").attr("append")(proc_self_dirname()); + sys.attr("path").attr("append")(proc_share_dirname()); #endif if (argc == 2) @@ -516,8 +517,9 @@ int main(int argc, char **argv) yosys_setup(); #ifdef WITH_PYTHON - PyRun_SimpleString(("sys.path.append(\""+proc_self_dirname()+"\")").c_str()); - PyRun_SimpleString(("sys.path.append(\""+proc_share_dirname()+"plugins\")").c_str()); + py::object sys = py::module_::import("sys"); + sys.attr("path").attr("append")(proc_self_dirname()); + sys.attr("path").attr("append")(proc_share_dirname()); #endif log_error_atexit = yosys_atexit; @@ -567,21 +569,18 @@ int main(int argc, char **argv) #endif } else if (scriptfile_python) { #ifdef WITH_PYTHON - PyObject *sys = PyImport_ImportModule("sys"); + py::list new_argv; int py_argc = special_args.size() + 1; - PyObject *new_argv = PyList_New(py_argc); - PyList_SetItem(new_argv, 0, PyUnicode_FromString(scriptfile.c_str())); + new_argv.append(scriptfile); for (int i = 1; i < py_argc; ++i) - PyList_SetItem(new_argv, i, PyUnicode_FromString(special_args[i - 1].c_str())); + new_argv.append(special_args[i - 1]); - PyObject *old_argv = PyObject_GetAttrString(sys, "argv"); - PyObject_SetAttrString(sys, "argv", new_argv); - Py_DECREF(old_argv); + py::setattr(sys, "argv", new_argv); - PyObject *py_path = PyUnicode_FromString(scriptfile.c_str()); - PyObject_SetAttrString(sys, "_yosys_script_path", py_path); - Py_DECREF(py_path); - PyRun_SimpleString("import os, sys; sys.path.insert(0, os.path.dirname(os.path.abspath(sys._yosys_script_path)))"); + py::object Path = py::module_::import("pathlib").attr("Path"); + py::object scriptfile_python_path = Path(scriptfile).attr("parent"); + + sys.attr("path").attr("insert")(0, py::str(scriptfile_python_path)); FILE *scriptfp = fopen(scriptfile.c_str(), "r"); if (scriptfp == nullptr) { diff --git a/kernel/yosys.cc b/kernel/yosys.cc index 95beca75c..9cab12bf6 100644 --- a/kernel/yosys.cc +++ b/kernel/yosys.cc @@ -64,13 +64,8 @@ #endif #ifdef WITH_PYTHON -#if PY_MAJOR_VERSION >= 3 -# define INIT_MODULE PyInit_libyosys - extern "C" PyObject* INIT_MODULE(); -#else -# define INIT_MODULE initlibyosys - extern "C" void INIT_MODULE(); -#endif +extern "C" PyObject* PyInit_libyosys(); +extern "C" PyObject* PyInit_pyosys(); #include #endif @@ -189,6 +184,17 @@ int run_command(const std::string &command, std::function +#include + +namespace py = pybind11; #endif #ifndef _YOSYS_ diff --git a/misc/py_wrap_generator.py b/misc/py_wrap_generator.py deleted file mode 100644 index aa1406f9b..000000000 --- a/misc/py_wrap_generator.py +++ /dev/null @@ -1,2549 +0,0 @@ -# -# yosys -- Yosys Open SYnthesis Suite -# -# Copyright (C) 2012 Claire Xenia Wolf -# -# 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. -# -# Author Benedikt Tutzer -# - -import copy - -#Map c++ operator Syntax to Python functions -wrappable_operators = { - "<" : "__lt__", - "==": "__eq__", - "!=": "__ne__", - "+" : "__add__", - "-" : "__sub__", - "*" : "__mul__", - "/" : "__div__", - "()": "__call__" - } - -#Restrict certain strings from being function names in Python -keyword_aliases = { - "in" : "in_", - "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_" - } - -#These can be used without any explicit conversion -primitive_types = ["void", "bool", "int", "double", "size_t", "std::string", - "string", "string_view", "std::string_view", "State", "char_p", "std::source_location", "source_location"] - -from enum import Enum - -#Ways to link between Python- and C++ Objects -class link_types(Enum): - global_list = 1 #Manage a global list of objects in C++, the Python - #object contains a key to find the corresponding C++ - #object and a Pointer to the object to verify it is - #still the same, making collisions unlikely to happen - ref_copy = 2 #The Python object contains a copy of the C++ object. - #The C++ object is deleted when the Python object gets - #deleted - pointer = 3 #The Python Object contains a pointer to it's C++ - #counterpart - derive = 4 #The Python-Wrapper is derived from the C++ object. - -class attr_types(Enum): - star = "*" - amp = "&" - ampamp = "&&" - default = "" - -#For source-files -class Source: - name = "" - classes = [] - - def __init__(self, name, classes): - self.name = name - self.classes = classes - -#Splits a list by the given delimiter, without splitting strings inside -#pointy-brackets (< and >) -def split_list(str_def, delim): - str_def = str_def.strip() - if len(str_def) == 0: - return [] - if str_def.count(delim) == 0: - return [str_def] - if str_def.count("<") == 0: - return str_def.split(delim) - if str_def.find("<") < str_def.find(" "): - closing = find_closing(str_def[str_def.find("<")+1:], "<", ">") + str_def.find("<") - comma = str_def[closing:].find(delim) - if comma == -1: - return [str_def] - comma = closing + comma - else: - comma = str_def.find(delim) - rest = split_list(str_def[comma+1:], delim) - ret = [str_def[:comma]] - if rest != None and len(rest) != 0: - ret.extend(rest) - return ret - -#Represents a Type -class WType: - name = "" - cont = None - attr_type = attr_types.default - - def __init__(self, name = "", cont = None, attr_type = attr_types.default): - self.name = name - self.cont = cont - self.attr_type = attr_type - - #Python type-string - def gen_text(self): - text = self.name - if self.name in enum_names: - text = enum_by_name(self.name).namespace + "::" + self.name - if self.cont != None: - return known_containers[self.name].typename - return text - - #C++ type-string - def gen_text_cpp(self): - postfix = "" - if self.attr_type == attr_types.star: - postfix = "*" - if self.name in primitive_types: - return self.name + postfix - if self.name in enum_names: - return enum_by_name(self.name).namespace + "::" + self.name + postfix - if self.name in classnames: - return class_by_name(self.name).namespace + "::" + self.name + postfix - text = self.name - if self.cont != None: - text += "<" - for a in self.cont.args: - text += a.gen_text_cpp() + ", " - text = text[:-2] - text += ">" - return text - - @staticmethod - def from_string(str_def, containing_file, line_number): - str_def = str_def.strip() - if len(str_def) == 0: - return None - str_def = str_def.replace("RTLIL::SigSig", "std::pair").replace("SigSig", "std::pair") - t = WType() - t.name = "" - t.cont = None - t.attr_type = attr_types.default - if str_def.find("<") != -1:# and str_def.find("<") < str_def.find(" "): - str_def = str_def.replace("const ", "") - - candidate = WContainer.from_string(str_def, containing_file, line_number) - if candidate == None: - return None - t.name = str_def[:str_def.find("<")] - - if t.name.count("*") + t.name.count("&") > 1: - return None - - if t.name.count("*") == 1 or str_def[0] == '*' or str_def[-1] == '*': - t.attr_type = attr_types.star - t.name = t.name.replace("*","") - elif t.name.count("&&") == 1: - t.attr_type = attr_types.ampamp - t.name = t.name.replace("&&","") - elif t.name.count("&") == 1 or str_def[0] == '&' or str_def[-1] == '&': - t.attr_type = attr_types.amp - t.name = t.name.replace("&","") - - t.cont = candidate - if(t.name not in known_containers): - return None - return t - - prefix = "" - - if str.startswith(str_def, "const "): - if "char_p" in str_def: - prefix = "const " - str_def = str_def[6:] - if str.startswith(str_def, "unsigned "): - prefix = "unsigned " + prefix - str_def = str_def[9:] - while str.startswith(str_def, "long "): - prefix= "long " + prefix - str_def = str_def[5:] - while str.startswith(str_def, "short "): - prefix = "short " + prefix - str_def = str_def[6:] - - str_def = str_def.split("::")[-1] - - if str_def.count("*") + str_def.count("&") >= 2: - return None - - if str_def.count("*") == 1: - t.attr_type = attr_types.star - str_def = str_def.replace("*","") - elif str_def.count("&&") == 1: - t.attr_type = attr_types.ampamp - str_def = str_def.replace("&&","") - elif str_def.count("&") == 1: - t.attr_type = attr_types.amp - str_def = str_def.replace("&","") - - if len(str_def) > 0 and str_def.split("::")[-1] not in primitive_types and str_def.split("::")[-1] not in classnames and str_def.split("::")[-1] not in enum_names: - return None - - if str_def.count(" ") == 0: - t.name = (prefix + str_def).replace("char_p", "char *") - t.cont = None - return t - return None - -#Represents a container-type -class WContainer: - name = "" - args = [] - - def from_string(str_def, containing_file, line_number): - if str_def == None or len(str_def) < 4: - return None - cont = WContainer() - cont.name = str_def[:str_def.find("<")] - str_def = str_def[str_def.find("<")+1:find_closing(str_def, "<", ">")] - cont.args = [] - for arg in split_list(str_def, ","): - candidate = WType.from_string(arg.strip(), containing_file, line_number) - if candidate == None: - return None - if candidate.name == "void": - return None - cont.args.append(candidate) - return cont - -#Translators between Python and C++ containers -#Base Type -class Translator: - tmp_cntr = 0 - typename = "DefaultType" - orig_name = "DefaultCpp" - - @classmethod - def gen_type(c, types): - return "\nImplement a function that outputs the c++ type of this container here\n" - - @classmethod - def translate(c, varname, types, prefix): - return "\nImplement a function translating a python container to a c++ container here\n" - - @classmethod - def translate_cpp(c, varname, types, prefix, ref): - return "\nImplement a function translating a c++ container to a python container here\n" - -#Translates list-types (vector, pool, set), that only differ in their name and -#the name of the insertion function -class PythonListTranslator(Translator): - typename = "boost::python::list" - insert_name = "Default" - - #generate the c++ type string - @classmethod - def gen_type(c, types): - text = c.orig_name + "<" - if types[0].name in primitive_types: - text += types[0].name - elif types[0].name in known_containers: - text += known_containers[types[0].name].gen_type(types[0].cont.args) - else: - text += class_by_name(types[0].name).namespace + "::" + types[0].name - if types[0].attr_type == attr_types.star: - text += "*" - text += ">" - return text - - #Generate C++ code to translate from a boost::python::list - @classmethod - def translate(c, varname, types, prefix): - text = prefix + c.gen_type(types) + " " + varname + "___tmp;" - cntr_name = "cntr_" + str(Translator.tmp_cntr) - Translator.tmp_cntr = Translator.tmp_cntr + 1 - text += prefix + "for(int " + cntr_name + " = 0; " + cntr_name + " < len(" + varname + "); " + cntr_name + "++)" - text += prefix + "{" - tmp_name = "tmp_" + str(Translator.tmp_cntr) - Translator.tmp_cntr = Translator.tmp_cntr + 1 - if types[0].name in known_containers: - text += prefix + "\t" + known_containers[types[0].name].typename + " " + tmp_name + " = boost::python::extract<" + known_containers[types[0].name].typename + ">(" + varname + "[" + cntr_name + "]);" - text += known_containers[types[0].name].translate(tmp_name, types[0].cont.args, prefix+"\t") - tmp_name = tmp_name + "___tmp" - text += prefix + "\t" + varname + "___tmp" + c.insert_name + "(" + tmp_name + ");" - elif types[0].name in classnames: - text += prefix + "\t" + types[0].name + "* " + tmp_name + " = boost::python::extract<" + types[0].name + "*>(" + varname + "[" + cntr_name + "]);" - if types[0].attr_type == attr_types.star: - text += prefix + "\t" + varname + "___tmp" + c.insert_name + "(" + tmp_name + "->get_cpp_obj());" - else: - text += prefix + "\t" + varname + "___tmp" + c.insert_name + "(*" + tmp_name + "->get_cpp_obj());" - else: - text += prefix + "\t" + types[0].name + " " + tmp_name + " = boost::python::extract<" + types[0].name + ">(" + varname + "[" + cntr_name + "]);" - text += prefix + "\t" + varname + "___tmp" + c.insert_name + "(" + tmp_name + ");" - text += prefix + "}" - return text - - #Generate C++ code to translate to a boost::python::list - @classmethod - def translate_cpp(c, varname, types, prefix, ref): - text = prefix + c.typename + " " + varname + "___tmp;" - tmp_name = "tmp_" + str(Translator.tmp_cntr) - Translator.tmp_cntr = Translator.tmp_cntr + 1 - if ref: - text += prefix + "for(auto " + tmp_name + " : *" + varname + ")" - else: - text += prefix + "for(auto " + tmp_name + " : " + varname + ")" - text += prefix + "{" - if types[0].name in classnames: - if types[0].attr_type == attr_types.star: - text += prefix + "\t" + varname + "___tmp.append(" + types[0].name + "::get_py_obj(" + tmp_name + "));" - else: - text += prefix + "\t" + varname + "___tmp.append(*" + types[0].name + "::get_py_obj(&" + tmp_name + "));" - elif types[0].name in known_containers: - text += known_containers[types[0].name].translate_cpp(tmp_name, types[0].cont.args, prefix + "\t", types[0].attr_type == attr_types.star) - text += prefix + "\t" + varname + "___tmp.append(" + tmp_name + "___tmp);" - else: - text += prefix + "\t" + varname + "___tmp.append(" + tmp_name + ");" - text += prefix + "}" - return text - -class IDictTranslator(PythonListTranslator): - typename = "boost::python::list" - orig_name = "idict" - insert_name = "" - -#Sub-type for std::set -class SetTranslator(PythonListTranslator): - insert_name = ".insert" - orig_name = "std::set" - -#Sub-type for std::vector -class VectorTranslator(PythonListTranslator): - insert_name = ".push_back" - orig_name = "std::vector" - -#Sub-type for pool -class PoolTranslator(PythonListTranslator): - insert_name = ".insert" - orig_name = "pool" - -#Sub-type for ObjRange -class ObjRangeTranslator(PythonListTranslator): - orig_name = "RTLIL::ObjRange" - -#Translates dict-types (dict, std::map), that only differ in their name and -#the name of the insertion function -class PythonDictTranslator(Translator): - typename = "boost::python::dict" - insert_name = "Default" - - @classmethod - def gen_type(c, types): - text = c.orig_name + "<" - if types[0].name in primitive_types: - text += types[0].name - elif types[0].name in known_containers: - text += known_containers[types[0].name].gen_type(types[0].cont.args) - else: - text += class_by_name(types[0].name).namespace + "::" + types[0].name - if types[0].attr_type == attr_types.star: - text += "*" - text += ", " - if types[1].name in primitive_types: - text += types[1].name - elif types[1].name in known_containers: - text += known_containers[types[1].name].gen_type(types[1].cont.args) - else: - text += class_by_name(types[1].name).namespace + "::" + types[1].name - if types[1].attr_type == attr_types.star: - text += "*" - text += ">" - return text - - #Generate c++ code to translate from a boost::python::dict - @classmethod - def translate(c, varname, types, prefix): - text = prefix + c.gen_type(types) + " " + varname + "___tmp;" - text += prefix + "boost::python::list " + varname + "_keylist = " + varname + ".keys();" - cntr_name = "cntr_" + str(Translator.tmp_cntr) - Translator.tmp_cntr = Translator.tmp_cntr + 1 - text += prefix + "for(int " + cntr_name + " = 0; " + cntr_name + " < len(" + varname + "_keylist); " + cntr_name + "++)" - text += prefix + "{" - key_tmp_name = "key_tmp_" + str(Translator.tmp_cntr) - val_tmp_name = "val_tmp_" + str(Translator.tmp_cntr) - Translator.tmp_cntr = Translator.tmp_cntr + 1 - - if types[0].name in known_containers: - text += prefix + "\t" + known_containers[types[0].name].typename + " " + key_tmp_name + " = boost::python::extract<" + known_containers[types[0].name].typename + ">(" + varname + "_keylist[ " + cntr_name + " ]);" - text += known_containers[types[0].name].translate(key_tmp_name, types[0].cont.args, prefix+"\t") - key_tmp_name = key_tmp_name + "___tmp" - elif types[0].name in classnames: - text += prefix + "\t" + types[0].name + "* " + key_tmp_name + " = boost::python::extract<" + types[0].name + "*>(" + varname + "_keylist[ " + cntr_name + " ]);" - else: - text += prefix + "\t" + types[0].name + " " + key_tmp_name + " = boost::python::extract<" + types[0].name + ">(" + varname + "_keylist[ " + cntr_name + " ]);" - - if types[1].name in known_containers: - text += prefix + "\t" + known_containers[types[1].name].typename + " " + val_tmp_name + " = boost::python::extract<" + known_containers[types[1].name].typename + ">(" + varname + "[" + varname + "_keylist[ " + cntr_name + " ]]);" - text += known_containers[types[1].name].translate(val_tmp_name, types[1].cont.args, prefix+"\t") - val_tmp_name = val_tmp_name + "___tmp" - elif types[1].name in classnames: - text += prefix + "\t" + types[1].name + "* " + val_tmp_name + " = boost::python::extract<" + types[1].name + "*>(" + varname + "[" + varname + "_keylist[ " + cntr_name + " ]]);" - else: - text += prefix + "\t" + types[1].name + " " + val_tmp_name + " = boost::python::extract<" + types[1].name + ">(" + varname + "[" + varname + "_keylist[ " + cntr_name + " ]]);" - - text += prefix + "\t" + varname + "___tmp." + c.insert_name + "(std::pair<" + types[0].gen_text_cpp() + ", " + types[1].gen_text_cpp() + ">(" - - if types[0].name not in classnames: - text += key_tmp_name - else: - if types[0].attr_type != attr_types.star: - text += "*" - text += key_tmp_name + "->get_cpp_obj()" - - text += ", " - if types[1].name not in classnames: - text += val_tmp_name - else: - if types[1].attr_type != attr_types.star: - text += "*" - text += val_tmp_name + "->get_cpp_obj()" - text += "));\n" + prefix + "}" - return text - - #Generate c++ code to translate to a boost::python::dict - @classmethod - def translate_cpp(c, varname, types, prefix, ref): - text = prefix + c.typename + " " + varname + "___tmp;" - tmp_name = "tmp_" + str(Translator.tmp_cntr) - Translator.tmp_cntr = Translator.tmp_cntr + 1 - if ref: - text += prefix + "for(auto " + tmp_name + " : *" + varname + ")" - else: - text += prefix + "for(auto " + tmp_name + " : " + varname + ")" - text += prefix + "{" - if types[1].name in known_containers: - text += prefix + "\tauto " + tmp_name + "_second = " + tmp_name + ".second;" - text += known_containers[types[1].name].translate_cpp(tmp_name + "_second", types[1].cont.args, prefix + "\t", types[1].attr_type == attr_types.star) - - if types[0].name in classnames: - text += prefix + "\t" + varname + "___tmp[" + types[0].name + "::get_py_obj(" + tmp_name + ".first)] = " - elif types[0].name not in known_containers: - text += prefix + "\t" + varname + "___tmp[" + tmp_name + ".first] = " - - if types[1].name in classnames: - if types[1].attr_type == attr_types.star: - text += types[1].name + "::get_py_obj(" + tmp_name + ".second);" - else: - text += "*" + types[1].name + "::get_py_obj(&" + tmp_name + ".second);" - elif types[1].name in known_containers: - text += tmp_name + "_second___tmp;" - else: - text += tmp_name + ".second;" - text += prefix + "}" - return text - -#Sub-type for dict -class DictTranslator(PythonDictTranslator): - insert_name = "insert" - orig_name = "dict" - -#Sub_type for std::map -class MapTranslator(PythonDictTranslator): - insert_name = "insert" - orig_name = "std::map" - -#Translator for std::pair. Derived from PythonDictTranslator because the -#gen_type function is the same (because both have two template parameters) -class TupleTranslator(PythonDictTranslator): - typename = "boost::python::tuple" - orig_name = "std::pair" - - #Generate c++ code to translate from a boost::python::tuple - @classmethod - def translate(c, varname, types, prefix): - text = prefix + types[0].name + " " + varname + "___tmp_0 = boost::python::extract<" + types[0].name + ">(" + varname + "[0]);" - text += prefix + types[1].name + " " + varname + "___tmp_1 = boost::python::extract<" + types[1].name + ">(" + varname + "[1]);" - text += prefix + TupleTranslator.gen_type(types) + " " + varname + "___tmp(" - if types[0].name.split(" ")[-1] in primitive_types: - text += varname + "___tmp_0, " - else: - text += "*" + varname + "___tmp_0.get_cpp_obj(), " - if types[1].name.split(" ")[-1] in primitive_types: - text += varname + "___tmp_1);" - else: - text += "*" + varname + "___tmp_1.get_cpp_obj());" - return text - - #Generate c++ code to translate to a boost::python::tuple - @classmethod - def translate_cpp(c, varname, types, prefix, ref): - # if the tuple is a pair of SigSpecs (aka SigSig), then we need - # to call get_py_obj() on each item in the tuple - if types[0].name in classnames: - first_var = types[0].name + "::get_py_obj(" + varname + ".first)" - else: - first_var = varname + ".first" - if types[1].name in classnames: - second_var = types[1].name + "::get_py_obj(" + varname + ".second)" - else: - second_var = varname + ".second" - text = prefix + TupleTranslator.typename + " " + varname + "___tmp = boost::python::make_tuple(" + first_var + ", " + second_var + ");" - return text - -#Associate the Translators with their c++ type -known_containers = { - "std::set" : SetTranslator, - "std::vector" : VectorTranslator, - "pool" : PoolTranslator, - "idict" : IDictTranslator, - "dict" : DictTranslator, - "std::pair" : TupleTranslator, - "std::map" : MapTranslator, - "RTLIL::ObjRange" : ObjRangeTranslator -} - -class Attribute: - wtype = None - varname = None - is_const = False - default_value = None - pos = None - pos_counter = 0 - coerce_arg = None - - def __init__(self, wtype, varname, is_const = False, default_value = None): - self.wtype = wtype - self.varname = varname - self.is_const = is_const - self.default_value = None - self.container = None - - @staticmethod - def from_string(str_def, containing_file, line_number, *, owner_fn_name=""): - if len(str_def) < 3: - return None - orig = str_def - arg = Attribute(None, None) - prefix = "" - arg.wtype = None - arg.varname = None - arg.is_const = False - arg.default_value = None - arg.container = None - if str.startswith(str_def, "const "): - arg.is_const = True - str_def = str_def[6:] - if str.startswith(str_def, "unsigned "): - prefix = "unsigned " - str_def = str_def[9:] - while str.startswith(str_def, "long "): - prefix= "long " + prefix - str_def = str_def[5:] - while str.startswith(str_def, "short "): - prefix = "short " + prefix - str_def = str_def[6:] - - if str_def.find("<") != -1 and str_def.find("<") < str_def.find(" "): - closing = find_closing(str_def[str_def.find("<"):], "<", ">") + str_def.find("<") + 1 - arg.wtype = WType.from_string(str_def[:closing].strip(), containing_file, line_number) - str_def = str_def[closing+1:] - else: - if str_def.count(" ") > 0: - arg.wtype = WType.from_string(prefix + str_def[:str_def.find(" ")].strip(), containing_file, line_number) - str_def = str_def[str_def.find(" ")+1:] - else: - arg.wtype = WType.from_string(prefix + str_def.strip(), containing_file, line_number) - str_def = "" - arg.varname = "" - - if arg.wtype == None: - return None - if str_def.count("=") == 0: - arg.varname = str_def.strip() - if arg.varname.find(" ") > 0: - return None - else: - arg.varname = str_def[:str_def.find("=")].strip() - if arg.varname.find(" ") > 0: - return None - str_def = str_def[str_def.find("=")+1:].strip() - arg.default_value = str_def[arg.varname.find("=")+1:].strip() - if len(arg.varname) == 0: - arg.varname = None - return arg - if arg.varname[0] == '*': - arg.wtype.attr_type = attr_types.star - arg.varname = arg.varname[1:] - elif arg.varname[0] == '&': - if arg.wtype.attr_type != attr_types.default: - return None - if arg.varname[1] == '&': - arg.wtype.attr_type = attr_types.ampamp - arg.varname = arg.varname[2:] - else: - arg.wtype.attr_type = attr_types.amp - arg.varname = arg.varname[1:] - - # handle string views - if arg.wtype.name in ["std::string_view", "string_view"]: - if arg.varname == "format" and owner_fn_name.startswith("log_"): - # coerce format strings to "%s" (not bridgable) - arg.coerce_arg = '"%s"' - elif arg.varname == "prefix" and "warning" in owner_fn_name: - # coerce warning prefix to "warning:" - arg.coerce_arg = '"Warning: "' - else: - # boost::python can't bridge string views, so just copy them - arg.wtype.name = "string" - - return arg - - #Generates the varname. If the attribute has no name in the header file, - #a name is generated - def gen_varname(self): - if self.varname != None: - return self.varname - if self.wtype.name == "void": - return "" - if self.pos == None: - self.pos = Attribute.pos_counter - Attribute.pos_counter = Attribute.pos_counter + 1 - return "gen_varname_" + str(self.pos) - - #Generates the text for the function headers with wrapper types - def gen_listitem(self): - prefix = "" - if self.is_const: - prefix = "const " - if self.wtype.name in classnames: - return prefix + self.wtype.name + "* " + self.gen_varname() - if self.wtype.name in known_containers: - return prefix + known_containers[self.wtype.name].typename + " " + self.gen_varname() - return prefix + self.wtype.name + " " + self.gen_varname() - - #Generates the test for the function headers with c++ types - def gen_listitem_cpp(self): - prefix = "" - if self.is_const: - prefix = "const " - infix = "" - if self.wtype.attr_type == attr_types.star: - infix = "*" - elif self.wtype.attr_type == attr_types.amp: - infix = "&" - elif self.wtype.attr_type == attr_types.ampamp: - infix = "&&" - if self.wtype.name in known_containers: - return prefix + known_containers[self.wtype.name].gen_type(self.wtype.cont.args) + " " + infix + self.gen_varname() - if self.wtype.name in classnames: - return prefix + class_by_name(self.wtype.name).namespace + "::" + self.wtype.name + " " + infix + self.gen_varname() - return prefix + self.wtype.name + " " + infix + self.gen_varname() - - #Generates the listitem withtout the varname, so the signature can be - #compared - def gen_listitem_hash(self): - prefix = "" - if self.is_const: - prefix = "const " - if self.wtype.name in classnames: - return prefix + self.wtype.name + "* " - if self.wtype.name in known_containers: - return known_containers[self.wtype.name].typename - return prefix + self.wtype.name - - #Generate Translation code for the attribute - def gen_translation(self): - if self.wtype.name in known_containers: - return known_containers[self.wtype.name].translate(self.gen_varname(), self.wtype.cont.args, "\n\t\t") - return "" - - #Generate Translation code from c++ for the attribute - def gen_translation_cpp(self): - if self.wtype.name in known_containers: - return known_containers[self.wtype.name].translate_cpp(self.gen_varname(), self.wtype.cont.args, "\n\t\t", self.wtype.attr_type == attr_types.star) - return "" - - #Generate Text for the call - def gen_call(self): - ret = self.gen_varname() - if self.wtype.name in known_containers: - if self.wtype.attr_type == attr_types.star: - return "&" + ret + "___tmp" - return ret + "___tmp" - if self.wtype.name in classnames: - if self.wtype.attr_type != attr_types.star: - ret = "*" + ret - return ret + "->get_cpp_obj()" - if self.wtype.name == "char *" and self.gen_varname() in ["format", "fmt"]: - return "\"%s\", " + self.gen_varname() - if self.wtype.attr_type == attr_types.star: - return "&" + ret - return ret - - def gen_call_cpp(self): - ret = self.gen_varname() - if self.wtype.name.split(" ")[-1] in primitive_types or self.wtype.name in enum_names: - if self.wtype.attr_type == attr_types.star: - return "&" + ret - return ret - if self.wtype.name not in classnames: - if self.wtype.attr_type == attr_types.star: - return "&" + ret + "___tmp" - return ret + "___tmp" - if self.wtype.attr_type != attr_types.star: - ret = "*" + ret - return self.wtype.name + "::get_py_obj(" + self.gen_varname() + ")" - - #Generate cleanup code - def gen_cleanup(self): - if self.wtype.name in primitive_types or self.wtype.name in classnames or self.wtype.name in enum_names or not self.wtype.attr_type == attr_types.star or (self.wtype.name in known_containers and self.wtype.attr_type == attr_types.star): - return "" - return "\n\t\tdelete " + self.gen_varname() + "___tmp;" - -class WClass: - name = None - namespace = None - link_type = None - base_class = None - id_ = None - string_id = None - hash_id = None - needs_clone = False - found_funs = [] - found_vars = [] - found_constrs = [] - - def __init__(self, name, link_type, id_, string_id = None, hash_id = None, needs_clone = False): - self.name = name - self.namespace = None - self.base_class = None - self.link_type = link_type - self.id_ = id_ - self.string_id = string_id - self.hash_id = hash_id - self.needs_clone = needs_clone - self.found_funs = [] - self.found_vars = [] - self.found_constrs = [] - - def printable_constrs(self): - ret = 0 - for con in self.found_constrs: - if not con.protected: - ret += 1 - return ret - - def gen_decl(self, filename): - long_name = self.namespace + "::" + self.name - - text = "\n\t// WRAPPED from " + filename - text += "\n\tstruct " + self.name - if self.link_type == link_types.derive: - text += " : public " + self.namespace + "::" + self.name - text += "\n\t{\n" - - if self.link_type != link_types.derive: - - text += "\t\t" + long_name + "* ref_obj;\n" - - if self.link_type == link_types.ref_copy or self.link_type == link_types.pointer: - text += "\n\t\t" + long_name + "* get_cpp_obj() const\n\t\t{\n\t\t\treturn ref_obj;\n\t\t}\n" - elif self.link_type == link_types.global_list: - text += "\t\t" + self.id_.wtype.name + " " + self.id_.varname + ";\n" - text += "\n\t\t" + long_name + "* get_cpp_obj() const\n\t\t{" - text += "\n\t\t\t" + long_name + "* ret = " + long_name + "::get_all_" + self.name.lower() + "s()->at(this->" + self.id_.varname + ");" - text += "\n\t\t\tif(ret != NULL && ret == this->ref_obj)" - text += "\n\t\t\t\treturn ret;" - text += "\n\t\t\tthrow std::runtime_error(\"" + self.name + "'s c++ object does not exist anymore.\");" - text += "\n\t\t\treturn NULL;" - text += "\n\t\t}\n" - - #if self.link_type != link_types.pointer: - text += "\n\t\tstatic " + self.name + "* get_py_obj(" + long_name + "* ref)\n\t\t{" - text += "\n\t\t\tif(ref == nullptr){" - text += "\n\t\t\t\tthrow std::runtime_error(\"" + self.name + " does not exist.\");" - text += "\n\t\t\t}" - text += "\n\t\t\t" + self.name + "* ret = (" + self.name + "*)malloc(sizeof(" + self.name + "));" - if self.link_type == link_types.pointer: - text += "\n\t\t\tret->ref_obj = ref;" - if self.link_type == link_types.ref_copy: - if self.needs_clone: - text += "\n\t\t\tret->ref_obj = ref->clone();" - else: - text += "\n\t\t\tret->ref_obj = new "+long_name+"(*ref);" - if self.link_type == link_types.global_list: - text += "\n\t\t\tret->ref_obj = ref;" - text += "\n\t\t\tret->" + self.id_.varname + " = ret->ref_obj->" + self.id_.varname + ";" - text += "\n\t\t\treturn ret;" - text += "\n\t\t}\n" - - if self.link_type == link_types.ref_copy: - text += "\n\t\tstatic " + self.name + "* get_py_obj(" + long_name + " ref)\n\t\t{" - text += "\n\t\t\t" + self.name + "* ret = (" + self.name + "*)malloc(sizeof(" + self.name + "));" - if self.needs_clone: - text += "\n\t\t\tret->ref_obj = ref.clone();" - else: - text += "\n\t\t\tret->ref_obj = new "+long_name+"(ref);" - text += "\n\t\t\treturn ret;" - text += "\n\t\t}\n" - - for con in self.found_constrs: - text += con.gen_decl() - if self.base_class is not None: - text += "\n\t\tvirtual ~" + self.name + "() { };" - for var in self.found_vars: - text += var.gen_decl() - for fun in self.found_funs: - text += fun.gen_decl() - - - if self.link_type == link_types.derive: - duplicates = {} - for fun in self.found_funs: - if fun.name in duplicates: - fun.gen_alias() - duplicates[fun.name].gen_alias() - else: - duplicates[fun.name] = fun - - text += "\n\t\t" + long_name + "* get_cpp_obj() const\n\t\t{\n\t\t\treturn (" + self.namespace + "::" + self.name +"*)this;\n\t\t}\n" - text += "\n\t\tstatic " + self.name + "* get_py_obj(" + long_name + "* ref)\n\t\t{" - text += "\n\t\t\treturn (" + self.name + "*)ref;" - text += "\n\t\t}\n" - - for con in self.found_constrs: - text += con.gen_decl_derive() - for var in self.found_vars: - text += var.gen_decl() - for fun in self.found_funs: - text += fun.gen_decl_virtual() - - if self.hash_id != None: - text += "\n\t\tunsigned int get_hash_py()" - text += "\n\t\t{" - suffix = f"->{self.hash_id}" if self.hash_id else f"->{self.hash_id}" - if self.hash_id == "": - text += f"\n\t\t\treturn run_hash(*(get_cpp_obj()));" - else: - text += f"\n\t\t\treturn run_hash(get_cpp_obj()->{self.hash_id});" - text += "\n\t\t}" - - text += "\n\t};\n" - - if self.link_type == link_types.derive: - text += "\n\tstruct " + self.name + "Wrap : " + self.name + ", boost::python::wrapper<" + self.name + ">" - text += "\n\t{" - - for con in self.found_constrs: - text += con.gen_decl_wrapperclass() - for fun in self.found_funs: - text += fun.gen_default_impl() - - text += "\n\t};" - - text += "\n\tstd::ostream &operator<<(std::ostream &ostr, const " + self.name + " &ref)" - text += "\n\t{" - text += "\n\t\tostr << \"" + self.name - if self.string_id != None: - text +=" \\\"\"" - text += " << ref.get_cpp_obj()->" + self.string_id - text += " << \"\\\"\"" - else: - text += " at \" << ref.get_cpp_obj()" - text += ";" - text += "\n\t\treturn ostr;" - text += "\n\t}" - text += "\n" - - return text - - def gen_funs(self, filename): - text = "" - if self.link_type != link_types.derive: - for con in self.found_constrs: - text += con.gen_def() - for var in self.found_vars: - text += var.gen_def() - for fun in self.found_funs: - text += fun.gen_def() - else: - for var in self.found_vars: - text += var.gen_def() - for fun in self.found_funs: - text += fun.gen_def_virtual() - return text - - def gen_boost_py_body(self): - text = "" - if self.printable_constrs() == 0 or not self.contains_default_constr(): - text += ", no_init" - text += ")" - text += "\n\t\t\t.def(boost::python::self_ns::str(boost::python::self_ns::self))" - text += "\n\t\t\t.def(boost::python::self_ns::repr(boost::python::self_ns::self))" - for con in self.found_constrs: - text += con.gen_boost_py() - for var in self.found_vars: - text += var.gen_boost_py() - static_funs = [] - for fun in self.found_funs: - text += fun.gen_boost_py() - if fun.is_static and fun.alias not in static_funs: - static_funs.append(fun.alias) - for fun in static_funs: - text += "\n\t\t\t.staticmethod(\"" + fun + "\")" - - if self.hash_id != None: - text += "\n\t\t\t.def(\"__hash__\", &" + self.name + "::get_hash_py)" - text += "\n\t\t\t;\n" - return text - - def gen_boost_py(self): - body = self.gen_boost_py_body() - base_info = "" - if self.base_class is not None: - base_info = ", bases<" + (self.base_class.name) + ">" - - if self.link_type == link_types.derive: - text = "\n\t\tclass_<" + self.name + base_info + ">(\"Cpp" + self.name + "\"" - text += body - text += "\n\t\tclass_<" + self.name - text += "Wrap, boost::noncopyable" - text += ">(\"" + self.name + "\"" - text += body - else: - text = "\n\t\tclass_<" + self.name + base_info + ">(\"" + self.name + "\"" - text += body - return text - - - def contains_default_constr(self): - for c in self.found_constrs: - if len(c.args) == 0: - return True - return False - -#CONFIGURE HEADER-FILES TO BE PARSED AND CLASSES EXPECTED IN THEM HERE - -sources = [ - Source("kernel/celltypes",[ - WClass("CellType", link_types.pointer, None, None, "type", True), - WClass("CellTypes", link_types.pointer, None, None, None, True) - ] - ), - Source("kernel/consteval",[ - WClass("ConstEval", link_types.pointer, None, None, None, True) - ] - ), - Source("kernel/log",[]), - Source("kernel/register",[ - WClass("Pass", link_types.derive, None, None, None, True), - ] - ), - Source("kernel/rtlil",[ - WClass("IdString", link_types.ref_copy, None, "str()", ""), - WClass("Const", link_types.ref_copy, None, "as_string()", ""), - WClass("AttrObject", link_types.ref_copy, None, None, None), - WClass("NamedObject", link_types.ref_copy, None, None, None), - WClass("Selection", link_types.ref_copy, None, None, None), - WClass("Monitor", link_types.derive, None, None, None), - WClass("CaseRule",link_types.ref_copy, None, None, None, True), - WClass("SwitchRule",link_types.ref_copy, None, None, None, True), - WClass("SyncRule", link_types.ref_copy, None, None, None, True), - WClass("Process", link_types.ref_copy, None, "name.c_str()", "name"), - WClass("SigChunk", link_types.ref_copy, None, None, None), - WClass("SigBit", link_types.ref_copy, None, None, ""), - WClass("SigSpec", link_types.ref_copy, None, None, ""), - WClass("Cell", link_types.global_list, Attribute(WType("unsigned int"), "hashidx_"), "name.c_str()", ""), - WClass("Wire", link_types.global_list, Attribute(WType("unsigned int"), "hashidx_"), "name.c_str()", ""), - WClass("Memory", link_types.global_list, Attribute(WType("unsigned int"), "hashidx_"), "name.c_str()", ""), - WClass("Module", link_types.global_list, Attribute(WType("unsigned int"), "hashidx_"), "name.c_str()", ""), - WClass("Design", link_types.global_list, Attribute(WType("unsigned int"), "hashidx_"), "hashidx_", "") - ] - ), - #Source("kernel/satgen",[ - # ] - # ), - #Source("libs/ezsat/ezsat",[ - # ] - # ), - #Source("libs/ezsat/ezminisat",[ - # ] - # ), - Source("kernel/sigtools",[ - WClass("SigMap", link_types.pointer, None, None, None, True) - ] - ), - Source("kernel/yosys",[ - ] - ), - Source("kernel/cost",[]) - ] - -blacklist_methods = ["YOSYS_NAMESPACE::Pass::run_register", "YOSYS_NAMESPACE::Module::Pow"] - -enum_names = ["State","SyncType","ConstFlags"] - -enums = [] #Do not edit -glbls = [] - -unowned_functions = [] - -classnames = [] -for source in sources: - for wclass in source.classes: - classnames.append(wclass.name) - -def class_by_name(name): - for source in sources: - for wclass in source.classes: - if wclass.name == name: - return wclass - return None - -def enum_by_name(name): - for e in enums: - if e.name == name: - return e - return None - -def find_closing(text, open_tok, close_tok): - if text.find(open_tok) == -1 or text.find(close_tok) <= text.find(open_tok): - return text.find(close_tok) - return text.find(close_tok) + find_closing(text[text.find(close_tok)+1:], open_tok, close_tok) + 1 - -def unpretty_string(s): - s = s.strip() - while s.find(" ") != -1: - s = s.replace(" "," ") - while s.find("\t") != -1: - s = s.replace("\t"," ") - s = s.replace(" (","(") - return s - -class WEnum: - name = None - namespace = None - values = [] - - def from_string(str_def, namespace, line_number): - str_def = str_def.strip() - if not str.startswith(str_def, "enum "): - return None - if str_def.count(";") != 1: - return None - str_def = str_def[5:] - enum = WEnum() - split = str_def.split(":") - if(len(split) != 2): - return None - enum.name = split[0].strip() - if enum.name not in enum_names: - return None - str_def = split[1] - if str_def.count("{") != str_def.count("}") != 1: - return None - if len(str_def) < str_def.find("}")+2 or str_def[str_def.find("}")+1] != ';': - return None - str_def = str_def.split("{")[-1].split("}")[0] - enum.values = [] - for val in str_def.split(','): - enum.values.append(val.strip().split('=')[0].strip()) - enum.namespace = namespace - return enum - - def gen_boost_py(self): - text = "\n\t\tenum_<" + self.namespace + "::" + self.name + ">(\"" + self.name + "\")\n" - for value in self.values: - text += "\t\t\t.value(\"" + value + "\"," + self.namespace + "::" + value + ")\n" - text += "\t\t\t;\n" - return text - - def __str__(self): - ret = "Enum " + self.namespace + "::" + self.name + "(\n" - for val in self.values: - ret = ret + "\t" + val + "\n" - return ret + ")" - - def __repr__(self): - return __str__(self) - -class WConstructor: - orig_text = None - args = [] - containing_file = None - member_of = None - duplicate = False - protected = False - - def __init__(self, containing_file, class_): - self.orig_text = "Auto generated default constructor" - self.args = [] - self.containing_file = containing_file - self.member_of = class_ - self.protected = False - - def from_string(str_def, containing_file, class_, line_number, protected = False): - if class_ == None: - return None - if str_def.count("delete;") > 0: - return None - con = WConstructor(containing_file, class_) - con.orig_text = str_def - con.args = [] - con.duplicate = False - con.protected = protected - if str.startswith(str_def, "inline "): - str_def = str_def[7:] - if not str.startswith(str_def, class_.name + "("): - return None - str_def = str_def[len(class_.name)+1:] - found = find_closing(str_def, "(", ")") - if found == -1: - return None - str_def = str_def[0:found].strip() - if len(str_def) == 0: - return con - args = split_list(str_def, ",") - for i, arg in enumerate(args): - parsed = Attribute.from_string(arg.strip(), containing_file, line_number) - if parsed == None: - return None - # Only allow std::source_location as defaulted last argument, and - # don't append so it takes default value - if parsed.wtype.name in ["std::source_location", "source_location"]: - if parsed.default_value is None or i != len(args) - 1: - debug("std::source_location not defaulted last arg of " + class_.name + " is unsupported", 2) - return None - continue - con.args.append(parsed) - return con - - def gen_decl(self): - if self.duplicate or self.protected: - return "" - text = "\n\t\t// WRAPPED from \"" + self.orig_text.replace("\n"," ") + "\" in " + self.containing_file - text += "\n\t\t" + self.member_of.name + "(" - for arg in self.args: - text += arg.gen_listitem() + ", " - if len(self.args) > 0: - text = text[:-2] - text += ");\n" - return text - - def gen_decl_derive(self): - if self.duplicate or self.protected: - return "" - text = "\n\t\t// WRAPPED from \"" + self.orig_text.replace("\n"," ") + "\" in " + self.containing_file - text += "\n\t\t" + self.member_of.name + "(" - for arg in self.args: - text += arg.gen_listitem() + ", " - if len(self.args) > 0: - text = text[:-2] - text += ")" - if len(self.args) == 0: - return text + "{}" - text += " : " - text += self.member_of.namespace + "::" + self.member_of.name + "(" - for arg in self.args: - text += arg.gen_call() + ", " - if len(self.args) > 0: - text = text[:-2] - text += "){}\n" - return text - - def gen_decl_wrapperclass(self): - if self.duplicate or self.protected: - return "" - text = "\n\t\t// WRAPPED from \"" + self.orig_text.replace("\n"," ") + "\" in " + self.containing_file - text += "\n\t\t" + self.member_of.name + "Wrap(" - for arg in self.args: - text += arg.gen_listitem() + ", " - if len(self.args) > 0: - text = text[:-2] - text += ")" - if len(self.args) == 0: - return text + "{}" - text += " : " - text += self.member_of.name + "(" - for arg in self.args: - text += arg.gen_call() + ", " - if len(self.args) > 0: - text = text[:-2] - text += "){}\n" - return text - - def gen_decl_hash_py(self): - text = self.member_of.name + "(" - for arg in self.args: - text += arg.gen_listitem_hash() + ", " - if len(self.args) > 0: - text = text[:-2] - text += ");" - return text - - def gen_def(self): - if self.duplicate or self.protected: - return "" - text = "\n\t// WRAPPED from \"" + self.orig_text.replace("\n"," ") + "\" in " + self.containing_file - text += "\n\t" + self.member_of.name + "::" + self.member_of.name + "(" - for arg in self.args: - text += arg.gen_listitem() + ", " - if len(self.args) > 0: - text = text[:-2] - text +=")\n\t{" - for arg in self.args: - text += arg.gen_translation() - if self.member_of.link_type != link_types.derive: - text += "\n\t\tthis->ref_obj = new " + self.member_of.namespace + "::" + self.member_of.name + "(" - for arg in self.args: - text += arg.gen_call() + ", " - if len(self.args) > 0: - text = text[:-2] - if self.member_of.link_type != link_types.derive: - text += ");" - if self.member_of.link_type == link_types.global_list: - text += "\n\t\tthis->" + self.member_of.id_.varname + " = this->ref_obj->" + self.member_of.id_.varname + ";" - for arg in self.args: - text += arg.gen_cleanup() - text += "\n\t}\n" - return text - - def gen_boost_py(self): - if self.duplicate or self.protected or len(self.args) == 0: - return "" - text = "\n\t\t\t.def(init" - text += "<" - for a in self.args: - text += a.gen_listitem_hash() + ", " - text = text[0:-2] + ">())" - return text - -class WFunction: - orig_text = None - is_static = False - is_inline = False - is_virtual = False - ret_attr_type = attr_types.default - is_operator = False - ret_type = None - name = None - alias = None - args = [] - containing_file = None - member_of = None - duplicate = False - namespace = "" - - def from_string(str_def, containing_file, class_, line_number, namespace): - if str_def.count("delete;") > 0: - return None - func = WFunction() - func.is_static = False - func.is_inline = False - func.is_virtual = False - func.is_const = False - func.ret_attr_type = attr_types.default - func.is_operator = False - func.member_of = None - func.orig_text = str_def - func.args = [] - func.containing_file = containing_file - func.member_of = class_ - func.duplicate = False - func.namespace = namespace - str_def = str_def.replace("operator ","operator") - - # remove attributes from the start - if str.startswith(str_def, "[[") and "]]" in str_def: - str_def = str_def[str_def.find("]]")+2:] - - if str.startswith(str_def, "static "): - func.is_static = True - str_def = str_def[7:] - else: - func.is_static = False - if str.startswith(str_def, "inline "): - func.is_inline = True - str_def = str_def[7:] - else: - func.is_inline = False - if str.startswith(str_def, "virtual "): - func.is_virtual = True - str_def = str_def[8:] - else: - func.is_virtual = False - - if str_def.count(" ") == 0: - return None - - parts = split_list(str_def.strip(), " ") - - prefix = "" - i = 0 - for part in parts: - if part in ["unsigned", "long", "short", "const"]: - prefix += part + " " - i += 1 - else: - break - parts = parts[i:] - - if len(parts) <= 1: - return None - - func.ret_type = WType.from_string(prefix + parts[0], containing_file, line_number) - - if func.ret_type == None: - return None - - str_def = parts[1] - for part in parts[2:]: - str_def = str_def + " " + part - - found = str_def.find("(") - if found == -1 or (str_def.find(" ") != -1 and found > str_def.find(" ")): - return None - func.name = str_def[:found] - str_def = str_def[found:] - if func.name.find("operator") != -1 and str.startswith(str_def, "()("): - func.name += "()" - str_def = str_def[2:] - str_def = str_def[1:] - if func.name.find("operator") != -1: - func.is_operator = True - if func.name.find("*") == 0: - func.name = func.name.replace("*", "") - func.ret_type.attr_type = attr_types.star - if func.name.find("&&") == 0: - func.name = func.name.replace("&&", "") - func.ret_type.attr_type = attr_types.ampamp - if func.name.find("&") == 0: - func.name = func.name.replace("&", "") - func.ret_type.attr_type = attr_types.amp - - found = find_closing(str_def, "(", ")") - if found == -1: - return None - - post_qualifiers = str_def[found + 1:].lstrip().replace("{", " {") + " " - if post_qualifiers.startswith("const "): - func.is_const = True - - str_def = str_def[0:found] - if func.name in blacklist_methods: - return None - if func.namespace != None and func.namespace != "": - if (func.namespace + "::" + func.name) in blacklist_methods: - return None - if func.member_of != None: - if (func.namespace + "::" + func.member_of.name + "::" + func.name) in blacklist_methods: - return None - if func.is_operator and func.name.replace(" ","").replace("operator","").split("::")[-1] not in wrappable_operators: - return None - - testname = func.name - if func.is_operator: - testname = testname[:testname.find("operator")] - if testname.count(")") != 0 or testname.count("(") != 0 or testname.count("~") != 0 or testname.count(";") != 0 or testname.count(">") != 0 or testname.count("<") != 0 or testname.count("throw") != 0: - return None - - func.alias = func.name - if func.name in keyword_aliases: - func.alias = keyword_aliases[func.name] - str_def = str_def[:found].strip() - if(len(str_def) == 0): - return func - args = split_list(str_def, ",") - for i, arg in enumerate(args): - if arg.strip() == "...": - continue - parsed = Attribute.from_string(arg.strip(), containing_file, line_number, owner_fn_name=func.name) - if parsed == None: - return None - # Only allow std::source_location as defaulted last argument, and - # don't append so it takes default value - if parsed.wtype.name in ["std::source_location", "source_location"]: - if parsed.default_value is None or i != len(args) - 1: - debug("std::source_location not defaulted last arg of " + func.name + " is unsupported", 2) - return None - continue - func.args.append(parsed) - return func - - @property - def mangled_name(self): - mangled_typename = lambda code: code.replace("::", "_").replace("<","_").replace(">","_") \ - .replace(" ","").replace("*","").replace(",","") - - return self.name + "".join( - f"__{mangled_typename(arg.wtype.gen_text_cpp())}" for arg in self.args - ) - - def gen_alias(self): - self.alias = self.mangled_name - - def gen_post_qualifiers(self, derived=False): - if self.member_of != None and self.member_of.link_type == link_types.derive and self.is_virtual and derived: - # we drop the qualifiers when deriving callbacks to be implemented in Python - return '' - return ' const' if self.is_const else '' - - def gen_decl(self): - if self.duplicate: - return "" - text = "\n\t\t// WRAPPED from \"" + self.orig_text.replace("\n"," ") + "\" in " + self.containing_file - text += "\n\t\t" - if self.is_static: - text += "static " - text += self.ret_type.gen_text() + " " + self.alias + "(" - for arg in self.args: - if arg.coerce_arg: - continue - text += arg.gen_listitem() - text += ", " - if len(self.args) > 0: - text = text[:-2] - text += f"){self.gen_post_qualifiers()};\n" - return text - - def gen_decl_virtual(self): - if self.duplicate: - return "" - if not self.is_virtual: - return self.gen_decl() - text = "\n\t\t// WRAPPED from \"" + self.orig_text.replace("\n"," ") + "\" in " + self.containing_file - text += "\n\t\tvirtual " - if self.is_static: - text += "static " - text += self.ret_type.gen_text() + " py_" + self.alias + "(" - for arg in self.args: - text += arg.gen_listitem() - text += ", " - if len(self.args) > 0: - text = text[:-2] - text += ")" - if len(self.args) == 0 and self.ret_type.name == "void": - text += "{}" - else: - text += "\n\t\t{" - for arg in self.args: - text += "\n\t\t\t(void)" + arg.gen_varname() + ";" - if self.ret_type.name == "void": - pass - elif self.ret_type.name == "bool": - text += "\n\t\t\treturn false;" - else: - raise NotImplementedError(self.ret_type.name) - text += "\n\t\t}\n" - text += "\n\t\tvirtual " - if self.is_static: - text += "static " - text += self.ret_type.gen_text() + " " + self.name + "(" - for arg in self.args: - text += arg.gen_listitem_cpp() - text += ", " - if len(self.args) > 0: - text = text[:-2] - text += f"){self.gen_post_qualifiers()} override;\n" - return text - - def gen_decl_hash_py(self): - text = self.ret_type.gen_text() + " " + self.alias + "(" - for arg in self.args: - text += arg.gen_listitem_hash() + ", " - if len(self.args) > 0: - text = text[:-2] - text += ");" - return text - - def gen_def(self): - if self.duplicate: - return "" - text = "\n\t// WRAPPED from \"" + self.orig_text.replace("\n"," ") + "\" in " + self.containing_file - text += "\n\t" + self.ret_type.gen_text() + " " - if self.member_of != None: - text += self.member_of.name + "::" - text += self.alias + "(" - for arg in self.args: - if arg.coerce_arg: - continue - text += arg.gen_listitem() - text += ", " - if len(self.args) > 0: - text = text[:-2] - text += f"){self.gen_post_qualifiers()}\n\t{{" - for arg in self.args: - text += arg.gen_translation() - text += "\n\t\t" - if self.ret_type.name != "void": - if self.ret_type.name in known_containers: - text += self.ret_type.gen_text_cpp() - else: - text += self.ret_type.gen_text() - if self.ret_type.name in classnames or (self.ret_type.name in known_containers and self.ret_type.attr_type == attr_types.star): - text += "*" - text += " ret_ = " - if self.ret_type.name in classnames: - text += self.ret_type.name + "::get_py_obj(" - if self.member_of == None: - text += "::" + self.namespace + "::" + self.name + "(" - elif self.is_static: - text += self.member_of.namespace + "::" + self.member_of.name + "::" + self.name + "(" - else: - text += "this->get_cpp_obj()->" + self.name + "(" - for arg in self.args: - text += arg.coerce_arg or arg.gen_call() - text += ", " - if len(self.args) > 0: - text = text[:-2] - if self.ret_type.name in classnames: - text += ")" - text += ");" - for arg in self.args: - text += arg.gen_cleanup() - if self.ret_type.name != "void": - if self.ret_type.name in classnames: - text += "\n\t\treturn *ret_;" - elif self.ret_type.name in known_containers: - text += known_containers[self.ret_type.name].translate_cpp("ret_", self.ret_type.cont.args, "\n\t\t", self.ret_type.attr_type == attr_types.star) - text += "\n\t\treturn ret____tmp;" - else: - text += "\n\t\treturn ret_;" - text += "\n\t}\n" - return text - - def gen_def_virtual(self): - if self.duplicate: - return "" - if not self.is_virtual: - return self.gen_def() - text = "\n\t// WRAPPED from \"" + self.orig_text.replace("\n"," ") + "\" in " + self.containing_file - text += "\n\t" - if self.is_static: - text += "static " - text += self.ret_type.gen_text() + " " + self.member_of.name + "::" + self.name + "(" - for arg in self.args: - text += arg.gen_listitem_cpp() - text += ", " - if len(self.args) > 0: - text = text[:-2] - text += f"){self.gen_post_qualifiers()}\n\t{{" - for arg in self.args: - text += arg.gen_translation_cpp() - return_stmt = "return " if self.ret_type.name != "void" else "" - text += f"\n\t\t{return_stmt}" - if self.member_of == None: - text += "::" + self.namespace + "::" + self.alias + "(" - elif self.is_static: - text += self.member_of.namespace + "::" + self.member_of.name + "::" + self.name + "(" - else: - text += f"const_cast<{self.member_of.name}*>(this)->py_" + self.alias + "(" - for arg in self.args: - text += arg.gen_call_cpp() + ", " - if len(self.args) > 0: - text = text[:-2] - if self.ret_type.name in classnames: - text += ")" - text += ");" - for arg in self.args: - text += arg.gen_cleanup() - text += "\n\t}\n" - return text - - def gen_default_impl(self): - if self.duplicate: - return "" - if not self.is_virtual: - return "" - text = "\n\n\t\t" + self.ret_type.gen_text() + " py_" + self.alias + "(" - for arg in self.args: - text += arg.gen_listitem() + ", " - if len(self.args) > 0: - text = text[:-2] - - call_string = "py_" + self.alias + "(" - for arg in self.args: - call_string += arg.gen_varname() + ", " - if len(self.args) > 0: - call_string = call_string[0:-2] - call_string += ");" - - return_stmt = "return " if self.ret_type.name != "void" else "" - - text += ")\n\t\t{" - text += "\n\t\t\tif (boost::python::override py_" + self.alias + " = this->get_override(\"py_" + self.alias + "\")) {" - text += "\n\t\t\t\ttry {" - text += f"\n\t\t\t\t\t{return_stmt}" + call_string - text += "\n\t\t\t\t} catch (boost::python::error_already_set &) {" - text += "\n\t\t\t\t\tlog_python_exception_as_error();" - text += "\n\t\t\t\t}" - text += "\n\t\t\t} else {" - text += f"\n\t\t\t\t{return_stmt}" + self.member_of.name + "::" + call_string - text += "\n\t\t\t}" - text += "\n\t\t}" - - text += "\n\n\t\t" + self.ret_type.gen_text() + " default_py_" + self.alias + "(" - for arg in self.args: - text += arg.gen_listitem() + ", " - if len(self.args) > 0: - text = text[:-2] - text += f")\n\t\t{{" - text += f"\n\t\t\t{return_stmt}this->" + self.member_of.name + "::" + call_string - text += "\n\t\t}" - return text - - - def gen_boost_py(self): - if self.duplicate: - return "" - if self.member_of == None: - text = "\n\t\tdef" - else: - text = "\n\t\t\t.def" - if len(self.args) > -1: - if self.ret_type.name in known_containers: - text += "<" + known_containers[self.ret_type.name].typename + " " - else: - text += "<" + self.ret_type.name + " " - if self.member_of == None or self.is_static: - text += "(*)(" - else: - text += "(" + self.member_of.name + "::*)(" - for a in self.args: - if a.coerce_arg: - continue - text += a.gen_listitem_hash() + ", " - if len(self.args) > 0: - text = text[0:-2] + f"){self.gen_post_qualifiers(True)}>" - else: - text += f"void){self.gen_post_qualifiers(True)}>" - - if self.is_operator: - text += "(\"" + wrappable_operators[self.name.replace("operator","")] + "\"" - else: - if self.member_of != None and self.member_of.link_type == link_types.derive and self.is_virtual: - text += "(\"py_" + self.alias + "\"" - else: - text += "(\"" + self.alias + "\"" - if self.member_of != None: - text += ", &" + self.member_of.name + "::" - if self.member_of.link_type == link_types.derive and self.is_virtual: - text += "py_" + self.alias - text += ", &" + self.member_of.name + "Wrap::default_py_" + self.alias - else: - text += self.alias - - text += ")" - else: - text += ", " + "YOSYS_PYTHON::" + self.alias + ");" - return text - -class WMember: - orig_text = None - wtype = attr_types.default - name = None - containing_file = None - member_of = None - namespace = "" - is_const = False - - def from_string(str_def, containing_file, class_, line_number, namespace): - member = WMember() - member.orig_text = str_def - member.wtype = None - member.name = "" - member.containing_file = containing_file - member.member_of = class_ - member.namespace = namespace - member.is_const = False - - if str.startswith(str_def, "const "): - member.is_const = True - str_def = str_def[6:] - - if str_def.count(" ") == 0: - return None - - parts = split_list(str_def.strip(), " ") - - prefix = "" - i = 0 - for part in parts: - if part in ["unsigned", "long", "short"]: - prefix += part + " " - i += 1 - else: - break - parts = parts[i:] - - if len(parts) <= 1: - return None - - member.wtype = WType.from_string(prefix + parts[0], containing_file, line_number) - - if member.wtype == None: - return None - - str_def = parts[1] - for part in parts[2:]: - str_def = str_def + " " + part - - if str_def.find("(") != -1 or str_def.find(")") != -1 or str_def.find("{") != -1 or str_def.find("}") != -1: - return None - - found = str_def.find(";") - if found == -1: - return None - - found_eq = str_def.find("=") - if found_eq != -1: - found = found_eq - - member.name = str_def[:found] - str_def = str_def[found+1:] - if member.name.find("*") == 0: - member.name = member.name.replace("*", "") - member.wtype.attr_type = attr_types.star - if member.name.find("&&") == 0: - member.name = member.name.replace("&&", "") - member.wtype.attr_type = attr_types.ampamp - if member.name.find("&") == 0: - member.name = member.name.replace("&", "") - member.wtype.attr_type = attr_types.amp - - if(len(str_def.strip()) != 0): - return None - - if len(member.name.split(",")) > 1: - member_list = [] - for name in member.name.split(","): - name = name.strip(); - member_list.append(WMember()) - member_list[-1].orig_text = member.orig_text - member_list[-1].wtype = member.wtype - member_list[-1].name = name - member_list[-1].containing_file = member.containing_file - member_list[-1].member_of = member.member_of - member_list[-1].namespace = member.namespace - member_list[-1].is_const = member.is_const - return member_list - - return member - - def gen_decl(self): - text = "\n\t\t" + self.wtype.gen_text() + " get_var_py_" + self.name + "();\n" - if self.is_const: - return text - if self.wtype.name in classnames: - text += "\n\t\tvoid set_var_py_" + self.name + "(" + self.wtype.gen_text() + " *rhs);\n" - else: - text += "\n\t\tvoid set_var_py_" + self.name + "(" + self.wtype.gen_text() + " rhs);\n" - return text - - def gen_def(self): - text = "\n\t" + self.wtype.gen_text() + " " + self.member_of.name +"::get_var_py_" + self.name + "()" - text += "\n\t{\n\t\t" - if self.wtype.attr_type == attr_types.star: - text += "if(this->get_cpp_obj()->" + self.name + " == NULL)\n\t\t\t" - text += "throw std::runtime_error(\"Member \\\"" + self.name + "\\\" is NULL\");\n\t\t" - if self.wtype.name in known_containers: - text += self.wtype.gen_text_cpp() - else: - text += self.wtype.gen_text() - - if self.wtype.name in classnames or (self.wtype.name in known_containers and self.wtype.attr_type == attr_types.star): - text += "*" - text += " ret_ = " - if self.wtype.name in classnames: - text += self.wtype.name + "::get_py_obj(" - if self.wtype.attr_type != attr_types.star: - text += "&" - text += "this->get_cpp_obj()->" + self.name - if self.wtype.name in classnames: - text += ")" - text += ";" - - if self.wtype.name in classnames: - text += "\n\t\treturn *ret_;" - elif self.wtype.name in known_containers: - text += known_containers[self.wtype.name].translate_cpp("ret_", self.wtype.cont.args, "\n\t\t", self.wtype.attr_type == attr_types.star) - text += "\n\t\treturn ret____tmp;" - else: - text += "\n\t\treturn ret_;" - text += "\n\t}\n" - - if self.is_const: - return text - - ret = Attribute(self.wtype, "rhs"); - - if self.wtype.name in classnames: - text += "\n\tvoid " + self.member_of.name+ "::set_var_py_" + self.name + "(" + self.wtype.gen_text() + " *rhs)" - else: - text += "\n\tvoid " + self.member_of.name+ "::set_var_py_" + self.name + "(" + self.wtype.gen_text() + " rhs)" - text += "\n\t{" - text += ret.gen_translation() - text += "\n\t\tthis->get_cpp_obj()->" + self.name + " = " + ret.gen_call() + ";" - text += "\n\t}\n" - - return text; - - def gen_boost_py(self): - text = "\n\t\t\t.add_property(\"" + self.name + "\", &" + self.member_of.name + "::get_var_py_" + self.name - if not self.is_const: - text += ", &" + self.member_of.name + "::set_var_py_" + self.name - text += ")" - return text - -class WGlobal: - orig_text = None - wtype = attr_types.default - name = None - containing_file = None - namespace = "" - is_const = False - - def from_string(str_def, containing_file, line_number, namespace): - glbl = WGlobal() - glbl.orig_text = str_def - glbl.wtype = None - glbl.name = "" - glbl.containing_file = containing_file - glbl.namespace = namespace - glbl.is_const = False - - if not str.startswith(str_def, "extern"): - return None - str_def = str_def[7:] - - if str.startswith(str_def, "const "): - glbl.is_const = True - str_def = str_def[6:] - - if str_def.count(" ") == 0: - return None - - parts = split_list(str_def.strip(), " ") - - prefix = "" - i = 0 - for part in parts: - if part in ["unsigned", "long", "short"]: - prefix += part + " " - i += 1 - else: - break - parts = parts[i:] - - if len(parts) <= 1: - return None - - glbl.wtype = WType.from_string(prefix + parts[0], containing_file, line_number) - - if glbl.wtype == None: - return None - - str_def = parts[1] - for part in parts[2:]: - str_def = str_def + " " + part - - if str_def.find("(") != -1 or str_def.find(")") != -1 or str_def.find("{") != -1 or str_def.find("}") != -1: - return None - - found = str_def.find(";") - if found == -1: - return None - - found_eq = str_def.find("=") - if found_eq != -1: - found = found_eq - - glbl.name = str_def[:found] - str_def = str_def[found+1:] - if glbl.name.find("*") == 0: - glbl.name = glbl.name.replace("*", "") - glbl.wtype.attr_type = attr_types.star - if glbl.name.find("&&") == 0: - glbl.name = glbl.name.replace("&&", "") - glbl.wtype.attr_type = attr_types.ampamp - if glbl.name.find("&") == 0: - glbl.name = glbl.name.replace("&", "") - glbl.wtype.attr_type = attr_types.amp - - if(len(str_def.strip()) != 0): - return None - - if len(glbl.name.split(",")) > 1: - glbl_list = [] - for name in glbl.name.split(","): - name = name.strip(); - glbl_list.append(WGlobal()) - glbl_list[-1].orig_text = glbl.orig_text - glbl_list[-1].wtype = glbl.wtype - glbl_list[-1].name = name - glbl_list[-1].containing_file = glbl.containing_file - glbl_list[-1].namespace = glbl.namespace - glbl_list[-1].is_const = glbl.is_const - return glbl_list - - return glbl - - def gen_def(self): - text = "\n\t" - if self.is_const: - text += "const " - text += self.wtype.gen_text() + " get_var_py_" + self.name + "()" - text += "\n\t{\n\t\t" - if self.wtype.attr_type == attr_types.star: - text += "if(" + self.namespace + "::" + self.name + " == NULL)\n\t\t\t" - text += "throw std::runtime_error(\"" + self.namespace + "::" + self.name + " is NULL\");\n\t\t" - if self.wtype.name in known_containers: - text += self.wtype.gen_text_cpp() - else: - if self.is_const: - text += "const " - text += self.wtype.gen_text() - - if self.wtype.name in classnames or (self.wtype.name in known_containers and self.wtype.attr_type == attr_types.star): - text += "*" - text += " ret_ = " - if self.wtype.name in classnames: - text += self.wtype.name + "::get_py_obj(" - if self.wtype.attr_type != attr_types.star: - text += "&" - text += self.namespace + "::" + self.name - if self.wtype.name in classnames: - text += ")" - text += ";" - - if self.wtype.name in classnames: - text += "\n\t\treturn *ret_;" - elif self.wtype.name in known_containers: - text += known_containers[self.wtype.name].translate_cpp("ret_", self.wtype.cont.args, "\n\t\t", self.wtype.attr_type == attr_types.star) - text += "\n\t\treturn ret____tmp;" - else: - text += "\n\t\treturn ret_;" - text += "\n\t}\n" - - if self.is_const: - return text - - ret = Attribute(self.wtype, "rhs"); - - if self.wtype.name in classnames: - text += "\n\tvoid set_var_py_" + self.name + "(" + self.wtype.gen_text() + " *rhs)" - else: - text += "\n\tvoid set_var_py_" + self.name + "(" + self.wtype.gen_text() + " rhs)" - text += "\n\t{" - text += ret.gen_translation() - text += "\n\t\t" + self.namespace + "::" + self.name + " = " + ret.gen_call() + ";" - text += "\n\t}\n" - - return text; - - def gen_boost_py(self): - text = "\n\t\t\t.add_static_property(\"" + self.name + "\", &" + "YOSYS_PYTHON::get_var_py_" + self.name - if not self.is_const: - text += ", &YOSYS_PYTHON::set_var_py_" + self.name - text += ")" - return text - -def concat_namespace(tuple_list): - if len(tuple_list) == 0: - return "" - ret = "" - for namespace in tuple_list: - ret += "::" + namespace[0] - return ret[2:] - -def calc_ident(text): - if len(text) == 0 or text[0] != ' ': - return 0 - return calc_ident(text[1:]) + 1 - -def assure_length(text, length, left = False): - if len(text) > length: - return text[:length] - if left: - return text + " "*(length - len(text)) - return " "*(length - len(text)) + text - -def nesting_delta(s): - return s.count("{") - s.count("}") - -def parse_header(source): - debug("Parsing " + source.name + ".pyh",1) - source_file = open(source.name + ".pyh", "r") - - source_text = [] - in_line = source_file.readline() - - namespaces = [] - - while(in_line): - if(len(in_line)>1): - source_text.append(in_line.replace("char *", "char_p ").replace("char* ", "char_p ")) - in_line = source_file.readline() - - i = 0 - - namespaces = [] - classes = [] - private_segment = False - - while i < len(source_text): - line = source_text[i].replace("YOSYS_NAMESPACE_BEGIN", " namespace YOSYS_NAMESPACE{").replace("YOSYS_NAMESPACE_END"," }") - ugly_line = unpretty_string(line) - debug(f"READ:>> {line}", 2) - - # for anonymous unions, ignore union enclosure by skipping start line and replacing end line with new line - if 'union {' in line: - j = i+1 - while j < len(source_text): - union_line = source_text[j] - if '};' in union_line: - source_text[j] = '\n' - break - j += 1 - if j != len(source_text): - i += 1 - continue - - if str.startswith(ugly_line, "namespace "):# and ugly_line.find("std") == -1 and ugly_line.find("__") == -1: - namespace_name = ugly_line[10:].replace("{","").strip() - namespaces.append((namespace_name, ugly_line.count("{"))) - debug("-----NAMESPACE " + concat_namespace(namespaces) + "-----",3) - i += 1 - continue - - if len(namespaces) != 0: - namespaces[-1] = (namespaces[-1][0], namespaces[-1][1] + nesting_delta(ugly_line)) - if namespaces[-1][1] == 0: - debug("-----END NAMESPACE " + concat_namespace(namespaces) + "-----",3) - namespaces.pop() - i += 1 - continue - - if (str.startswith(ugly_line, "struct ") or str.startswith(ugly_line, "class")) and ugly_line.count(";") == 0: - # Opening a record declaration which isn't a forward declaration - struct_name = ugly_line.split(" ")[1].split("::")[-1] - impl_namespaces = ugly_line.split(" ")[1].split("::")[:-1] - complete_namespace = concat_namespace(namespaces) - for namespace in impl_namespaces: - complete_namespace += "::" + namespace - debug("\tFound " + struct_name + " in " + complete_namespace,2) - - base_class_name = None - if len(ugly_line.split(" : ")) > 1: # class is derived - deriv_str = ugly_line.split(" : ")[1] - if len(deriv_str.split("::")) > 1: # namespace of base class is given - base_class_name = deriv_str.split("::", 1)[1] - else: - base_class_name = deriv_str.split(" ")[0] - debug("\t " + struct_name + " is derived from " + base_class_name,2) - base_class = class_by_name(base_class_name) - - c = (class_by_name(struct_name), ugly_line.count("{"))#calc_ident(line)) - debug(f"switch to {struct_name} in namespace {namespaces}", 2) - if struct_name in classnames: - c[0].namespace = complete_namespace - c[0].base_class = base_class - classes.append(c) - i += 1 - continue - - if len(classes): - c = (classes[-1][0], classes[-1][1] + nesting_delta(ugly_line)) - classes[-1] = c - if c[1] == 0: - if c[0] == None: - debug("\tExiting unknown class", 3) - else: - debug("\tExiting class " + c[0].name, 3) - classes.pop() - private_segment = False - i += 1 - continue - - class_ = classes[-1] if classes else None - - if class_ != None and (line.find("private:") != -1 or line.find("protected:") != -1): - private_segment = True - i += 1 - continue - if class_ != None and line.find("public:") != -1: - private_segment = False - i += 1 - continue - - candidate = None - - if private_segment and class_ != None and class_[0] != None: - candidate = WConstructor.from_string(ugly_line, source.name, class_[0], i, True) - if candidate != None: - debug("\t\tFound constructor of class \"" + class_[0].name + "\" in namespace " + concat_namespace(namespaces),2) - class_[0].found_constrs.append(candidate) - i += 1 - continue - - if not private_segment and (class_ == None or class_[0] != None): - if class_ != None: - candidate = WFunction.from_string(ugly_line, source.name, class_[0], i, concat_namespace(namespaces)) - else: - candidate = WFunction.from_string(ugly_line, source.name, None, i, concat_namespace(namespaces)) - if candidate != None and candidate.name.find("::") == -1: - if class_ == None: - debug("\tFound unowned function \"" + candidate.name + "\" in namespace " + concat_namespace(namespaces),2) - unowned_functions.append(candidate) - - # generate log aliases - if candidate.name.startswith("log_formatted"): - alias = candidate.name.replace("log_formatted", "log") - if alias == "log_string": - alias = "log" - copied_candidate = copy.copy(candidate) - copied_candidate.alias = alias - unowned_functions.append(copied_candidate) - - else: - debug("\t\tFound function \"" + candidate.name + "\" of class \"" + class_[0].name + "\" in namespace " + concat_namespace(namespaces),2) - class_[0].found_funs.append(candidate) - else: - candidate = WEnum.from_string(ugly_line, concat_namespace(namespaces), i) - if candidate != None: - enums.append(candidate) - debug("\tFound enum \"" + candidate.name + "\" in namespace " + concat_namespace(namespaces),2) - elif class_ != None and class_[1] == 1: - candidate = WConstructor.from_string(ugly_line, source.name, class_[0], i) - if candidate != None: - debug("\t\tFound constructor of class \"" + class_[0].name + "\" in namespace " + concat_namespace(namespaces),2) - class_[0].found_constrs.append(candidate) - else: - candidate = WMember.from_string(ugly_line, source.name, class_[0], i, concat_namespace(namespaces)) - if candidate != None: - if type(candidate) == list: - for c in candidate: - debug("\t\tFound member \"" + c.name + "\" of class \"" + class_[0].name + "\" of type \"" + c.wtype.name + "\"", 2) - class_[0].found_vars.extend(candidate) - else: - debug("\t\tFound member \"" + candidate.name + "\" of class \"" + class_[0].name + "\" of type \"" + candidate.wtype.name + "\"", 2) - class_[0].found_vars.append(candidate) - if candidate == None and class_ == None: - candidate = WGlobal.from_string(ugly_line, source.name, i, concat_namespace(namespaces)) - if candidate != None: - if type(candidate) == list: - for c in candidate: - glbls.append(c) - debug("\tFound global \"" + c.name + "\" in namespace " + concat_namespace(namespaces), 2) - else: - glbls.append(candidate) - debug("\tFound global \"" + candidate.name + "\" in namespace " + concat_namespace(namespaces), 2) - - j = i - line = unpretty_string(line) - while candidate == None and j+1 < len(source_text) and line.count(';') <= 1 and line.count("(") >= line.count(")"): - j += 1 - line = line + "\n" + unpretty_string(source_text[j]) - if class_ != None: - candidate = WFunction.from_string(ugly_line, source.name, class_[0], i, concat_namespace(namespaces)) - else: - candidate = WFunction.from_string(ugly_line, source.name, None, i, concat_namespace(namespaces)) - if candidate != None and candidate.name.find("::") == -1: - if class_ == None: - debug("\tFound unowned function \"" + candidate.name + "\" in namespace " + concat_namespace(namespaces),2) - unowned_functions.append(candidate) - else: - debug("\t\tFound function \"" + candidate.name + "\" of class \"" + class_[0].name + "\" in namespace " + concat_namespace(namespaces),2) - class_[0].found_funs.append(candidate) - continue - candidate = WEnum.from_string(line, concat_namespace(namespaces), i) - if candidate != None: - enums.append(candidate) - debug("\tFound enum \"" + candidate.name + "\" in namespace " + concat_namespace(namespaces),2) - continue - if class_ != None: - candidate = WConstructor.from_string(line, source.name, class_[0], i) - if candidate != None: - debug("\t\tFound constructor of class \"" + class_[0].name + "\" in namespace " + concat_namespace(namespaces),2) - class_[0].found_constrs.append(candidate) - continue - if class_ == None: - candidate = WGlobal.from_string(line, source.name, i, concat_namespace(namespaces)) - if candidate != None: - if type(candidate) == list: - for c in candidate: - glbls.append(c) - debug("\tFound global \"" + c.name + "\" in namespace " + concat_namespace(namespaces), 2) - else: - glbls.append(candidate) - debug("\tFound global \"" + candidate.name + "\" in namespace " + concat_namespace(namespaces), 2) - continue - if candidate != None: - while i < j: - i += 1 - line = source_text[i].replace("YOSYS_NAMESPACE_BEGIN", " namespace YOSYS_NAMESPACE{").replace("YOSYS_NAMESPACE_END"," }") - ugly_line = unpretty_string(line) - if len(namespaces) != 0: - namespaces[-1] = (namespaces[-1][0], namespaces[-1][1] + nesting_delta(ugly_line)) - if namespaces[-1][1] == 0: - debug("-----END NAMESPACE " + concat_namespace(namespaces) + "-----",3) - namespaces.pop() - if len(classes): - c = (classes[-1][0] , classes[-1][1] + nesting_delta(ugly_line)) - classes[-1] = c - if c[1] == 0: - if c[0] == None: - debug("\tExiting unknown class", 3) - else: - debug("\tExiting class " + c[0].name, 3) - classes.pop() - private_segment = False - i += 1 - else: - i += 1 - -def debug(message, level): - if level <= debug.debug_level: - print(message) - -def expand_function(f): - fun_list = [] - arg_list = [] - for arg in f.args: - if arg.default_value != None and (arg.wtype.name.split(" ")[-1] in primitive_types or arg.wtype.name in enum_names or (arg.wtype.name in classnames and arg.default_value == "nullptr")): - fi = copy.deepcopy(f) - fi.args = copy.deepcopy(arg_list) - fun_list.append(fi) - arg_list.append(arg) - fun_list.append(f) - return fun_list - -def expand_functions(): - global unowned_functions - new_funs = [] - for fun in unowned_functions: - new_funs.extend(expand_function(fun)) - unowned_functions = new_funs - for source in sources: - for class_ in source.classes: - new_funs = [] - for fun in class_.found_funs: - new_funs.extend(expand_function(fun)) - class_.found_funs = new_funs - -def inherit_members(): - for source in sources: - for class_ in source.classes: - if class_.base_class: - base_funs = copy.deepcopy(class_.base_class.found_funs) - for fun in base_funs: - fun.member_of = class_ - fun.namespace = class_.namespace - base_vars = copy.deepcopy(class_.base_class.found_vars) - for var in base_vars: - var.member_of = class_ - var.namespace = class_.namespace - class_.found_funs.extend(base_funs) - class_.found_vars.extend(base_vars) - -def clean_duplicates(): - for source in sources: - for class_ in source.classes: - known_decls = {} - for fun in class_.found_funs: - if fun.gen_decl_hash_py() in known_decls: - debug("Multiple declarations of " + fun.gen_decl_hash_py(),3) - - other = known_decls[fun.gen_decl_hash_py()] - if fun.mangled_name == other.mangled_name: - fun.duplicate = True - debug("Disabled \"" + fun.gen_decl_hash_py() + "\"", 3) - continue - - other.gen_alias() - fun.gen_alias() - else: - known_decls[fun.gen_decl_hash_py()] = fun - known_decls = [] - for con in class_.found_constrs: - if con.gen_decl_hash_py() in known_decls: - debug("Multiple declarations of " + con.gen_decl_hash_py(),3) - con.duplicate = True - else: - known_decls.append(con.gen_decl_hash_py()) - known_decls = [] - for fun in unowned_functions: - if fun.gen_decl_hash_py() in known_decls: - debug("Multiple declarations of " + fun.gen_decl_hash_py(),3) - fun.duplicate = True - else: - known_decls.append(fun.gen_decl_hash_py()) - -def gen_wrappers(filename, debug_level_ = 0): - debug.debug_level = debug_level_ - for source in sources: - parse_header(source) - - expand_functions() - inherit_members() - clean_duplicates() - - import shutil - import math - col = shutil.get_terminal_size((80,20)).columns - debug("-"*col, 1) - debug("-"*math.floor((col-7)/2)+"SUMMARY"+"-"*math.ceil((col-7)/2), 1) - debug("-"*col, 1) - for source in sources: - for class_ in source.classes: - debug("Class " + assure_length(class_.name, len(max(classnames, key=len)), True) + " contains " + assure_length(str(len(class_.found_vars)), 3, False) + " member variables, "+ assure_length(str(len(class_.found_funs)), 3, False) + " methods and " + assure_length(str(len(class_.found_constrs)), 2, False) + " constructors", 1) - if len(class_.found_constrs) == 0: - class_.found_constrs.append(WConstructor(source.name, class_)) - debug(str(len(unowned_functions)) + " functions are unowned", 1) - debug(str(len(unowned_functions)) + " global variables", 1) - for enum in enums: - debug("Enum " + assure_length(enum.name, len(max(enum_names, key=len)), True) + " contains " + assure_length(str(len(enum.values)), 2, False) + " values", 1) - debug("-"*col, 1) - wrapper_file = open(filename, "w+") - wrapper_file.write( -"""/* - * yosys -- Yosys Open SYnthesis Suite - * - * Copyright (C) 2012 Claire Xenia Wolf - * - * 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. - * - * This is a generated file and can be overwritten by make - */ - -#ifdef WITH_PYTHON -""") - for source in sources: - wrapper_file.write("#include \""+source.name+".h\"\n") - wrapper_file.write(""" -#include -#include -#include -#include -#include -#include // std::streamsize -#include -#include // boost::iostreams::sink -#include -USING_YOSYS_NAMESPACE - -using std::string_view; - -namespace YOSYS_PYTHON { - - [[noreturn]] static void log_python_exception_as_error() { - PyErr_Print(); - log_error("Python interpreter encountered an exception.\\n"); - } - - struct YosysStatics{}; -""") - - for source in sources: - for wclass in source.classes: - wrapper_file.write("\n\tstruct " + wclass.name + ";") - - wrapper_file.write("\n") - - for source in sources: - for wclass in source.classes: - wrapper_file.write(wclass.gen_decl(source.name)) - - wrapper_file.write("\n") - - for source in sources: - for wclass in source.classes: - wrapper_file.write(wclass.gen_funs(source.name)) - - for fun in unowned_functions: - wrapper_file.write(fun.gen_def()) - - for glbl in glbls: - wrapper_file.write(glbl.gen_def()) - - wrapper_file.write(""" struct Initializer - { - Initializer() { - if(!Yosys::yosys_already_setup()) - { - Yosys::log_streams.push_back(&std::cout); - Yosys::log_error_stderr = true; - Yosys::yosys_setup(); - } - } - - Initializer(Initializer const &) {} - - ~Initializer() { - Yosys::yosys_shutdown(); - } - }; - - - /// source: https://stackoverflow.com/questions/26033781/converting-python-io-object-to-stdostream-when-using-boostpython?noredirect=1&lq=1 - /// @brief Type that implements the Boost.IOStream's Sink and Flushable - /// concept for writing data to Python object that support: - /// n = object.write(str) # n = None or bytes written - /// object.flush() # if flush exists, then it is callable - class PythonOutputDevice - { - public: - - // This class models both the Sink and Flushable concepts. - struct category - : boost::iostreams::sink_tag, - boost::iostreams::flushable_tag - {}; - - explicit - PythonOutputDevice(boost::python::object object) - : object_(object) - {} - - // Sink concept. - public: - - typedef char char_type; - - std::streamsize write(const char* buffer, std::streamsize buffer_size) - { - namespace python = boost::python; - // Copy the buffer to a python string. - python::str data(buffer, buffer_size); - - // Invoke write on the python object, passing in the data. The following - // is equivalent to: - // n = object_.write(data) - python::extract bytes_written( - object_.attr("write")(data)); - - // Per the Sink concept, return the number of bytes written. If the - // Python return value provides a numeric result, then use it. Otherwise, - // such as the case of a File object, use the buffer_size. - return bytes_written.check() - ? bytes_written - : buffer_size; - } - - // Flushable concept. - public: - - bool flush() - { - // If flush exists, then call it. - boost::python::object flush = object_.attr("flush"); - if (!flush.is_none()) - { - flush(); - } - - // Always return true. If an error occurs, an exception should be thrown. - return true; - } - - private: - boost::python::object object_; - }; - - /// @brief Use an auxiliary function to adapt the legacy function. - void log_to_stream(boost::python::object object) - { - // Create an ostream that delegates to the python object. - boost::iostreams::stream* output = new boost::iostreams::stream(object); - Yosys::log_streams.insert(Yosys::log_streams.begin(), output); - }; - - - BOOST_PYTHON_MODULE(libyosys) - { - using namespace boost::python; - - class_("Initializer"); - scope().attr("_hidden") = new Initializer(); - - def("log_to_stream", &log_to_stream); -""") - - for enum in enums: - wrapper_file.write(enum.gen_boost_py()) - - for source in sources: - for wclass in source.classes: - wrapper_file.write(wclass.gen_boost_py()) - - for fun in unowned_functions: - wrapper_file.write(fun.gen_boost_py()) - - wrapper_file.write("\n\n\t\tclass_(\"Yosys\")\n") - for glbl in glbls: - wrapper_file.write(glbl.gen_boost_py()) - wrapper_file.write("\t\t;\n") - - wrapper_file.write("\n\t}\n}\n#endif") - -def print_includes(): - for source in sources: - print(source.name + ".pyh") diff --git a/passes/cmds/plugin.cc b/passes/cmds/plugin.cc index afa830552..e1e888a85 100644 --- a/passes/cmds/plugin.cc +++ b/passes/cmds/plugin.cc @@ -24,12 +24,6 @@ # include #endif -#ifdef WITH_PYTHON -# include -# include -# include -#endif - YOSYS_NAMESPACE_BEGIN std::map loaded_plugins; @@ -57,23 +51,23 @@ void load_plugin(std::string filename, std::vector aliases) if (!is_loaded) { // Check if we're loading a python script - if(filename.find(".py") != std::string::npos) - { + if (filename.rfind(".py") != std::string::npos) { #ifdef WITH_PYTHON - boost::filesystem::path full_path(filename); - std::string path(full_path.parent_path().c_str()); - filename = full_path.filename().c_str(); - filename = filename.substr(0,filename.size()-3); - PyRun_SimpleString(("sys.path.insert(0,\""+path+"\")").c_str()); - PyErr_Print(); - PyObject *module_p = PyImport_ImportModule(filename.c_str()); - if(module_p == NULL) - { - PyErr_Print(); - log_cmd_error("Can't load python module `%s'\n", full_path.filename()); + py::object Path = py::module_::import("pathlib").attr("Path"); + py::object full_path = Path(py::cast(filename)); + py::object plugin_python_path = full_path.attr("parent"); + auto basename = py::cast(full_path.attr("stem")); + + py::object sys = py::module_::import("sys"); + sys.attr("path").attr("insert")(0, py::str(plugin_python_path)); + + try { + auto module_container = py::module_::import(basename.c_str()); + loaded_python_plugins[orig_filename] = module_container.ptr(); + } catch (py::error_already_set &e) { + log_cmd_error("Can't load python module `%s': %s\n", basename, e.what()); return; } - loaded_python_plugins[orig_filename] = module_p; Pass::init_register(); #else log_error( diff --git a/pyosys/.gitignore b/pyosys/.gitignore new file mode 100644 index 000000000..f9fbdf4f6 --- /dev/null +++ b/pyosys/.gitignore @@ -0,0 +1 @@ +wrappers.cc diff --git a/misc/__init__.py b/pyosys/__init__.py similarity index 99% rename from misc/__init__.py rename to pyosys/__init__.py index d74e3f5bd..4622464da 100644 --- a/misc/__init__.py +++ b/pyosys/__init__.py @@ -1,3 +1,4 @@ + import os import sys diff --git a/pyosys/generator.py b/pyosys/generator.py new file mode 100644 index 000000000..ebe89737e --- /dev/null +++ b/pyosys/generator.py @@ -0,0 +1,2039 @@ +#!/usr/bin/env python3 +# yosys -- Yosys Open SYnthesis Suite +# +# Copyright (C) 2012 Claire Xenia Wolf +# +# 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. +# +# Author: Benedikt Tutzer +# Modified for pybind11 by: Mohamed Gaber + +import argparse +import copy +from enum import Enum +from io import StringIO +from pathlib import Path +from functools import wraps +from typing import ClassVar, Optional + +__file_dir__ = Path(__file__).absolute().parent + + +def autostring(fn): + @wraps(fn) + def wrapper(*args, **kwargs): + if "stream" not in kwargs: + stream = StringIO() + fn(*args, stream=stream, **kwargs) + return stream.getvalue() + else: + fn(*args, **kwargs) + + return wrapper + + +# Map c++ operator Syntax to Python functions +wrappable_operators = { + "<": "__lt__", + "==": "__eq__", + "!=": "__ne__", + "+": "__add__", + "-": "__sub__", + "*": "__mul__", + "/": "__div__", + "()": "__call__", +} + +# Restrict certain strings from being function names in Python +keyword_aliases = { + "in": "in_", + "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_", +} + +# These can be used without any explicit conversion +autocast_types = { + "void": "void", + "bool": "bool", + "int": "int", + "double": "double", + "size_t": "size_t", + "std::string": "std::string", + "string": "string", + "char_p": "char_p", + "std::source_location": "std::source_location", + "source_location": "source_location", + "State": "RTLIL::State", + # trampoline types + "Pass": "RTLIL::Pass", + "Monitor": "RTLIL::Monitor", +} + +def strip_std_prefix(str_in): + # removesuffix is python 3.9+ + std_namespace = "std::" + if str_in.startswith(std_namespace): + return str_in[len(std_namespace):] + return str_in + + +# Ways to link between Python- and C++ Objects +class link_types(Enum): + global_list = 1 # Identical to pointer, kept for historical reasons + ref_copy = 2 # Copy + pointer = 3 # The Python Object contains a pointer to its C++ + # counterpart + derive = 4 # Identical to ref_copy, kept for historical reasons + + +class attr_types(Enum): + star = "*" + amp = "&" + ampamp = "&&" + default = "" + + +# For source-files +class Source: + name = "" + classes = [] + + def __init__(self, name, classes): + self.name = name + self.classes = classes + + +# Splits a list by the given delimiter, without splitting strings inside +# pointy-brackets (< and >) +def split_list(str_def, delim): + str_def = str_def.strip() + if len(str_def) == 0: + return [] + if str_def.count(delim) == 0: + return [str_def] + if str_def.count("<") == 0: + return str_def.split(delim) + if str_def.find("<") < str_def.find(" "): + closing = find_closing( + str_def[str_def.find("<") + 1 :], "<", ">" + ) + str_def.find("<") + comma = str_def[closing:].find(delim) + if comma == -1: + return [str_def] + comma = closing + comma + else: + comma = str_def.find(delim) + rest = split_list(str_def[comma + 1 :], delim) + ret = [str_def[:comma]] + if rest != None and len(rest) != 0: + ret.extend(rest) + return ret + + +# Represents a Type +class WType: + name = "" + cont = None + attr_type = attr_types.default + + def __init__(self, name="", cont=None, attr_type=attr_types.default): + self.name = name + self.cont = cont + self.attr_type = attr_type + + # Python type-string + def gen_text(self): + text = self.name + if self.name in enum_names: + text = enum_by_name(self.name).namespace + "::" + self.name + if self.cont != None: + return self.cont.gen_identifier() + return text + + # C++ type-string + def gen_text_cpp(self): + postfix = "" + if self.attr_type == attr_types.star: + postfix = " *" + elif self.attr_type == attr_types.amp: + postfix = " &" + elif self.attr_type == attr_types.ampamp: + postfix = " &&" + if self.name in classnames: + return class_by_name(self.name).namespace + "::" + self.name + postfix + if self.name in enum_names: + return enum_by_name(self.name).namespace + "::" + self.name + postfix + if self.name in autocast_types: + return autocast_types[self.name] + postfix + text = self.name + if self.cont != None: + text += "<" + for a in self.cont.args: + text += a.gen_text_cpp() + ", " + text = text[:-2] + text += ">" + return text + + @staticmethod + def from_string(str_def, containing_file, line_number): + str_def = str_def.strip() + if len(str_def) == 0: + return None + str_def = str_def.replace( + "RTLIL::SigSig", "std::pair" + ).replace("SigSig", "std::pair") + t = WType() + t.name = "" + t.cont = None + t.attr_type = attr_types.default + if str_def.find("<") != -1: # and str_def.find("<") < str_def.find(" "): + str_def = str_def.replace("const ", "") + + candidate = WContainer.from_string(str_def, containing_file, line_number) + if candidate == None: + return None + t.name = str_def[: str_def.find("<")] + + if t.name.count("*") + t.name.count("&") > 1: + return None + + if t.name.count("*") == 1 or str_def[0] == "*" or str_def[-1] == "*": + t.attr_type = attr_types.star + t.name = t.name.replace("*", "") + elif t.name.count("&&") == 1: + t.attr_type = attr_types.ampamp + t.name = t.name.replace("&&", "") + elif t.name.count("&") == 1 or str_def[0] == "&" or str_def[-1] == "&": + t.attr_type = attr_types.amp + t.name = t.name.replace("&", "") + + t.cont = candidate + if t.name not in known_containers: + return None + return t + + prefix = "" + + if str.startswith(str_def, "const "): + if "char_p" in str_def: + prefix = "const " + str_def = str_def[6:] + if str.startswith(str_def, "unsigned "): + prefix = "unsigned " + prefix + str_def = str_def[9:] + while str.startswith(str_def, "long "): + prefix = "long " + prefix + str_def = str_def[5:] + while str.startswith(str_def, "short "): + prefix = "short " + prefix + str_def = str_def[6:] + + str_def = str_def.split("::")[-1] + + if str_def.count("*") + str_def.count("&") >= 2: + return None + + if str_def.count("*") == 1: + t.attr_type = attr_types.star + str_def = str_def.replace("*", "") + elif str_def.count("&&") == 1: + t.attr_type = attr_types.ampamp + str_def = str_def.replace("&&", "") + elif str_def.count("&") == 1: + t.attr_type = attr_types.amp + str_def = str_def.replace("&", "") + + if ( + len(str_def) > 0 + and str_def.split("::")[-1] not in autocast_types + and str_def.split("::")[-1] not in classnames + and str_def.split("::")[-1] not in enum_names + ): + return None + + if str_def.count(" ") == 0: + t.name = (prefix + str_def).replace("char_p", "char *") + t.cont = None + return t + return None + + def gen_identifier(self): + if self.cont: + return self.cont.gen_identifier() + return self.name.title() + + def as_wclass(self) -> Optional["WClass"]: + return class_by_name(self.name) + + def __repr__(self): + return f"{self.__class__.__qualname__}(**{repr(self.__dict__)})" + + +# Associate the "Translators" with their c++ type +known_containers = { + "std::set", + "std::vector", + "std::map", + "std::pair", + "pool", + "idict", + "dict", + "RTLIL::ObjRange" +} + +# Represents a container-type +class WContainer: + name = "" + args = [] + + def from_string(str_def, containing_file, line_number): + if str_def == None or len(str_def) < 4: + return None + cont = WContainer() + cont.name = str_def[: str_def.find("<")] + str_def = str_def[str_def.find("<") + 1 : find_closing(str_def, "<", ">")] + cont.args = [] + for arg in split_list(str_def, ","): + candidate = WType.from_string(arg.strip(), containing_file, line_number) + if candidate == None: + return None + if candidate.name == "void": + return None + cont.args.append(candidate) + return cont + + # generate the c++ type string + def gen_type_cpp(self): + tpl_args = [] + for arg in self.args: + postfix = (arg.attr_type == attr_types.star) * " *" + if arg.name in autocast_types: + tpl_args.append(autocast_types[arg.name] + postfix) + elif arg.name in known_containers: + tpl_args.append(arg.cont.gen_type_cpp()) + else: + tpl_args.append(arg.as_wclass().fully_qualified_name() + postfix) + return f'{self.name}<{ ", ".join(tpl_args) }>' + + def gen_identifier(self): + container = strip_std_prefix(self.name).title() + + if container == "Dict": + assert len(self.args) == 2 + return f"{self.args[0].gen_identifier()}To{self.args[1].gen_identifier()}{container}" + + args = [] + for arg in self.args: + arg_name = arg.name.title() + if arg.cont: + arg_name = arg.cont.gen_identifier() + args.append(arg_name) + args.append(container) + result = "".join(args) + if result == "SigspecSigspecPair": + return "SigSig" + return result + + @autostring + def gen_boost_py(self, *, stream): + bind_fn = "py::bind_" + strip_std_prefix(self.name) + tpl_args = [self.gen_type_cpp()] + if bind_fn != "py::bind_vector": + # using custom bind function, can't use ::value so need more + # template arguments + for arg in self.args: + postfix = (arg.attr_type == attr_types.star) * " *" + if arg.name in autocast_types: + tpl_args.append(autocast_types[arg.name] + postfix) + elif arg.name in known_containers: + tpl_args.append(arg.cont.gen_type_cpp()) + else: + tpl_args.append(arg.as_wclass().fully_qualified_name() + postfix) + if bind_fn == "py::bind_set": + bind_fn = "py::bind_pool" + stream.write(f'\t\t{bind_fn}<{",".join(tpl_args)}>(m, "{self.gen_identifier()}");\n') + + def __repr__(self): + return f"{self.__class__.__qualname__}(**{repr(self.__dict__)})" + + +class Attribute: + wtype = None + varname = None + is_const = False + default_value = None + pos = None + pos_counter = 0 + + def __init__(self, wtype, varname, is_const=False, default_value=None): + self.wtype = wtype + self.varname = varname + self.is_const = is_const + self.default_value = None + self.container = None + + @staticmethod + def from_string(str_def, containing_file, line_number): + if len(str_def) < 3: + return None + orig = str_def + arg = Attribute(None, None) + prefix = "" + arg.wtype = None + arg.varname = None + arg.is_const = False + arg.default_value = None + arg.container = None + if str.startswith(str_def, "const "): + arg.is_const = True + str_def = str_def[6:] + if str.startswith(str_def, "unsigned "): + prefix = "unsigned " + str_def = str_def[9:] + while str.startswith(str_def, "long "): + prefix = "long " + prefix + str_def = str_def[5:] + while str.startswith(str_def, "short "): + prefix = "short " + prefix + str_def = str_def[6:] + + if str_def.find("<") != -1 and str_def.find("<") < str_def.find(" "): + closing = ( + find_closing(str_def[str_def.find("<") :], "<", ">") + + str_def.find("<") + + 1 + ) + arg.wtype = WType.from_string( + str_def[:closing].strip(), containing_file, line_number + ) + str_def = str_def[closing + 1 :] + else: + if str_def.count(" ") > 0: + arg.wtype = WType.from_string( + prefix + str_def[: str_def.find(" ")].strip(), + containing_file, + line_number, + ) + str_def = str_def[str_def.find(" ") + 1 :] + else: + arg.wtype = WType.from_string( + prefix + str_def.strip(), containing_file, line_number + ) + str_def = "" + arg.varname = "" + + if arg.wtype == None: + return None + if str_def.count("=") == 0: + arg.varname = str_def.strip() + if arg.varname.find(" ") > 0: + return None + else: + arg.varname = str_def[: str_def.find("=")].strip() + if arg.varname.find(" ") > 0: + return None + str_def = str_def[str_def.find("=") + 1 :].strip() + arg.default_value = str_def[arg.varname.find("=") + 1 :].strip() + if len(arg.varname) == 0: + arg.varname = None + return arg + if arg.varname[0] == "*": + arg.wtype.attr_type = attr_types.star + arg.varname = arg.varname[1:] + elif arg.varname[0] == "&": + if arg.wtype.attr_type != attr_types.default: + return None + if arg.varname[1] == "&": + arg.wtype.attr_type = attr_types.ampamp + arg.varname = arg.varname[2:] + else: + arg.wtype.attr_type = attr_types.amp + arg.varname = arg.varname[1:] + return arg + + # Generates the varname. If the attribute has no name in the header file, + # a name is generated + def gen_varname(self): + if self.varname != None: + return self.varname + if self.wtype.name == "void": + return "" + if self.pos == None: + self.pos = Attribute.pos_counter + Attribute.pos_counter = Attribute.pos_counter + 1 + return "gen_varname_" + str(self.pos) + + # Generates the test for the function headers with c++ types + def gen_listitem_cpp(self, include_varname=True): + postfix = self.gen_varname() * include_varname + prefix = "" + if self.is_const: + prefix = "const " + infix = "" + if self.wtype.attr_type == attr_types.star: + infix = "*" + elif self.wtype.attr_type == attr_types.amp: + infix = "&" + elif self.wtype.attr_type == attr_types.ampamp: + infix = "&&" + if self.wtype.name in known_containers: + return ( + prefix + + self.wtype.cont.gen_type_cpp() + + " " + + infix + + postfix + ) + if self.wtype.name in classnames: + return ( + prefix + + class_by_name(self.wtype.name).namespace + + "::" + + self.wtype.name + + " " + + infix + + postfix + ) + return prefix + self.wtype.name + " " + infix + postfix + + def gen_listitem_pyarg(self): + default = "" + if self.default_value is not None: + default = f" = {self.default_value}" + return f'py::arg("{self.varname}"){default}' + + # Generates the listitem withtout the varname, so the signature can be + # compared + def gen_listitem_hash(self): + prefix = "" + if self.is_const: + prefix = "const " + if self.wtype.name in classnames: + return prefix + self.wtype.name + "* " + if self.wtype.name in known_containers: + return self.wtype.cont.gen_identifier() + return prefix + self.wtype.name + + +class WClass: + name = None + namespace = None + link_type = None + base_class = None + id_ = None + string_id = None + hash_id = None + needs_clone = False + found_funs = [] + found_vars = [] + found_constrs = [] + + def __init__( + self, + name, + link_type, + *, + id_=None, + string_id=None, + hash_id=None, + needs_clone=False, + ): + self.name = name + self.namespace = None + self.base_class = None + self.link_type = link_type + self.id_ = id_ + self.string_id = string_id + self.hash_id = hash_id + self.needs_clone = needs_clone + self.found_funs = [] + self.found_vars = [] + self.found_constrs = [] + + @autostring + def gen_boost_py(self, *, stream): + name_qualified = f"{self.namespace}::{self.name}" + tpl_args = [name_qualified] + if self.link_type in [link_types.pointer, link_types.global_list]: + tpl_args.append(f"std::unique_ptr<{name_qualified}, py::nodelete>") + stream.write(f'\t\tpy::class_<{", ".join(tpl_args)}>(m, "{self.name}")\n') + for con in self.found_constrs: + # HACK: skip move constructors + if "&&" in con.orig_text: + continue + con.gen_boost_py(stream=stream) + for fun in sorted(self.found_funs, key=lambda f: (f.is_operator, f.name)): + fun.gen_boost_py(stream=stream) + if self.string_id: + stream.write( + f'\t\t\t.def("__str__", [](const {name_qualified} &self){{ return self.{self.string_id}; }})\n' + ) + if self.hash_id: + hash_member = f".{self.hash_id}" if self.hash_id != "" else "" + stream.write( + f'\t\t\t.def("__hash__", [](const {name_qualified} &self){{ return run_hash(self{hash_member}); }})\n' + ) + for var in self.found_vars: + var.gen_boost_py(stream=stream) + stream.write("\t\t;\n") + + def fully_qualified_name(self) -> str: + return f"{self.namespace}::{self.name}" + + def __repr__(self): + return f"{self.__class__.__qualname__}({repr(self.__dict__)})" + + +# CONFIGURE HEADER-FILES TO BE PARSED AND CLASSES EXPECTED IN THEM HERE + +sources = [ + Source( + "kernel/celltypes", + [ + WClass("CellType", link_types.ref_copy, hash_id="type", needs_clone=True), + WClass("CellTypes", link_types.ref_copy, needs_clone=True), + ], + ), + Source( + "kernel/consteval", [WClass("ConstEval", link_types.ref_copy, needs_clone=True)] + ), + Source("kernel/log", []), + Source( + "kernel/register", + [ + # WClass("Pass", link_types.derive, needs_clone=True), # Manual mapping because of virtual method + ], + ), + Source( + "kernel/rtlil", + [ + WClass("IdString", link_types.ref_copy, string_id="str()", hash_id="str()"), + WClass("Const", link_types.ref_copy, string_id="as_string()"), + WClass("AttrObject", link_types.ref_copy), + WClass("NamedObject", link_types.ref_copy), + WClass("Selection", link_types.ref_copy), + #WClass("Monitor", link_types.derive), # Moved to tpl for virtual methods + WClass("CaseRule", link_types.ref_copy, needs_clone=True), + WClass("SwitchRule", link_types.ref_copy, needs_clone=True), + WClass("SyncRule", link_types.ref_copy, needs_clone=True), + WClass( + "Process", link_types.pointer, string_id="name.c_str()", hash_id="name" + ), + WClass("SigChunk", link_types.ref_copy), + WClass("SigBit", link_types.ref_copy, hash_id=""), + WClass("SigSpec", link_types.ref_copy, hash_id=""), + WClass( + "Cell", + link_types.global_list, + id_=Attribute(WType("unsigned int"), "hashidx_"), + string_id="name.c_str()", + hash_id="", + ), + WClass( + "Wire", + link_types.global_list, + id_=Attribute(WType("unsigned int"), "hashidx_"), + string_id="name.c_str()", + hash_id="", + ), + WClass( + "Memory", + link_types.global_list, + id_=Attribute(WType("unsigned int"), "hashidx_"), + string_id="name.c_str()", + hash_id="", + ), + WClass( + "Module", + link_types.global_list, + id_=Attribute(WType("unsigned int"), "hashidx_"), + string_id="name.c_str()", + hash_id="", + ), + WClass( + "Design", + link_types.ref_copy, + id_=Attribute(WType("unsigned int"), "hashidx_"), + string_id="hashidx_", + hash_id="", + ), + ], + ), + # Source("kernel/satgen",[ + # ] + # ), + # Source("libs/ezsat/ezsat",[ + # ] + # ), + # Source("libs/ezsat/ezminisat",[ + # ] + # ), + Source( + "kernel/sigtools", [WClass("SigMap", link_types.ref_copy, needs_clone=True)] + ), + Source("kernel/yosys", []), + Source("kernel/cost", []), +] + +blacklist_methods = [ + "YOSYS_NAMESPACE::Pass::run_register", + "YOSYS_NAMESPACE::Module::Pow", + "YOSYS_NAMESPACE::RTLIL::Design::selected_whole_modules", + "YOSYS_NAMESPACE::RTLIL::AttrObject::get_blackbox_attribute" +] + +enum_names = ["State", "SyncType", "ConstFlags"] + +enums = [] # Do not edit +glbls = [] + +unowned_functions = [] + +classnames = [] +for source in sources: + for wclass in source.classes: + classnames.append(wclass.name) + + +def class_by_name(name): + for source in sources: + for wclass in source.classes: + if wclass.name == name: + return wclass + return None + + +def enum_by_name(name): + for e in enums: + if e.name == name: + return e + return None + + +def find_closing(text, open_tok, close_tok): + if text.find(open_tok) == -1 or text.find(close_tok) <= text.find(open_tok): + return text.find(close_tok) + return ( + text.find(close_tok) + + find_closing(text[text.find(close_tok) + 1 :], open_tok, close_tok) + + 1 + ) + + +def unpretty_string(s): + s = s.strip() + while s.find(" ") != -1: + s = s.replace(" ", " ") + while s.find("\t") != -1: + s = s.replace("\t", " ") + s = s.replace(" (", "(") + return s + + +class WEnum: + name = None + namespace = None + values = [] + + def from_string(str_def, namespace, line_number): + str_def = str_def.strip() + if not str.startswith(str_def, "enum "): + return None + if str_def.count(";") != 1: + return None + str_def = str_def[5:] + enum = WEnum() + split = str_def.split(":") + if len(split) != 2: + return None + enum.name = split[0].strip() + if enum.name not in enum_names: + return None + str_def = split[1] + if str_def.count("{") != str_def.count("}") != 1: + return None + if ( + len(str_def) < str_def.find("}") + 2 + or str_def[str_def.find("}") + 1] != ";" + ): + return None + str_def = str_def.split("{")[-1].split("}")[0] + enum.values = [] + for val in str_def.split(","): + enum.values.append(val.strip().split("=")[0].strip()) + enum.namespace = namespace + return enum + + @autostring + def gen_boost_py(self, *, stream): + stream.write( + f'\t\tpy::native_enum<{self.namespace}::{self.name}>(m, "{self.name}", "enum.Enum")\n' + ) + for value in self.values: + stream.write(f'\t\t\t.value("{value}", {self.namespace}::{value})\n') + stream.write("\t\t\t.finalize();\n") + + def __str__(self): + ret = "Enum " + self.namespace + "::" + self.name + "(\n" + for val in self.values: + ret = ret + "\t" + val + "\n" + return ret + ")" + + +class WConstructor: + orig_text = None + args = [] + containing_file = None + member_of = None + duplicate = False + protected = False + + def __init__(self, containing_file, class_): + self.orig_text = "Auto generated default constructor" + self.args = [] + self.containing_file = containing_file + self.member_of = class_ + self.protected = False + + def from_string(str_def, containing_file, class_, line_number, protected=False): + if class_ == None: + return None + if str_def.count("delete;") > 0: + return None + con = WConstructor(containing_file, class_) + con.orig_text = str_def + con.args = [] + con.duplicate = False + con.protected = protected + if str.startswith(str_def, "inline "): + str_def = str_def[7:] + if not str.startswith(str_def, class_.name + "("): + return None + str_def = str_def[len(class_.name) + 1 :] + found = find_closing(str_def, "(", ")") + if found == -1: + return None + str_def = str_def[0:found].strip() + if len(str_def) == 0: + return con + args = split_list(str_def, ",") + for i, arg in enumerate(args): + parsed = Attribute.from_string(arg.strip(), containing_file, line_number) + if parsed == None: + return None + # Only allow std::source_location as defaulted last argument, and + # don't append so it takes default value + if parsed.wtype.name in ["std::source_location", "source_location"]: + if parsed.default_value is None or i != len(args) - 1: + debug( + "std::source_location not defaulted last arg of " + + class_.name + + " is unsupported", + 2, + ) + return None + continue + con.args.append(parsed) + return con + + def gen_decl_hash_py(self): + text = self.member_of.name + "(" + for arg in self.args: + text += arg.gen_listitem_hash() + ", " + if len(self.args) > 0: + text = text[:-2] + text += ");" + return text + + def overload_params(self): + return ", ".join([a.gen_listitem_cpp(include_varname=False) for a in self.args]) + + @autostring + def gen_boost_py(self, *, stream): + if self.duplicate or self.protected: + return + stream.write(f"\t\t\t.def(py::init<{self.overload_params()}>())\n") + + +class WFunction: + orig_text = None + is_static = False + is_inline = False + is_virtual = False + is_const = False + ret_attr_type = attr_types.default + is_operator = False + ret_type = None + name = None + alias = None + args = [] + containing_file = None + member_of = None + duplicate = False + namespace = "" + + def from_string(str_def, containing_file, class_, line_number, namespace): + if str_def.count("delete;") > 0: + return None + func = WFunction() + func.is_static = False + func.is_inline = False + func.is_virtual = False + func.is_const = False + func.ret_attr_type = attr_types.default + func.is_operator = False + func.member_of = None + func.orig_text = str_def + func.args = [] + func.containing_file = containing_file + func.member_of = class_ + func.duplicate = False + func.namespace = namespace + str_def = str_def.replace("operator ", "operator") + + # remove attributes from the start + if str.startswith(str_def, "[[") and "]]" in str_def: + str_def = str_def[str_def.find("]]") + 2 :] + + if str.startswith(str_def, "static "): + func.is_static = True + str_def = str_def[7:] + else: + func.is_static = False + if str.startswith(str_def, "inline "): + func.is_inline = True + str_def = str_def[7:] + else: + func.is_inline = False + if str.startswith(str_def, "virtual "): + func.is_virtual = True + str_def = str_def[8:] + else: + func.is_virtual = False + + if str_def.count(" ") == 0: + return None + + parts = split_list(str_def.strip(), " ") + + prefix = "" + i = 0 + for part in parts: + if part in ["unsigned", "long", "short", "const"]: + prefix += part + " " + i += 1 + else: + break + parts = parts[i:] + + if len(parts) <= 1: + return None + + func.ret_type = WType.from_string( + prefix + parts[0], containing_file, line_number + ) + + if func.ret_type == None: + return None + + str_def = parts[1] + for part in parts[2:]: + str_def = str_def + " " + part + + found = str_def.find("(") + if found == -1 or (str_def.find(" ") != -1 and found > str_def.find(" ")): + return None + func.name = str_def[:found] + str_def = str_def[found:] + if func.name.find("operator") != -1 and str.startswith(str_def, "()("): + func.name += "()" + str_def = str_def[2:] + str_def = str_def[1:] + if func.name.find("operator") != -1: + func.is_operator = True + if func.name.find("*") == 0: + func.name = func.name.replace("*", "") + func.ret_type.attr_type = attr_types.star + if func.name.find("&&") == 0: + func.name = func.name.replace("&&", "") + func.ret_type.attr_type = attr_types.ampamp + if func.name.find("&") == 0: + func.name = func.name.replace("&", "") + func.ret_type.attr_type = attr_types.amp + + found = find_closing(str_def, "(", ")") + if found == -1: + return None + + post_qualifiers = str_def[found + 1 :].lstrip() + if post_qualifiers.startswith("const"): + func.is_const = True + + str_def = str_def[0:found] + if func.name in blacklist_methods: + return None + if func.namespace != None and func.namespace != "": + if (func.namespace + "::" + func.name) in blacklist_methods: + return None + if func.member_of != None: + if ( + func.namespace + "::" + func.member_of.name + "::" + func.name + ) in blacklist_methods: + return None + if ( + func.is_operator + and func.name.replace(" ", "").replace("operator", "").split("::")[-1] + not in wrappable_operators + ): + return None + + testname = func.name + if func.is_operator: + testname = testname[: testname.find("operator")] + if ( + testname.count(")") != 0 + or testname.count("(") != 0 + or testname.count("~") != 0 + or testname.count(";") != 0 + or testname.count(">") != 0 + or testname.count("<") != 0 + or testname.count("throw") != 0 + ): + return None + + func.alias = func.name + if func.name in keyword_aliases: + func.alias = keyword_aliases[func.name] + str_def = str_def[:found].strip() + if len(str_def) == 0: + return func + args = split_list(str_def, ",") + for i, arg in enumerate(args): + if arg.strip() == "...": + continue + parsed = Attribute.from_string(arg.strip(), containing_file, line_number) + if parsed == None: + return None + # Only allow std::source_location as defaulted last argument, and + # don't append so it takes default value + if parsed.wtype.name in ["std::source_location", "source_location"]: + if parsed.default_value is None or i != len(args) - 1: + debug( + "std::source_location not defaulted last arg of " + + func.name + + " is unsupported", + 2, + ) + return None + continue + func.args.append(parsed) + # handle (void) parameter declarations + if len(func.args) == 1 and func.args[0].wtype.name == "void": + func.args = [] + return func + + @property + def mangled_name(self): + mangled_typename = ( + lambda code: code.replace("::", "_") + .replace("<", "_") + .replace(">", "_") + .replace(" ", "") + .replace("*", "") + .replace(",", "") + ) + + return self.name + "".join( + f"__{mangled_typename(arg.wtype.gen_text_cpp())}" for arg in self.args + ) + + def gen_alias(self): + self.alias = self.mangled_name + + def gen_post_qualifiers(self, derived=False): + if ( + self.member_of != None + and self.member_of.link_type == link_types.derive + and self.is_virtual + and derived + ): + # we drop the qualifiers when deriving callbacks to be implemented in Python + return "" + return " const" if self.is_const else "" + + def gen_decl_hash_py(self): + text = self.ret_type.gen_text() + " " + self.alias + "(" + for arg in self.args: + text += arg.gen_listitem_hash() + ", " + if len(self.args) > 0: + text = text[:-2] + text += ");" + return text + + def overload_params(self): + return ", ".join([a.gen_listitem_cpp(False) for a in self.args]) + + def py_args(self): + return ", ".join([a.gen_listitem_pyarg() for a in self.args]) + + def wrapper_params(self): + return ", ".join([a.gen_listitem_cpp() for a in self.args]) + + def wrapper_args(self): + varnames = [] + for a in self.args: + if a.varname == "format": + # HACK: handle format strings (by ignoring the format part) + varnames.extend(['"%s"', "format"]) + else: + varnames.append(a.varname) + return ", ".join(varnames) + + @autostring + def gen_boost_py(self, *, stream): + if self.duplicate: + return + + fully_qualify = False + if self.member_of is not None and ( + (self.member_of.name == "IdString" and self.name == "in") + or (self.member_of.name == "Design" and self.name == "selection") + ): + fully_qualify = True + + # HACK: skip methods with inline-related nonsense + if self.alias in [ + "log_id", + "log_dump_val_worker", + "log_dump_args_worker", + "GetSize", + ]: + return + + prefix = "\t\tm" + ns = self.namespace + if self.member_of: + prefix = "\t\t\t" + ns = self.member_of.fully_qualified_name() + + stream.write(f"{prefix}.def") + if self.member_of and self.is_static: + stream.write("_static") + stream.write("(") + + if self.is_operator: + stream.write(f"py::self {self.name[len('operator'):]} py::self") + else: + stream.write(f'"{self.alias}", ') + # HACK: wrap variadics by only allowing a string + if "..." in self.orig_text: + stream.write( + f"[]({self.wrapper_params()}) {{ {self.namespace}::{self.name}({self.wrapper_args()}); }}" + ) + else: + + # HACK: Some of these needs special handling, i.e., a FULL + # overload disambiguation. Not sure why. Something to do with + # inlines and overloads. + # + # In theory, this disambiguation should work for everything but + # WType doesn't properly retain const return types yet. + if fully_qualify: + stream.write( + f"static_cast < {self.ret_type.gen_text_cpp()} ({ns}::*)({self.overload_params()}) {self.gen_post_qualifiers()} >(" + ) + elif not len(self.args) == 0: + stream.write(f"py::overload_cast<{self.overload_params()}>(") + stream.write(f"&{ns}::{self.name}") + if fully_qualify: + stream.write(")") + elif not len(self.args) == 0: + if self.is_const: + stream.write(f", py::const_") + stream.write(")") + py_args = self.py_args() + if len(py_args): + stream.write(", ") + stream.write(py_args) + stream.write(")\n" if self.member_of is not None else ");\n") + + def __repr__(self): + return f"{self.__class__.__qualname__}({repr(self.__dict__)})" + + +class WMember: + opaque_containers: ClassVar[dict] = dict() + orig_text = None + wtype = attr_types.default + name = None + containing_file = None + member_of = None + namespace = "" + is_const = False + + def from_string(str_def, containing_file, class_, line_number, namespace): + member = WMember() + member.orig_text = str_def + member.wtype = None + member.name = "" + member.containing_file = containing_file + member.member_of = class_ + member.namespace = namespace + member.is_const = False + + if str.startswith(str_def, "const "): + member.is_const = True + str_def = str_def[6:] + + if str_def.count(" ") == 0: + return None + + parts = split_list(str_def.strip(), " ") + + prefix = "" + i = 0 + for part in parts: + if part in ["unsigned", "long", "short"]: + prefix += part + " " + i += 1 + else: + break + parts = parts[i:] + + if len(parts) <= 1: + return None + + member.wtype = WType.from_string( + prefix + parts[0], containing_file, line_number + ) + + if member.wtype == None: + return None + + str_def = parts[1] + for part in parts[2:]: + str_def = str_def + " " + part + + if ( + str_def.find("(") != -1 + or str_def.find(")") != -1 + or str_def.find("{") != -1 + or str_def.find("}") != -1 + ): + return None + + found = str_def.find(";") + if found == -1: + return None + + found_eq = str_def.find("=") + if found_eq != -1: + found = found_eq + + member.name = str_def[:found] + str_def = str_def[found + 1 :] + if member.name.find("*") == 0: + member.name = member.name.replace("*", "") + member.wtype.attr_type = attr_types.star + if member.name.find("&&") == 0: + member.name = member.name.replace("&&", "") + member.wtype.attr_type = attr_types.ampamp + if member.name.find("&") == 0: + member.name = member.name.replace("&", "") + member.wtype.attr_type = attr_types.amp + + if len(str_def.strip()) != 0: + return None + + if len(member.name.split(",")) > 1: + member_list = [] + for name in member.name.split(","): + name = name.strip() + member_list.append(WMember()) + member_list[-1].orig_text = member.orig_text + member_list[-1].wtype = member.wtype + member_list[-1].name = name + member_list[-1].containing_file = member.containing_file + member_list[-1].member_of = member.member_of + member_list[-1].namespace = member.namespace + member_list[-1].is_const = member.is_const + return member_list + + if member.wtype.cont: + WMember.opaque_containers[member.wtype.gen_identifier()] = member.wtype + + return member + + @autostring + def gen_boost_py(self, *, stream): + if False and self.wtype.attr_type == attr_types.default: + property_type = self.wtype.gen_text_cpp() + stream.write(f'\t\t\t.def_property("{self.name}",\n') + stream.write(f'\t\t\t\t[]({self.member_of.fully_qualified_name()} &o) -> {property_type}& {{ return o.{self.name}; }},\n') + stream.write(f'\t\t\t\t[]({self.member_of.fully_qualified_name()} &o, {property_type} const &p) {{ o.{self.name} = p; }},\n') + stream.write(f'\t\t\t\tpy::return_value_policy::reference_internal\n') + stream.write(f'\t\t\t)\n') + else: + meth = "def_readonly" if self.is_const else "def_readwrite" + stream.write( + f'\t\t\t.{meth}("{self.name}", &{self.member_of.fully_qualified_name()}::{self.name})\n' + ) + + def __repr__(self): + return f"{self.__class__.__qualname__}({repr(self.__dict__)})" + + +class WGlobal: + orig_text = None + wtype = attr_types.default + name = None + containing_file = None + namespace = "" + is_const = False + + def from_string(str_def, containing_file, line_number, namespace): + glbl = WGlobal() + glbl.orig_text = str_def + glbl.wtype = None + glbl.name = "" + glbl.containing_file = containing_file + glbl.namespace = namespace + glbl.is_const = False + + if not str.startswith(str_def, "extern"): + return None + str_def = str_def[7:] + + if str.startswith(str_def, "const "): + glbl.is_const = True + str_def = str_def[6:] + + if str_def.count(" ") == 0: + return None + + parts = split_list(str_def.strip(), " ") + + prefix = "" + i = 0 + for part in parts: + if part in ["unsigned", "long", "short"]: + prefix += part + " " + i += 1 + else: + break + parts = parts[i:] + + if len(parts) <= 1: + return None + + glbl.wtype = WType.from_string(prefix + parts[0], containing_file, line_number) + + if glbl.wtype == None: + return None + + str_def = parts[1] + for part in parts[2:]: + str_def = str_def + " " + part + + if ( + str_def.find("(") != -1 + or str_def.find(")") != -1 + or str_def.find("{") != -1 + or str_def.find("}") != -1 + ): + return None + + found = str_def.find(";") + if found == -1: + return None + + found_eq = str_def.find("=") + if found_eq != -1: + found = found_eq + + glbl.name = str_def[:found] + str_def = str_def[found + 1 :] + if glbl.name.find("*") == 0: + glbl.name = glbl.name.replace("*", "") + glbl.wtype.attr_type = attr_types.star + if glbl.name.find("&&") == 0: + glbl.name = glbl.name.replace("&&", "") + glbl.wtype.attr_type = attr_types.ampamp + if glbl.name.find("&") == 0: + glbl.name = glbl.name.replace("&", "") + glbl.wtype.attr_type = attr_types.amp + + if len(str_def.strip()) != 0: + return None + + if len(glbl.name.split(",")) > 1: + glbl_list = [] + for name in glbl.name.split(","): + name = name.strip() + glbl_list.append(WGlobal()) + glbl_list[-1].orig_text = glbl.orig_text + glbl_list[-1].wtype = glbl.wtype + glbl_list[-1].name = name + glbl_list[-1].containing_file = glbl.containing_file + glbl_list[-1].namespace = glbl.namespace + glbl_list[-1].is_const = glbl.is_const + return glbl_list + + return glbl + + @autostring + def gen_boost_py(self, *, stream): + args = [ + f'"{self.name}"', + ] + meth = "def_readonly_static" + if not self.is_const: + meth = "def_readwrite_static" + args.append(f"&{self.namespace}::{self.name}") + stream.write(f'\t\t\t.{meth}({", ".join(args)})\n') + + +def concat_namespace(tuple_list): + if len(tuple_list) == 0: + return "" + ret = "" + for namespace in tuple_list: + ret += "::" + namespace[0] + return ret[2:] + + +def calc_ident(text): + if len(text) == 0 or text[0] != " ": + return 0 + return calc_ident(text[1:]) + 1 + + +def assure_length(text, length, left=False): + if len(text) > length: + return text[:length] + if left: + return text + " " * (length - len(text)) + return " " * (length - len(text)) + text + + +def nesting_delta(s): + return s.count("{") - s.count("}") + + +def parse_header(source): + debug("Parsing " + source.name + ".pyh", 1) + source_file = open(source.name + ".pyh", "r") + + source_text = [] + in_line = source_file.readline() + + namespaces = [] + + while in_line: + if len(in_line) > 1: + source_text.append( + in_line.replace("char *", "char_p ").replace("char* ", "char_p ") + ) + in_line = source_file.readline() + + i = 0 + + namespaces = [] + classes = [] + private_segment = False + + while i < len(source_text): + line = ( + source_text[i] + .replace( + "YOSYS_NAMESPACE_BEGIN", + " namespace YOSYS_NAMESPACE{", + ) + .replace("YOSYS_NAMESPACE_END", " }") + ) + ugly_line = unpretty_string(line) + debug(f"READ:>> {line}", 2) + + # for anonymous unions, ignore union enclosure by skipping start line and replacing end line with new line + if "union {" in line: + j = i + 1 + while j < len(source_text): + union_line = source_text[j] + if "};" in union_line: + source_text[j] = "\n" + break + j += 1 + if j != len(source_text): + i += 1 + continue + + if str.startswith( + ugly_line, "namespace " + ): # and ugly_line.find("std") == -1 and ugly_line.find("__") == -1: + namespace_name = ugly_line[10:].replace("{", "").strip() + namespaces.append((namespace_name, ugly_line.count("{"))) + debug("-----NAMESPACE " + concat_namespace(namespaces) + "-----", 3) + i += 1 + continue + + if len(namespaces) != 0: + namespaces[-1] = ( + namespaces[-1][0], + namespaces[-1][1] + nesting_delta(ugly_line), + ) + if namespaces[-1][1] == 0: + debug("-----END NAMESPACE " + concat_namespace(namespaces) + "-----", 3) + namespaces.pop() + i += 1 + continue + + if ( + str.startswith(ugly_line, "struct ") or str.startswith(ugly_line, "class") + ) and ugly_line.count(";") == 0: + # Opening a record declaration which isn't a forward declaration + struct_name = ugly_line.split(" ")[1].split("::")[-1] + impl_namespaces = ugly_line.split(" ")[1].split("::")[:-1] + complete_namespace = concat_namespace(namespaces) + for namespace in impl_namespaces: + complete_namespace += "::" + namespace + debug("\tFound " + struct_name + " in " + complete_namespace, 2) + + base_class_name = None + if len(ugly_line.split(" : ")) > 1: # class is derived + deriv_str = ugly_line.split(" : ")[1] + if len(deriv_str.split("::")) > 1: # namespace of base class is given + base_class_name = deriv_str.split("::", 1)[1] + else: + base_class_name = deriv_str.split(" ")[0] + debug("\t " + struct_name + " is derived from " + base_class_name, 2) + base_class = class_by_name(base_class_name) + + c = (class_by_name(struct_name), ugly_line.count("{")) # calc_ident(line)) + debug(f"switch to {struct_name} in namespace {namespaces}", 2) + if struct_name in classnames: + c[0].namespace = complete_namespace + c[0].base_class = base_class + classes.append(c) + i += 1 + continue + + if len(classes): + c = (classes[-1][0], classes[-1][1] + nesting_delta(ugly_line)) + classes[-1] = c + if c[1] == 0: + if c[0] == None: + debug("\tExiting unknown class", 3) + else: + debug("\tExiting class " + c[0].name, 3) + classes.pop() + private_segment = False + i += 1 + continue + + class_ = classes[-1] if classes else None + + if class_ != None and ( + line.find("private:") != -1 or line.find("protected:") != -1 + ): + private_segment = True + i += 1 + continue + if class_ != None and line.find("public:") != -1: + private_segment = False + i += 1 + continue + + candidate = None + + if private_segment and class_ != None and class_[0] != None: + candidate = WConstructor.from_string( + ugly_line, source.name, class_[0], i, True + ) + if candidate != None: + debug( + '\t\tFound constructor of class "' + + class_[0].name + + '" in namespace ' + + concat_namespace(namespaces), + 2, + ) + class_[0].found_constrs.append(candidate) + i += 1 + continue + + if not private_segment and (class_ == None or class_[0] != None): + if class_ != None: + candidate = WFunction.from_string( + ugly_line, source.name, class_[0], i, concat_namespace(namespaces) + ) + else: + candidate = WFunction.from_string( + ugly_line, source.name, None, i, concat_namespace(namespaces) + ) + if candidate != None and candidate.name.find("::") == -1: + if class_ == None: + debug( + '\tFound unowned function "' + + candidate.name + + '" in namespace ' + + concat_namespace(namespaces), + 2, + ) + unowned_functions.append(candidate) + else: + debug( + '\t\tFound function "' + + candidate.name + + '" of class "' + + class_[0].name + + '" in namespace ' + + concat_namespace(namespaces), + 2, + ) + class_[0].found_funs.append(candidate) + else: + candidate = WEnum.from_string( + ugly_line, concat_namespace(namespaces), i + ) + if candidate != None: + enums.append(candidate) + debug( + '\tFound enum "' + + candidate.name + + '" in namespace ' + + concat_namespace(namespaces), + 2, + ) + elif class_ != None and class_[1] == 1: + candidate = WConstructor.from_string( + ugly_line, source.name, class_[0], i + ) + if candidate != None: + debug( + '\t\tFound constructor of class "' + + class_[0].name + + '" in namespace ' + + concat_namespace(namespaces), + 2, + ) + class_[0].found_constrs.append(candidate) + else: + candidate = WMember.from_string( + ugly_line, + source.name, + class_[0], + i, + concat_namespace(namespaces), + ) + if candidate != None: + if type(candidate) == list: + for c in candidate: + debug( + '\t\tFound member "' + + c.name + + '" of class "' + + class_[0].name + + '" of type "' + + c.wtype.name + + '"', + 2, + ) + class_[0].found_vars.extend(candidate) + else: + debug( + '\t\tFound member "' + + candidate.name + + '" of class "' + + class_[0].name + + '" of type "' + + candidate.wtype.name + + '"', + 2, + ) + class_[0].found_vars.append(candidate) + if candidate == None and class_ == None: + candidate = WGlobal.from_string( + ugly_line, source.name, i, concat_namespace(namespaces) + ) + if candidate != None: + if type(candidate) == list: + for c in candidate: + glbls.append(c) + debug( + '\tFound global "' + + c.name + + '" in namespace ' + + concat_namespace(namespaces), + 2, + ) + else: + glbls.append(candidate) + debug( + '\tFound global "' + + candidate.name + + '" in namespace ' + + concat_namespace(namespaces), + 2, + ) + + j = i + line = unpretty_string(line) + while ( + candidate == None + and j + 1 < len(source_text) + and line.count(";") <= 1 + and line.count("(") >= line.count(")") + ): + j += 1 + line = line + "\n" + unpretty_string(source_text[j]) + if class_ != None: + candidate = WFunction.from_string( + ugly_line, + source.name, + class_[0], + i, + concat_namespace(namespaces), + ) + else: + candidate = WFunction.from_string( + ugly_line, source.name, None, i, concat_namespace(namespaces) + ) + if candidate != None and candidate.name.find("::") == -1: + if class_ == None: + debug( + '\tFound unowned function "' + + candidate.name + + '" in namespace ' + + concat_namespace(namespaces), + 2, + ) + unowned_functions.append(candidate) + else: + debug( + '\t\tFound function "' + + candidate.name + + '" of class "' + + class_[0].name + + '" in namespace ' + + concat_namespace(namespaces), + 2, + ) + class_[0].found_funs.append(candidate) + continue + candidate = WEnum.from_string(line, concat_namespace(namespaces), i) + if candidate != None: + enums.append(candidate) + debug( + '\tFound enum "' + + candidate.name + + '" in namespace ' + + concat_namespace(namespaces), + 2, + ) + continue + if class_ != None: + candidate = WConstructor.from_string( + line, source.name, class_[0], i + ) + if candidate != None: + debug( + '\t\tFound constructor of class "' + + class_[0].name + + '" in namespace ' + + concat_namespace(namespaces), + 2, + ) + class_[0].found_constrs.append(candidate) + continue + if class_ == None: + candidate = WGlobal.from_string( + line, source.name, i, concat_namespace(namespaces) + ) + if candidate != None: + if type(candidate) == list: + for c in candidate: + glbls.append(c) + debug( + '\tFound global "' + + c.name + + '" in namespace ' + + concat_namespace(namespaces), + 2, + ) + else: + glbls.append(candidate) + debug( + '\tFound global "' + + candidate.name + + '" in namespace ' + + concat_namespace(namespaces), + 2, + ) + continue + if candidate != None: + while i < j: + i += 1 + line = ( + source_text[i] + .replace( + "YOSYS_NAMESPACE_BEGIN", + " namespace YOSYS_NAMESPACE{", + ) + .replace("YOSYS_NAMESPACE_END", " }") + ) + ugly_line = unpretty_string(line) + if len(namespaces) != 0: + namespaces[-1] = ( + namespaces[-1][0], + namespaces[-1][1] + nesting_delta(ugly_line), + ) + if namespaces[-1][1] == 0: + debug( + "-----END NAMESPACE " + + concat_namespace(namespaces) + + "-----", + 3, + ) + namespaces.pop() + if len(classes): + c = (classes[-1][0], classes[-1][1] + nesting_delta(ugly_line)) + classes[-1] = c + if c[1] == 0: + if c[0] == None: + debug("\tExiting unknown class", 3) + else: + debug("\tExiting class " + c[0].name, 3) + classes.pop() + private_segment = False + i += 1 + else: + i += 1 + + +def debug(message, level): + if level <= debug.debug_level: + print(message) + + +def expand_functions(): + global unowned_functions + new_funs = [] + for fun in unowned_functions: + new_funs.append(fun) + unowned_functions = new_funs + for source in sources: + for class_ in source.classes: + new_funs = [] + for fun in class_.found_funs: + new_funs.append(fun) + class_.found_funs = new_funs + + +def inherit_members(): + for source in sources: + for class_ in source.classes: + if class_.base_class: + base_funs = copy.deepcopy(class_.base_class.found_funs) + for fun in base_funs: + fun.member_of = class_ + fun.namespace = class_.namespace + base_vars = copy.deepcopy(class_.base_class.found_vars) + for var in base_vars: + var.member_of = class_ + var.namespace = class_.namespace + class_.found_funs.extend(base_funs) + class_.found_vars.extend(base_vars) + + +def clean_duplicates(): + for source in sources: + for class_ in source.classes: + known_decls = {} + for fun in class_.found_funs: + if fun.gen_decl_hash_py() in known_decls: + debug("Multiple declarations of " + fun.gen_decl_hash_py(), 3) + + other = known_decls[fun.gen_decl_hash_py()] + if fun.mangled_name == other.mangled_name: + fun.duplicate = True + debug('Disabled "' + fun.gen_decl_hash_py() + '"', 3) + continue + + other.gen_alias() + fun.gen_alias() + else: + known_decls[fun.gen_decl_hash_py()] = fun + known_decls = [] + for con in class_.found_constrs: + if con.gen_decl_hash_py() in known_decls: + debug("Multiple declarations of " + con.gen_decl_hash_py(), 3) + con.duplicate = True + else: + known_decls.append(con.gen_decl_hash_py()) + known_decls = [] + for fun in unowned_functions: + if fun.gen_decl_hash_py() in known_decls: + debug("Multiple declarations of " + fun.gen_decl_hash_py(), 3) + fun.duplicate = True + else: + known_decls.append(fun.gen_decl_hash_py()) + + +def gen_wrappers(filename, debug_level_=0): + filename = Path(filename) + + debug.debug_level = debug_level_ + for source in sources: + parse_header(source) + + expand_functions() + inherit_members() + clean_duplicates() + + import shutil + import math + + col = shutil.get_terminal_size((80, 20)).columns + debug("-" * col, 1) + debug( + "-" * math.floor((col - 7) / 2) + "SUMMARY" + "-" * math.ceil((col - 7) / 2), 1 + ) + debug("-" * col, 1) + for source in sources: + for class_ in source.classes: + debug( + "Class " + + assure_length(class_.name, len(max(classnames, key=len)), True) + + " contains " + + assure_length(str(len(class_.found_vars)), 3, False) + + " member variables, " + + assure_length(str(len(class_.found_funs)), 3, False) + + " methods and " + + assure_length(str(len(class_.found_constrs)), 2, False) + + " constructors", + 1, + ) + if len(class_.found_constrs) == 0: + class_.found_constrs.append(WConstructor(source.name, class_)) + debug(str(len(unowned_functions)) + " functions are unowned", 1) + debug(str(len(unowned_functions)) + " global variables", 1) + for enum in enums: + debug( + "Enum " + + assure_length(enum.name, len(max(enum_names, key=len)), True) + + " contains " + + assure_length(str(len(enum.values)), 2, False) + + " values", + 1, + ) + debug("-" * col, 1) + + tpl = __file_dir__ / "wrappers_tpl.cc" + with open(tpl, encoding="utf8") as f, open( + filename, "w", encoding="utf8" + ) as wrapper_file: + do_line_directive = True + for i, line in enumerate(f): + if do_line_directive: + wrapper_file.write(f'#line {i + 1} "{tpl}"\n') + do_line_directive = False + if "" in line: + for source in sources: + wrapper_file.write(f'#include "{source.name}.h"\n') + do_line_directive = True + elif "" in line: + wrapper_file.write("// Opaque Container Declaration\n") + for i, (container_identifier, container) in enumerate(WMember.opaque_containers.items()): + wrapper_file.write(f"using {container_identifier} = {container.gen_text_cpp()};\n") + wrapper_file.write(f'PYBIND11_MAKE_OPAQUE({container_identifier})\n') + wrapper_file.write("\n") + do_line_directive = True + elif "" in line: + do_line_directive = True + elif "" in line: + wrapper_file.write("\t\t// Opaque Container Binding\n") + for container in WMember.opaque_containers.values(): + container.cont.gen_boost_py(stream=wrapper_file) + wrapper_file.write("\n") + + wrapper_file.write("\t\t// Enums\n") + for enum in enums: + enum.gen_boost_py(stream=wrapper_file) + wrapper_file.write("\n") + + wrapper_file.write("\t\t// Classes\n") + for source in sources: + for wclass in source.classes: + wclass.gen_boost_py(stream=wrapper_file) + wrapper_file.write("\n") + + wrapper_file.write("\t\t// Global Functions\n") + for fun in sorted(unowned_functions, key=lambda x: x.alias): + fun.gen_boost_py(stream=wrapper_file) + wrapper_file.write("\n") + + wrapper_file.write("\t\t// Global Variables\n") + wrapper_file.write('\t\tpy::class_(m, "Yosys")\n') + for glbl in sorted(glbls, key=lambda x: (not x.is_const, x.name)): + glbl.gen_boost_py(stream=wrapper_file) + wrapper_file.write("\t\t\t;\n") + do_line_directive = True + else: + wrapper_file.write(line) + + +def print_includes(): + for source in sources: + print(source.name + ".pyh") + + +if __name__ == "__main__": + ap = argparse.ArgumentParser() + ap.add_argument("--print-includes", action="store_true") + ap.add_argument("--debug", default=0, type=int) + ap.add_argument("output", nargs="?") + ns = ap.parse_args() + if ns.print_includes: + print_includes() + exit(0) + gen_wrappers(ns.output, ns.debug) diff --git a/pyosys/hashlib.h b/pyosys/hashlib.h new file mode 100644 index 000000000..e8ace90c8 --- /dev/null +++ b/pyosys/hashlib.h @@ -0,0 +1,275 @@ +// ------------------------------------------------------- +// Written by Mohamed Gaber in 2025 +// Based on kernel/hashlib.h by Claire Xenia Wolf +// ------------------------------------------------------- +// This header is free and unencumbered software released into the public domain. +// +// Anyone is free to copy, modify, publish, use, compile, sell, or +// distribute this software, either in source code form or as a compiled +// binary, for any purpose, commercial or non-commercial, and by any +// means. +// +// In jurisdictions that recognize copyright laws, the author or authors +// of this software dedicate any and all copyright interest in the +// software to the public domain. We make this dedication for the benefit +// of the public at large and to the detriment of our heirs and +// successors. We intend this dedication to be an overt act of +// relinquishment in perpetuity of all present and future rights to this +// software under copyright law. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +// IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +// OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// +// For more information, please refer to +// ------------------------------------------------------- +// +// pybind11 bridging headers for hashlib template +// +// These are various binding functions that expose hashlib templates as opaque +// types (https://pybind11.readthedocs.io/en/latest/advanced/cast/stl.html#making-opaque-types). +// +// Opaque types cross language barries by reference, not value. This allows +// things like mutating containers that are class properties. +// +// All methods should be vaguely in the same order as the python reference +// https://docs.python.org/3/library/stdtypes.html +// +#include // optional maps cleanest to methods that accept None in Python + +#include // base +#include // std::optional +#include // easier operator binding + +#include "kernel/hashlib.h" + +namespace pybind11 { + +template +struct is_pointer { static const bool value = false; }; +template +struct is_pointer { static const bool value = true; }; + +bool is_mapping(object obj) { + object mapping = module_::import("collections.abc").attr("Mapping"); + return isinstance(obj, mapping); +} + + +// also used for std::set because the semantics are close enough +template +void bind_pool(module &m, const char *name_cstr) { + std::string {name_cstr}; + + class_(m, name_cstr) + .def(init<>()) + .def("__len__", [](const C &s){ return (size_t)s.size(); }) + .def("__contains__", [](const C &s, const T &v){ return s.count(v); }) + .def("__delitem__", [](C &s, const T &v) { + auto n = s.erase(v); + if (n == 0) throw key_error(str(cast(v))); + }) + // TODO: disjoint, subset, union, intersection, difference, symdif + .def("copy", [](const C &s) { + return new C(s); + }) + .def("update", [](C &s, iterable iterable) { + for (auto item: iterable) { + s.insert(item.cast()); + } + }) + .def("add", [](C &s, const T &v){ s.insert(v); }) + .def("remove", [](C &s, const T &v){ + auto n = s.erase(v); + if (n == 0) throw key_error(str(cast(v))); + }) + .def("discard", [](C &s, const T &v){ s.erase(v); }) + .def("clear", [](C &s){ s.clear(); }) + .def("pop", [](C &s){ + if (s.size() == 0) { + throw key_error("empty pool"); + } + auto result = *s.begin(); + s.erase(result); + return result; + }) + .def("__bool__", [](const C &s) { return s.size() != 0; }) + .def("__iter__", [](const C &s){ + return make_iterator(s.begin(), s.end()); + }, keep_alive<0,1>()) + .def("__repr__", [name_cstr](const C &s){ + return std::string("<") + name_cstr + " size=" + std::to_string(s.size()) + ">"; + }); +} + +template +void update_dict(C *target, iterable &iterable_or_mapping) { + if (is_mapping(iterable_or_mapping)) { + for (const auto &key: iterable_or_mapping) { + (*target)[cast(key)] = cast(iterable_or_mapping[key]); + } + } else { + for (const auto &pair: iterable_or_mapping) { + if (len(pair) != 2) { + throw value_error(str("iterable element %s has more than two elements").format(str(pair))); + } + (*target)[cast(pair[cast(0)])] = cast(pair[cast(1)]); + } + } +} + +template +void bind_dict(module &m, const char *name_cstr) { + std::string {name_cstr}; + + class_(m, name_cstr) + .def(init<>()) + .def("__len__", [](const C &s){ return (size_t)s.size(); }) + .def("__getitem__", [](const C &s, const K &k) { return s.at(k); }) + .def("__setitem__", [](C &s, const K &k, const V &v) { s[k] = v; }) + .def("__delitem__", [](C &s, const K &k) { + auto n = s.erase(k); + if (n == 0) throw key_error("remove: key not found"); + }) + .def("__contains__", [](const C &s, const K &k) { return s.count(k) != 0; }) + .def("__iter__", [](const C &s){ + return make_key_iterator(s.begin(), s.end()); + }, keep_alive<0,1>()) + .def("clear", [](C &s){ s.clear(); }) + .def("copy", [](const C &s) { + return new C(s); + }) + .def("get", [](const C &s, const K& k, std::optional &default_) { + if (default_.has_value()) { + return s.at(k, *default_); + } else { + return s.at(k); + } + }, arg("key"), arg("default") = std::nullopt) + .def("items", [](const C &s){ + return make_iterator(s.begin(), s.end()); + }, keep_alive<0,1>()) + .def("keys", [](const C &s){ + return make_key_iterator(s.begin(), s.end()); + }, keep_alive<0,1>()) + .def("pop", [](const C &s, const K& k, std::optional &default_) { + if (default_.has_value()) { + return s.at(k, *default_); + } else { + return s.at(k); + } + }, arg("key"), arg("default") = std::nullopt) + .def("popitem", [name_cstr](args _) { throw std::runtime_error(std::string(name_cstr) + " is not an ordered dictionary"); }) + .def("setdefault", [name_cstr](C &s, const K& k, std::optional &default_) { + auto it = s.find(k); + if (it != s.end()) { + return it->second; + } + if (default_.has_value()) { + s[k] = *default_; + return *default_; + } + // if pointer, nullptr can be our default + if constexpr (is_pointer::value) { + s[k] = nullptr; + return (V)nullptr; + } + // TODO: std::optional? do we care? + throw type_error(std::string("the value type of ") + name_cstr + " is not nullable"); + }, arg("key"), arg("default") = std::nullopt) + .def("update", [](C &s, iterable iterable_or_mapping) { + update_dict(&s, iterable_or_mapping); + }, arg("iterable_or_mapping")) + .def("values", [](const C &s){ + return make_value_iterator(s.begin(), s.end()); + }, keep_alive<0,1>()) + .def("__or__", [](const C &s, iterable iterable_or_mapping) { + auto result = new C(s); + update_dict(result, iterable_or_mapping); + return result; + }) + .def("__ior__", [](C &s, iterable iterable_or_mapping) { + update_dict(&s, iterable_or_mapping); + return s; + }) + .def("__bool__", [](const C &s) { return s.size() != 0; }) + .def("__repr__", [name_cstr](const C &s){ + return std::string("<") + name_cstr + " size=" + std::to_string(s.size()) + ">"; + }); +} + +// idict is a special bijection and doesn't map cleanly to dict +// +// it's cleanest, despite the inconsistency with __getitem__, to just think of +// the hashable as key and the integer as value +template +void bind_idict(module &m, const char *name_cstr) { + std::string {name_cstr}; + + auto cls = class_(m, name_cstr) + .def(init<>()) + .def("__len__", [](const C &s){ return (size_t)s.size(); }) + .def("__getitem__", [](const C &s, int v) { return s[v]; }) + .def("__call__", [](C &s, const K &k) { return s(k); }) + .def("__contains__", [](const C &s, const K &k) { + return s.count(k) != 0; + }) + .def("__iter__", [](const C &s){ + return make_iterator(s.begin(), s.end()); + }, keep_alive<0,1>()) + .def("clear", [](C &s) { + s.clear(); + }) + .def("copy", [](const C &s) { + return new C(s); + }) + .def("get", [](const C &s, const K& k, std::optional &default_) { + if (default_.has_value()) { + return s.at(k, *default_); + } else { + return s.at(k); + } + }, arg("key"), arg("default") = std::nullopt) + .def("keys", [](const C &s){ + return make_iterator(s.begin(), s.end()); + }) + .def("values", [](args _){ + throw type_error("idicts do not support iteration on the integers"); + }) + .def("items", [](args _){ + throw type_error("idicts do not support pairwise iteration"); + }) + .def("update", [](C &s, iterable iterable) { + for (auto &e: iterable) { + s(cast(e)); + } + }) + .def("__or__", [](const C &s, iterable iterable) { + auto result = new C(s); + for (auto &e: iterable) { + (*result)(cast(e)); + } + return result; + }) + .def("__ior__", [](C &s, iterable iterable) { + for (auto &e: iterable) { + s(cast(e)); + } + return s; + }) + .def("__bool__", [](const C &s) { return s.size() != 0; }) + .def("__repr__", [name_cstr](const C &s){ + return std::string("<") + name_cstr + " size=" + std::to_string(s.size()) + ">"; + }); + + for (const char *mutator: {"__setitem__", "__delitem__", "pop", "popitem", "setdefault"}) { + cls.def(mutator, [](args _) { + throw type_error("idicts do not support arbitrary element mutation"); + }); + } +} +}; // namespace pybind11 diff --git a/pyosys/wrappers_tpl.cc b/pyosys/wrappers_tpl.cc new file mode 100644 index 000000000..e921ae17a --- /dev/null +++ b/pyosys/wrappers_tpl.cc @@ -0,0 +1,248 @@ +/* + * yosys -- Yosys Open SYnthesis Suite + * + * Copyright (C) 2012 Claire Xenia Wolf + * + * 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. + */ + +#ifdef WITH_PYTHON + +// + +#include +#include +#include "pyosys/hashlib.h" + +USING_YOSYS_NAMESPACE + +// + +namespace YOSYS_PYTHON { + + [[noreturn]] static void log_python_exception_as_error() { + PyErr_Print(); + log_error("Python interpreter encountered an exception.\\n"); + } + + struct YosysStatics{}; + + // + + // Trampolines for Classes with Python-Overridable Virtual Methods + // https://pybind11.readthedocs.io/en/stable/advanced/classes.html#overriding-virtual-functions-in-python + class PassTrampoline : public Pass { + public: + using Pass::Pass; + + void help() override { + PYBIND11_OVERRIDE(void, Pass, help); + } + + bool formatted_help() override { + PYBIND11_OVERRIDE(bool, Pass, formatted_help); + } + + void clear_flags() override { + PYBIND11_OVERRIDE(void, Pass, clear_flags); + } + + void execute(std::vector args, RTLIL::Design *design) override { + PYBIND11_OVERRIDE_PURE( + void, + Pass, + execute, + args, + design + ); + } + + void on_register() override { + PYBIND11_OVERRIDE(void, Pass, on_register); + } + + void on_shutdown() override { + PYBIND11_OVERRIDE(void, Pass, on_shutdown); + } + + bool replace_existing_pass() const override { + PYBIND11_OVERRIDE( + bool, + Pass, + replace_existing_pass + ); + } + }; + + class MonitorTrampoline : public RTLIL::Monitor { + public: + using RTLIL::Monitor::Monitor; + + void notify_module_add(RTLIL::Module *module) override { + PYBIND11_OVERRIDE( + void, + RTLIL::Monitor, + notify_module_add, + module + ); + } + + void notify_module_del(RTLIL::Module *module) override { + PYBIND11_OVERRIDE( + void, + RTLIL::Monitor, + notify_module_del, + module + ); + } + + void notify_connect( + RTLIL::Cell *cell, + const RTLIL::IdString &port, + const RTLIL::SigSpec &old_sig, + const RTLIL::SigSpec &sig + ) override { + PYBIND11_OVERRIDE( + void, + RTLIL::Monitor, + notify_connect, + cell, + port, + old_sig, + sig + ); + } + + void notify_connect( + RTLIL::Module *module, + const RTLIL::SigSig &sigsig + ) override { + PYBIND11_OVERRIDE( + void, + RTLIL::Monitor, + notify_connect, + module, + sigsig + ); + } + + void notify_connect( + RTLIL::Module *module, + const std::vector &sigsig_vec + ) override { + PYBIND11_OVERRIDE( + void, + RTLIL::Monitor, + notify_connect, + module, + sigsig_vec + ); + } + + void notify_blackout( + RTLIL::Module *module + ) override { + PYBIND11_OVERRIDE( + void, + RTLIL::Monitor, + notify_blackout, + module + ); + } + }; + + /// @brief Use an auxiliary function to adapt the legacy function. + void log_to_stream(py::object object) + { + // TODO + }; + + PYBIND11_MODULE(libyosys, m) { + // this code is run on import + m.doc() = "python access to libyosys"; + + if (!yosys_already_setup()) { + log_streams.push_back(&std::cout); + log_error_stderr = true; + yosys_setup(); + + // Cleanup + m.add_object("_cleanup_handle", py::capsule([](){ + yosys_shutdown(); + })); + } + + m.def("log_to_stream", &log_to_stream, "pipes yosys logs to a Python stream"); + + // Trampoline Classes + py::class_>(m, "Pass") + .def(py::init([](std::string name, std::string short_help) { + auto created = new YOSYS_PYTHON::PassTrampoline(name, short_help); + Pass::init_register(); + return created; + }), py::arg("name"), py::arg("short_help")) + .def("help", &Pass::help) + .def("formatted_help", &Pass::formatted_help) + .def("execute", &Pass::execute) + .def("clear_flags", &Pass::clear_flags) + .def("on_register", &Pass::on_register) + .def("on_shutdown", &Pass::on_shutdown) + .def("replace_existing_pass", &Pass::replace_existing_pass) + .def("experimental", &Pass::experimental) + .def("internal", &Pass::internal) + .def("pre_execute", &Pass::pre_execute) + .def("post_execute", &Pass::post_execute) + .def("cmd_log_args", &Pass::cmd_log_args) + .def("cmd_error", &Pass::cmd_error) + .def("extra_args", &Pass::extra_args) + .def("call", py::overload_cast(&Pass::call)) + .def("call", py::overload_cast>(&Pass::call)) + ; + + py::class_(m, "Monitor") + .def(py::init([]() { + return new YOSYS_PYTHON::MonitorTrampoline(); + })) + .def("notify_module_add", &RTLIL::Monitor::notify_module_add) + .def("notify_module_del", &RTLIL::Monitor::notify_module_del) + .def( + "notify_connect", + py::overload_cast< + RTLIL::Cell *, + const RTLIL::IdString &, + const RTLIL::SigSpec &, + const RTLIL::SigSpec & + >(&RTLIL::Monitor::notify_connect) + ) + .def( + "notify_connect", + py::overload_cast< + RTLIL::Module *, + const RTLIL::SigSig & + >(&RTLIL::Monitor::notify_connect) + ) + .def( + "notify_connect", + py::overload_cast< + RTLIL::Module *, + const std::vector & + >(&RTLIL::Monitor::notify_connect) + ) + .def("notify_blackout", &RTLIL::Monitor::notify_blackout) + ; + + // + }; +}; + +#endif // WITH_PYTHON diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..caa620528 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,6 @@ +[build-system] +requires = [ + "setuptools>=42", + "pybind11>=3,<4", +] +build-backend = "setuptools.build_meta" diff --git a/setup.py b/setup.py index 4a6ef5af9..59c1c4b33 100644 --- a/setup.py +++ b/setup.py @@ -16,18 +16,19 @@ import os import re import shlex import shutil +from pathlib import Path from setuptools import setup, Extension -from setuptools.command.build_ext import build_ext -__dir__ = os.path.dirname(os.path.abspath(__file__)) +import pybind11 +from pybind11.setup_helpers import build_ext + +__yosys_root__ = Path(__file__).parent yosys_version_rx = re.compile(r"YOSYS_VER\s*:=\s*([\w\-\+\.]+)") -version = yosys_version_rx.search( - open(os.path.join(__dir__, "Makefile"), encoding="utf8").read() -)[1].replace( - "+", "." -) # Convert to patch version +with open(__yosys_root__ / "Makefile", encoding="utf8") as f: + # Extract and convert + to patch version + version = yosys_version_rx.search(f.read())[1].replace("+", ".") class libyosys_so_ext(Extension): @@ -38,7 +39,13 @@ class libyosys_so_ext(Extension): "libyosys.so", [], ) + + # when iterating locally, you probably want to set this variable + # to avoid mass rebuilds bec of pybind11's include path changing + pybind_include = os.getenv("_FORCE_PYBIND_INCLUDE", pybind11.get_include()) + self.args = [ + f"PYBIND11_INCLUDE={pybind_include}", "ENABLE_PYOSYS=1", # Would need to be installed separately by the user "ENABLE_TCL=0", @@ -73,22 +80,23 @@ class libyosys_so_ext(Extension): *self.args, ] ) - build_path = os.path.dirname(os.path.dirname(bext.get_ext_fullpath(self.name))) - pyosys_path = os.path.join(build_path, "pyosys") + ext_fullpath = Path(bext.get_ext_fullpath(self.name)) + build_path = ext_fullpath.parents[1] + pyosys_path = build_path / "pyosys" os.makedirs(pyosys_path, exist_ok=True) # libyosys.so - target = os.path.join(pyosys_path, os.path.basename(self.name)) + target = pyosys_path / self.name shutil.copy(self.name, target) - bext.spawn(["strip", "-S", target]) + bext.spawn(["strip", "-S", str(target)]) # yosys-abc - yosys_abc_target = os.path.join(pyosys_path, "yosys-abc") + yosys_abc_target = pyosys_path / "yosys-abc" shutil.copy("yosys-abc", yosys_abc_target) - bext.spawn(["strip", "-S", yosys_abc_target]) + bext.spawn(["strip", "-S", str(yosys_abc_target)]) # share directory - share_target = os.path.join(pyosys_path, "share") + share_target = pyosys_path / "share" try: shutil.rmtree(share_target) except FileNotFoundError: @@ -104,14 +112,16 @@ class custom_build_ext(build_ext): return ext.custom_build(self) +with open(__yosys_root__ / "README.md", encoding="utf8") as f: + long_description = f.read() + setup( name="pyosys", packages=["pyosys"], version=version, description="Python access to libyosys", - long_description=open(os.path.join(__dir__, "README.md")).read(), + long_description=long_description, long_description_content_type="text/markdown", - install_requires=["wheel", "setuptools"], license="MIT", classifiers=[ "Programming Language :: Python :: 3", @@ -119,7 +129,6 @@ setup( "Operating System :: POSIX :: Linux", "Operating System :: MacOS :: MacOS X", ], - package_dir={"pyosys": "misc"}, python_requires=">=3.8", ext_modules=[libyosys_so_ext()], cmdclass={ diff --git a/tests/pyosys/run_tests.py b/tests/pyosys/run_tests.py new file mode 100644 index 000000000..953462c4f --- /dev/null +++ b/tests/pyosys/run_tests.py @@ -0,0 +1,39 @@ +from pathlib import Path +import shutil +import subprocess +import sys + +__file_dir__ = Path(__file__).absolute().parent + +if len(sys.argv) != 2: + print(f"Usage: {sys.argv[0]} {sys.argv[1]}") + exit(64) + +binary = [] +if sys.argv[1] in ["yosys"]: + binary = [__file_dir__.parents[1] / "yosys", "-Qy"] +else: + binary = [sys.argv[1]] + +tests = __file_dir__.glob("test_*.py") +errors = False +log_dir = __file_dir__ / "logs" +try: + shutil.rmtree(log_dir) +except FileNotFoundError: + pass +for test in tests: + print(f"* {test.name} ", end="") + log_dir.mkdir(parents=True, exist_ok=True) + log = log_dir / (test.stem + ".log") + result = subprocess.run([ + *binary, + test + ], stdout=open(log, "w"), stderr=subprocess.STDOUT) + if result.returncode == 0: + print("OK!") + else: + print(f"FAILED: {log}") + errors = True +if errors: + exit(1) diff --git a/tests/pyosys/spm.cut.v.gz b/tests/pyosys/spm.cut.v.gz new file mode 100644 index 0000000000000000000000000000000000000000..44791a035e534f1fc9ebdfc4c47bb0849bff1da8 GIT binary patch literal 5294 zcmbu92{hFGzsJSc8Oo9ed0H^WWGhj&glt)|W@p5MFvvEHC9*HcRtY1?I)fq2DBC2G zr4$+K7)ugkLNb=I+?k%|-2Z*feeOB;{_i=T^E%(p=lgztKflj+e!uxWorB}r?`_^p zEWv^P@;5_6RuqPI{(iXTU_Bc z8gINxO*nZ-wA}JGB!E@M%c6%cUcP1xW+!Y%10Q!BbblwozQfv`HKv_b> z#!S6lubRa)M^9<)!J%QJ5B&n_rWQ6cO+&Kd8y-?8ramsF_YZz@if%93?sF;VA101G zOmF!;W~aZ{$m?rHc*pKua_nIfZDf_$LsW>_pM5h*BbRhctwT{DS_Xo-Sf5JuD+i}$o z?LpMc=K)30^DAPD%|RFSa{o%aNdIl$lO#jaA6M;bx1eS=2INJTt%xl&w;jC9lKzJs zMoG9a;M6~%M{ZkJS(hs3LDFHMb7it8b{B)#+3LWNU}1~H>3@8#&xHGT`+{5$>uM>Z zk}l8TC%Vbj9V*NvN4GQZUI48#Yrn+}azyFRoM)8KcrR8h$eMlJAXk*_%yZ@vp+8HY zwM%h>oKa>o!OSHB+eP?AfYz@Pca+P_26M^r?N))4*(285JTX4eqk>)hQLHAEl*L11gR^Hg=OOtuEgN zlkyFK6%X8v+Br>rQ}q0pcf?9xiPaC}p?+l22ucU%xEVgoa*PfgtN^B1kb(OYfFCH4 zf&Yz+$Ir_{on_JGAMjzeV|3_a&tr;(7|5p36^RV|n=*+w%F%H;ET9Mg*1&^- zM9R@IiEIXhum)}jJqE1D{(nNwG30#;9fDYp8$yQx%Q55~3g(!Hqw#V8OGwL`i=oEhUV`=s>Tm0_Okj$)^O~qJaAQ(9q3=*;Z#I)(TY;1 z{cJfLvpG&(jcGZU96^yN)@8AKqldfm2Yr*`nW+2LL|J0Qi6Nhsr^DDv zMuZtwQqs&Ok-y?pF&3ickN6e;Cj&7*`4j-cq*k&TsYXc(nW+8NL|$Tq*pScY@Eno{ zSUr=*K~=_p;&i#1Pv6npC-;)>@7AWcn$seORG&pbI!A}wkSn~^(`nx}mFqzfx@ox6 zaLqPyFY*3_PrQojK6TBmHM03twF}X<{(Jh3EZ2v}ug$M7ZFXxMSjSSWP$L)Lw{?Nq zhV7j5GT&C}m#r^-JUh;}Duy0p_Zxgsy7-)S!H?FjhTZSZpv+c!%~xy;jxO@uSiI~~ zQsQbC91C;Z64~7#_U_d^k5vLm4P;f)por77)PRRE8$T}RiG9WOlS&2IDO=5yi65A*N&K1|%lwK=mL0y|x! z%mG<3eyw4BT<`5sd@^GJ8bK-bsB-9C{pQo0%H z{l?q-JvZ3H`>yB&Q)&4b9bXrjMmb+s+UIjk*c0}mndpdu&d%UWLsRh*g4@86nRWcG z8pfLa0U7^x9sg9iIwZ414PDKJ zDyYFcMs^*Cnw~>jaHEvYp_d+E9{Z3*$$1x4ROW9*`d+_>QYxHtVBTp;y9va2r7H?WY>DtN0?lRO9$=lN9>tN$N02&Nv7FyK~WBOmDt@p zG{@MV$RkavSgtW_t^nb*N~27Ou2UA5@6_W4V>^nm69RJm=xCGNO{F?s@n9x>+XN{l ztbDNvnx(J?KY{F8#5>3oqebo2UmrtQds7HQOQGrP31PRA2=}JE&z8QJyj6MZU8Rk9 zFo(WyVl>C{T<>GqCIX?9k{Wf?;Vd#o)PcRU_#PL9@S|78rorc7ci0#Go4-%^Z|1&k zY>vALeEqHl!*|75+uk)<<#Q>em+q2zlHnlY$gHVVb1uyBnheUL*1mTl+c8tB3FgS@ zspKnm@hgAES)OYgPR=^^b-H5YkJJRMWCE0P6T4Wxdk2uMDeFTda2YRM9uGV0<=raD zn6$m3RMP;Q4?Q3T(t4X;#orF4^^Q9YN$%+yMG>;`*w|z5hBd?}5$42Tf_iM;n}e!Y+k%Bb<>ZV(I5 z+{s)J(0g&@Ch}mNkXw%e-M}H8h>^GFYe9S?xi%;edmAV`kDvDbJt!-CAPDb`tKXZ?fKn5_zQT+Z6XeP*271>EoFze3%_Stgaow{L*b0&`c(pMAx9h2U`2UD^6rd6H1A{#%ma79(}g*wFBT%rBo z3|tXMpEQbYc#x}UoDvb-Z?t66BqjiDo$nG;HybPPR+a9Azey!`f)!3gWR@gl>wGJ<=`mXW`jsv@oKbvl+J60^V z>VxRl6eU2OAa&8t1;ZePYw2Tb8JC#m_kVe|=pNNi^c|h?FP|0MIccT_(MQEpw+izz z`~$+CuE$tSjRkZv)^qeVnj2I@Gj-Z3gtw9e8v-rE)s>WsRxBoqhrV_iO$rQ~*-Hdm zD$mT%Z3I^oKi8NJHV$3t3LQ)(BL%XyiY(__)Hvi|;cBv8xK-gB-=U^7=?6W9WKV4!YVg^30J@rG>X;SLx}K*3i_p0O zqyH@`PDM1j<1UUCgX+}HgPJ!y@`-abCk?)76x;-i!=~lx{DlJo(o^3QgIa=c&5ic0 zZ4hZ-I1)W3Cqe#Th5pk+-?0hr)|*-j8K@I!iRfsy_#SCOs{;X5OIF_U&a(v|MCqS0 zPk0G>Oi6S?wJK%9ob)xKhTV5IYPO<>pTm=`(-CGY$!{5m}QHjzN(Y`xg`a+1}4%uub; z@4I(EjUf?Hs%vPNq_nftU0j6F3j? zd_ju}dQ!9*S#H8e3R_){kela>Up8pMU+Q6}{($KfJF(#oES_9C9L!w~k0beZ=uf3| zoj&FbS1^>;c4tcc!Ey+d1ZIEJ=D33aJk_vv7fEIpzs{#{F{jI753qt*2=@{T0u=FW z8^hTw4_tisx~_QSl}z)UIi=0KwaI;-iEeCVVP^sEg}!B$WWEm&jNr>jX^5i)p3nKQ z$jzNIcbtdh1-hr7!=H*&S%A?rbC_O77)+;_t}okCSUFhkb7)U1NddcswuA=_#SI^` z7!4yQlrsR`HiK^Lva8w)q-;E}Tqrz1UR&Gogg1)XS_GApsSBKMWI7VGd9to=*d!_F zN%P!6^KU8ajKYM~i=^x)&CB_Bex<82fTGiNYWA8bD1)GAK1U0u&5wYO1g42yfNMFs|Nq$>qs15S!g$$NZp$@9^ojaIg z94PA9W~gKpdRucL9*~!R-JWET1Gl!_bN9c(Jk)oxb|QE-DO)BHr3K=d=(B5X_cFWg zxbB3{%A@$S)EJ2KCdoP*x=)@ ztns2uIZEE|C_u%XvJ3R&PU~DYJf;VNhNzr*`KqHuL$xLN#?RK6En03eiuUHP1rBfC zpM`C0hi(n3Z}43<>mGYEQmX>LdzU035wy14Z+;PeadvjW(n_*_3xaTTIrNQS(}PZ2 z8=m_VK*MRRn!>viP$tR}()Oi=${}V-{^dR=itWOwY_;wm^Vl5u4$s!6y_ZA`>nmv* z4MXfUh&|IY32YB@?SX7!SwJ*epVe2ZrCjku`O*b+!`sJ$`Cy*go|yGV-F>pYcJ@l9 zLqA1rh3$-#>^!Z=(AIpLA&l&}a)eJLFN70q3^TQXy^wHmvgZ}GmBL9wOzWY%%%~hk zt76AYgJN6Twj`V0TA!SXQA}iG zYB

?mDysFXSuX;4F^O7lLtzQ3eatMa1o@(T$cupie>-ZOUy)BOK^QjcuA_hst*+>b zavH)@KO^WX2#(ORk>EXRF~N1!SmKi;qevD2M+nwvh}t2WM!+%=f=?A&2^=*+Y*JY| z(dHMf@A_WM*WjWng$|f2a|}vd9A4iIEZYUomXT*SjvCH1AB^ECu)Iu iM^_(Fv>?ZgTU5hGU03U44{mSFt_EJpjj<_UV)_p!;ECJ- literal 0 HcmV?d00001 diff --git a/tests/pyosys/test_data_read.py b/tests/pyosys/test_data_read.py new file mode 100644 index 000000000..08b6695c2 --- /dev/null +++ b/tests/pyosys/test_data_read.py @@ -0,0 +1,45 @@ +from pyosys import libyosys as ys +from pathlib import Path + +__file_dir__ = Path(__file__).absolute().parent + +d = ys.Design() + +ys.run_pass(f"read_verilog {__file_dir__ / 'spm.cut.v.gz'}", d) +ys.run_pass("hierarchy -top spm", d) + +name_by_tv_location = [] +name_by_au_location = [] + +# test both dictionary mapping and equiv operators working fine +module = None +print(d.modules_) +for idstr, module_obj in d.modules_.items(): + if idstr != ys.IdString("\\spm"): + continue + if idstr.str() != "\\spm": + continue + module = module_obj + break + +assert module == d.top_module(), "top module search failed" +for name in module.ports: + wire = module.wires_[name] + name_str = name.str() + if name_str.endswith(".d"): # single reg output, in au + name_by_au_location.append(name_str[1:-2]) + elif name_str.endswith(".q"): # single reg input, in tv + name_by_tv_location.append(name_str[1:-2]) + else: # port/boundary scan + frm = wire.start_offset + wire.width + to = wire.start_offset + for i in range(frm - 1, to - 1, -1): + bit_name = name_str[1:] + f"\\[{i}\\]" + if wire.port_input: + name_by_tv_location.append(bit_name) + elif wire.port_output: + name_by_au_location.append(bit_name) + +assert name_by_tv_location == ['x\\[0\\]', 'a\\[31\\]', 'a\\[30\\]', 'a\\[29\\]', 'a\\[28\\]', 'a\\[27\\]', 'a\\[26\\]', 'a\\[25\\]', 'a\\[24\\]', 'a\\[23\\]', 'a\\[22\\]', 'a\\[21\\]', 'a\\[20\\]', 'a\\[19\\]', 'a\\[18\\]', 'a\\[17\\]', 'a\\[16\\]', 'a\\[15\\]', 'a\\[14\\]', 'a\\[13\\]', 'a\\[12\\]', 'a\\[11\\]', 'a\\[10\\]', 'a\\[9\\]', 'a\\[8\\]', 'a\\[7\\]', 'a\\[6\\]', 'a\\[5\\]', 'a\\[4\\]', 'a\\[3\\]', 'a\\[2\\]', 'a\\[1\\]', 'a\\[0\\]', '_315_', '_314_', '_313_', '_312_', '_311_', '_310_', '_309_', '_308_', '_307_', '_306_', '_305_', '_304_', '_303_', '_302_', '_301_', '_300_', '_299_', '_298_', '_297_', '_296_', '_295_', '_294_', '_293_', '_292_', '_291_', '_290_', '_289_', '_288_', '_287_', '_286_', '_285_', '_284_', '_283_', '_282_', '_281_', '_280_', '_279_', '_278_', '_277_', '_276_', '_275_', '_274_', '_273_', '_272_', '_271_', '_270_', '_269_', '_268_', '_267_', '_266_', '_265_', '_264_', '_263_', '_262_', '_261_', '_260_', '_259_', '_258_', '_257_', '_256_', '_255_', '_254_', '_253_', '_252_'], "failed to extract test vector register locations" +assert name_by_au_location == ['y\\[0\\]', '_315_', '_314_', '_313_', '_312_', '_311_', '_310_', '_309_', '_308_', '_307_', '_306_', '_305_', '_304_', '_303_', '_302_', '_301_', '_300_', '_299_', '_298_', '_297_', '_296_', '_295_', '_294_', '_293_', '_292_', '_291_', '_290_', '_289_', '_288_', '_287_', '_286_', '_285_', '_284_', '_283_', '_282_', '_281_', '_280_', '_279_', '_278_', '_277_', '_276_', '_275_', '_274_', '_273_', '_272_', '_271_', '_270_', '_269_', '_268_', '_267_', '_266_', '_265_', '_264_', '_263_', '_262_', '_261_', '_260_', '_259_', '_258_', '_257_', '_256_', '_255_', '_254_', '_253_', '_252_'], "failed to extract golden output register locations" +print("ok!") diff --git a/tests/pyosys/test_dict.py b/tests/pyosys/test_dict.py new file mode 100644 index 000000000..81e7b5516 --- /dev/null +++ b/tests/pyosys/test_dict.py @@ -0,0 +1,13 @@ +from pyosys import libyosys as ys + +my_dict = ys.StringToStringDict() +my_dict["foo"] = "bar" +my_dict.update([("first", "second")]) +my_dict.update({"key": "value"}) +for key, value in my_dict.items(): + print(key, value) + +new_dict = my_dict | {"tomato": "tomato"} +del new_dict["foo"] +assert set(my_dict.keys()) == {"first", "key", "foo"} +assert set(new_dict.keys()) == {"first", "key", "tomato"} diff --git a/tests/pyosys/test_idict.py b/tests/pyosys/test_idict.py new file mode 100644 index 000000000..fd04f4b8c --- /dev/null +++ b/tests/pyosys/test_idict.py @@ -0,0 +1,31 @@ +from pyosys import libyosys as ys + +my_idict = ys.IdstringIdict() +print(my_idict(ys.IdString("\\hello"))) +print(my_idict(ys.IdString("\\world"))) +print(my_idict.get(ys.IdString("\\world"))) +try: + print(my_idict.get(ys.IdString("\\dummy"))) +except IndexError as e: + print(f"{repr(e)}") +print(my_idict[0]) +print(my_idict[1]) +try: + print(my_idict[2]) +except IndexError as e: + print(f"{repr(e)}") + +for i in my_idict: + print(i) + +current_len = len(my_idict) +assert current_len == 2, "copy" + +my_copy = my_idict.copy() +my_copy(ys.IdString("\\copy")) +assert len(my_idict) == current_len, "copy seemed to have mutate original idict" +assert len(my_copy) == current_len + 1, "copy not behaving as expected" + +current_copy_len = len(my_copy) +my_copy |= (ys.IdString(e) for e in ("\\the", "\\world")) # 1 new element +assert len(my_copy) == current_copy_len + 1, "or operator returned unexpected result" diff --git a/tests/pyosys/test_import.py b/tests/pyosys/test_import.py new file mode 100644 index 000000000..48e911033 --- /dev/null +++ b/tests/pyosys/test_import.py @@ -0,0 +1,3 @@ +from pyosys import libyosys as ys + +ys.log("Hello, world!") diff --git a/tests/pyosys/test_monitor.py b/tests/pyosys/test_monitor.py new file mode 100644 index 000000000..2eefdad60 --- /dev/null +++ b/tests/pyosys/test_monitor.py @@ -0,0 +1,22 @@ +from pyosys import libyosys as ys +from pathlib import Path + +__file_dir__ = Path(__file__).absolute().parent + +d = ys.Design() + +class Monitor(ys.Monitor): + def __init__(self): + super().__init__() + self.mods = [] + + def notify_module_add(self, mod): + self.mods.append(mod.name.str()) + +m = Monitor() +d.monitors.add(m) + +ys.run_pass(f"read_verilog {__file_dir__ / 'spm.cut.v.gz'}", d) +ys.run_pass("hierarchy -top spm", d) + +assert m.mods == ["\\spm"] diff --git a/tests/pyosys/test_pass.py b/tests/pyosys/test_pass.py new file mode 100644 index 000000000..d8174d4fc --- /dev/null +++ b/tests/pyosys/test_pass.py @@ -0,0 +1,34 @@ +from pyosys import libyosys as ys + +import json +from pathlib import Path + +__file_dir__ = Path(__file__).absolute().parent + +class CellStatsPass(ys.Pass): + def __init__(self): + super().__init__( + "cell_stats", + "dumps cell statistics in JSON format" + ) + + def execute(self, args, design): + ys.log_header(design, "Dumping cell stats\n") + ys.log_push() + cell_stats = {} + for module in design.all_selected_whole_modules(): + for cell in module.selected_cells(): + if cell.type.str() in cell_stats: + cell_stats[cell.type.str()] += 1 + else: + cell_stats[cell.type.str()] = 1 + ys.log(json.dumps(cell_stats)) + ys.log_pop() + +p = CellStatsPass() # registration + +design = ys.Design() +ys.run_pass(f"read_verilog {__file_dir__.parent / 'simple' / 'fiedler-cooley.v'}", design) +ys.run_pass("prep", design) +ys.run_pass("opt -full", design) +ys.run_pass("cell_stats", design) diff --git a/tests/pyosys/test_script.py b/tests/pyosys/test_script.py new file mode 100644 index 000000000..7c3ec96ef --- /dev/null +++ b/tests/pyosys/test_script.py @@ -0,0 +1,21 @@ +from pathlib import Path +from pyosys import libyosys as ys + + +__file_dir__ = Path(__file__).absolute().parent +add_sub = __file_dir__.parent / "arch" / "common" / "add_sub.v" + +base = ys.Design() +ys.run_pass(f"read_verilog {add_sub}", base) +ys.run_pass("hierarchy -top top", base) +ys.run_pass("proc", base) +ys.run_pass("equiv_opt -assert -map +/ecp5/cells_sim.v synth_ecp5", base) + +postopt = ys.Design() +ys.run_pass("design -load postopt", postopt) +ys.run_pass("cd top", postopt) +ys.run_pass("select -assert-min 25 t:LUT4", postopt) +ys.run_pass("select -assert-max 26 t:LUT4", postopt) +ys.run_pass("select -assert-count 10 t:PFUMX", postopt) +ys.run_pass("select -assert-count 6 t:L6MUX21", postopt) +ys.run_pass("select -assert-none t:LUT4 t:PFUMX t:L6MUX21 %% t:* %D", postopt) From 384f7431fdb08b7ba348567b003745a3369c204f Mon Sep 17 00:00:00 2001 From: Mohamed Gaber Date: Sun, 21 Sep 2025 23:04:27 +0300 Subject: [PATCH 2/9] pyosys: rewrite wrapper generator [skip ci] --- pyosys/.gitignore | 1 + pyosys/__init__.py | 1 - pyosys/generator.py | 2695 ++++++++++--------------------------- pyosys/hashlib.h | 14 + pyosys/wrappers_tpl.cc | 25 +- pyproject.toml | 5 +- tests/pyosys/test_logs.py | 8 + 7 files changed, 738 insertions(+), 2011 deletions(-) create mode 100644 tests/pyosys/test_logs.py diff --git a/pyosys/.gitignore b/pyosys/.gitignore index f9fbdf4f6..925a2bd84 100644 --- a/pyosys/.gitignore +++ b/pyosys/.gitignore @@ -1 +1,2 @@ wrappers.cc +wrappers.inc.cc diff --git a/pyosys/__init__.py b/pyosys/__init__.py index 4622464da..d74e3f5bd 100644 --- a/pyosys/__init__.py +++ b/pyosys/__init__.py @@ -1,4 +1,3 @@ - import os import sys diff --git a/pyosys/generator.py b/pyosys/generator.py index ebe89737e..b8a7aa8dc 100644 --- a/pyosys/generator.py +++ b/pyosys/generator.py @@ -15,2025 +15,712 @@ # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. # -# Author: Benedikt Tutzer -# Modified for pybind11 by: Mohamed Gaber +# Written by Mohamed Gaber +# +# Inspired by py_wrap_generator.py by Benedikt Tutzer -import argparse -import copy -from enum import Enum -from io import StringIO +import io +import shutil from pathlib import Path -from functools import wraps -from typing import ClassVar, Optional +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 -def autostring(fn): - @wraps(fn) - def wrapper(*args, **kwargs): - if "stream" not in kwargs: - stream = StringIO() - fn(*args, stream=stream, **kwargs) - return stream.getvalue() - else: - fn(*args, **kwargs) +@dataclass +class PyosysClass: + """ + Metadata about classes or structs intended to be wrapped using Pyosys. - return wrapper + :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 + :param hash_expr: A C++ expression that will be fed to ``run_hash`` to declare a __hash__ method for Python + :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) + string_expr: Optional[str] = None + hash_expr: Optional[str] = None + + denylist: FrozenSet[str] = frozenset({}) -# Map c++ operator Syntax to Python functions -wrappable_operators = { - "<": "__lt__", - "==": "__eq__", - "!=": "__ne__", - "+": "__add__", - "-": "__sub__", - "*": "__mul__", - "/": "__div__", - "()": "__call__", -} +@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 + +""" +Add headers and classes here! +""" +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 "" in line: + for header in self.headers: + self.f.write(f'#include "{header.name}"\n') + do_line_directive = True + elif "" 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++" + return ParserOptions( + preprocessor=make_gcc_preprocessor( + defines=["_YOSYS_", "WITH_PYTHON"], + gcc_args=[preprocessor_bin, "-fsyntax-only"], + 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 + 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) + -# Restrict certain strings from being function names in Python keyword_aliases = { - "in": "in_", - "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_", + "in": "in_", + "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_", } -# These can be used without any explicit conversion -autocast_types = { - "void": "void", - "bool": "bool", - "int": "int", - "double": "double", - "size_t": "size_t", - "std::string": "std::string", - "string": "string", - "char_p": "char_p", - "std::source_location": "std::source_location", - "source_location": "source_location", - "State": "RTLIL::State", - # trampoline types - "Pass": "RTLIL::Pass", - "Monitor": "RTLIL::Monitor", -} - -def strip_std_prefix(str_in): - # removesuffix is python 3.9+ - std_namespace = "std::" - if str_in.startswith(std_namespace): - return str_in[len(std_namespace):] - return str_in - - -# Ways to link between Python- and C++ Objects -class link_types(Enum): - global_list = 1 # Identical to pointer, kept for historical reasons - ref_copy = 2 # Copy - pointer = 3 # The Python Object contains a pointer to its C++ - # counterpart - derive = 4 # Identical to ref_copy, kept for historical reasons - - -class attr_types(Enum): - star = "*" - amp = "&" - ampamp = "&&" - default = "" - - -# For source-files -class Source: - name = "" - classes = [] - - def __init__(self, name, classes): - self.name = name - self.classes = classes - - -# Splits a list by the given delimiter, without splitting strings inside -# pointy-brackets (< and >) -def split_list(str_def, delim): - str_def = str_def.strip() - if len(str_def) == 0: - return [] - if str_def.count(delim) == 0: - return [str_def] - if str_def.count("<") == 0: - return str_def.split(delim) - if str_def.find("<") < str_def.find(" "): - closing = find_closing( - str_def[str_def.find("<") + 1 :], "<", ">" - ) + str_def.find("<") - comma = str_def[closing:].find(delim) - if comma == -1: - return [str_def] - comma = closing + comma - else: - comma = str_def.find(delim) - rest = split_list(str_def[comma + 1 :], delim) - ret = [str_def[:comma]] - if rest != None and len(rest) != 0: - ret.extend(rest) - return ret - - -# Represents a Type -class WType: - name = "" - cont = None - attr_type = attr_types.default - - def __init__(self, name="", cont=None, attr_type=attr_types.default): - self.name = name - self.cont = cont - self.attr_type = attr_type - - # Python type-string - def gen_text(self): - text = self.name - if self.name in enum_names: - text = enum_by_name(self.name).namespace + "::" + self.name - if self.cont != None: - return self.cont.gen_identifier() - return text - - # C++ type-string - def gen_text_cpp(self): - postfix = "" - if self.attr_type == attr_types.star: - postfix = " *" - elif self.attr_type == attr_types.amp: - postfix = " &" - elif self.attr_type == attr_types.ampamp: - postfix = " &&" - if self.name in classnames: - return class_by_name(self.name).namespace + "::" + self.name + postfix - if self.name in enum_names: - return enum_by_name(self.name).namespace + "::" + self.name + postfix - if self.name in autocast_types: - return autocast_types[self.name] + postfix - text = self.name - if self.cont != None: - text += "<" - for a in self.cont.args: - text += a.gen_text_cpp() + ", " - text = text[:-2] - text += ">" - return text - - @staticmethod - def from_string(str_def, containing_file, line_number): - str_def = str_def.strip() - if len(str_def) == 0: - return None - str_def = str_def.replace( - "RTLIL::SigSig", "std::pair" - ).replace("SigSig", "std::pair") - t = WType() - t.name = "" - t.cont = None - t.attr_type = attr_types.default - if str_def.find("<") != -1: # and str_def.find("<") < str_def.find(" "): - str_def = str_def.replace("const ", "") - - candidate = WContainer.from_string(str_def, containing_file, line_number) - if candidate == None: - return None - t.name = str_def[: str_def.find("<")] - - if t.name.count("*") + t.name.count("&") > 1: - return None - - if t.name.count("*") == 1 or str_def[0] == "*" or str_def[-1] == "*": - t.attr_type = attr_types.star - t.name = t.name.replace("*", "") - elif t.name.count("&&") == 1: - t.attr_type = attr_types.ampamp - t.name = t.name.replace("&&", "") - elif t.name.count("&") == 1 or str_def[0] == "&" or str_def[-1] == "&": - t.attr_type = attr_types.amp - t.name = t.name.replace("&", "") - - t.cont = candidate - if t.name not in known_containers: - return None - return t - - prefix = "" - - if str.startswith(str_def, "const "): - if "char_p" in str_def: - prefix = "const " - str_def = str_def[6:] - if str.startswith(str_def, "unsigned "): - prefix = "unsigned " + prefix - str_def = str_def[9:] - while str.startswith(str_def, "long "): - prefix = "long " + prefix - str_def = str_def[5:] - while str.startswith(str_def, "short "): - prefix = "short " + prefix - str_def = str_def[6:] - - str_def = str_def.split("::")[-1] - - if str_def.count("*") + str_def.count("&") >= 2: - return None - - if str_def.count("*") == 1: - t.attr_type = attr_types.star - str_def = str_def.replace("*", "") - elif str_def.count("&&") == 1: - t.attr_type = attr_types.ampamp - str_def = str_def.replace("&&", "") - elif str_def.count("&") == 1: - t.attr_type = attr_types.amp - str_def = str_def.replace("&", "") - - if ( - len(str_def) > 0 - and str_def.split("::")[-1] not in autocast_types - and str_def.split("::")[-1] not in classnames - and str_def.split("::")[-1] not in enum_names - ): - return None - - if str_def.count(" ") == 0: - t.name = (prefix + str_def).replace("char_p", "char *") - t.cont = None - return t - return None - - def gen_identifier(self): - if self.cont: - return self.cont.gen_identifier() - return self.name.title() - - def as_wclass(self) -> Optional["WClass"]: - return class_by_name(self.name) - - def __repr__(self): - return f"{self.__class__.__qualname__}(**{repr(self.__dict__)})" - - -# Associate the "Translators" with their c++ type -known_containers = { - "std::set", - "std::vector", - "std::map", - "std::pair", - "pool", - "idict", - "dict", - "RTLIL::ObjRange" -} - -# Represents a container-type -class WContainer: - name = "" - args = [] - - def from_string(str_def, containing_file, line_number): - if str_def == None or len(str_def) < 4: - return None - cont = WContainer() - cont.name = str_def[: str_def.find("<")] - str_def = str_def[str_def.find("<") + 1 : find_closing(str_def, "<", ">")] - cont.args = [] - for arg in split_list(str_def, ","): - candidate = WType.from_string(arg.strip(), containing_file, line_number) - if candidate == None: - return None - if candidate.name == "void": - return None - cont.args.append(candidate) - return cont - - # generate the c++ type string - def gen_type_cpp(self): - tpl_args = [] - for arg in self.args: - postfix = (arg.attr_type == attr_types.star) * " *" - if arg.name in autocast_types: - tpl_args.append(autocast_types[arg.name] + postfix) - elif arg.name in known_containers: - tpl_args.append(arg.cont.gen_type_cpp()) - else: - tpl_args.append(arg.as_wclass().fully_qualified_name() + postfix) - return f'{self.name}<{ ", ".join(tpl_args) }>' - - def gen_identifier(self): - container = strip_std_prefix(self.name).title() - - if container == "Dict": - assert len(self.args) == 2 - return f"{self.args[0].gen_identifier()}To{self.args[1].gen_identifier()}{container}" - - args = [] - for arg in self.args: - arg_name = arg.name.title() - if arg.cont: - arg_name = arg.cont.gen_identifier() - args.append(arg_name) - args.append(container) - result = "".join(args) - if result == "SigspecSigspecPair": - return "SigSig" - return result - - @autostring - def gen_boost_py(self, *, stream): - bind_fn = "py::bind_" + strip_std_prefix(self.name) - tpl_args = [self.gen_type_cpp()] - if bind_fn != "py::bind_vector": - # using custom bind function, can't use ::value so need more - # template arguments - for arg in self.args: - postfix = (arg.attr_type == attr_types.star) * " *" - if arg.name in autocast_types: - tpl_args.append(autocast_types[arg.name] + postfix) - elif arg.name in known_containers: - tpl_args.append(arg.cont.gen_type_cpp()) - else: - tpl_args.append(arg.as_wclass().fully_qualified_name() + postfix) - if bind_fn == "py::bind_set": - bind_fn = "py::bind_pool" - stream.write(f'\t\t{bind_fn}<{",".join(tpl_args)}>(m, "{self.gen_identifier()}");\n') - - def __repr__(self): - return f"{self.__class__.__qualname__}(**{repr(self.__dict__)})" - - -class Attribute: - wtype = None - varname = None - is_const = False - default_value = None - pos = None - pos_counter = 0 - - def __init__(self, wtype, varname, is_const=False, default_value=None): - self.wtype = wtype - self.varname = varname - self.is_const = is_const - self.default_value = None - self.container = None - - @staticmethod - def from_string(str_def, containing_file, line_number): - if len(str_def) < 3: - return None - orig = str_def - arg = Attribute(None, None) - prefix = "" - arg.wtype = None - arg.varname = None - arg.is_const = False - arg.default_value = None - arg.container = None - if str.startswith(str_def, "const "): - arg.is_const = True - str_def = str_def[6:] - if str.startswith(str_def, "unsigned "): - prefix = "unsigned " - str_def = str_def[9:] - while str.startswith(str_def, "long "): - prefix = "long " + prefix - str_def = str_def[5:] - while str.startswith(str_def, "short "): - prefix = "short " + prefix - str_def = str_def[6:] - - if str_def.find("<") != -1 and str_def.find("<") < str_def.find(" "): - closing = ( - find_closing(str_def[str_def.find("<") :], "<", ">") - + str_def.find("<") - + 1 - ) - arg.wtype = WType.from_string( - str_def[:closing].strip(), containing_file, line_number - ) - str_def = str_def[closing + 1 :] - else: - if str_def.count(" ") > 0: - arg.wtype = WType.from_string( - prefix + str_def[: str_def.find(" ")].strip(), - containing_file, - line_number, - ) - str_def = str_def[str_def.find(" ") + 1 :] - else: - arg.wtype = WType.from_string( - prefix + str_def.strip(), containing_file, line_number - ) - str_def = "" - arg.varname = "" - - if arg.wtype == None: - return None - if str_def.count("=") == 0: - arg.varname = str_def.strip() - if arg.varname.find(" ") > 0: - return None - else: - arg.varname = str_def[: str_def.find("=")].strip() - if arg.varname.find(" ") > 0: - return None - str_def = str_def[str_def.find("=") + 1 :].strip() - arg.default_value = str_def[arg.varname.find("=") + 1 :].strip() - if len(arg.varname) == 0: - arg.varname = None - return arg - if arg.varname[0] == "*": - arg.wtype.attr_type = attr_types.star - arg.varname = arg.varname[1:] - elif arg.varname[0] == "&": - if arg.wtype.attr_type != attr_types.default: - return None - if arg.varname[1] == "&": - arg.wtype.attr_type = attr_types.ampamp - arg.varname = arg.varname[2:] - else: - arg.wtype.attr_type = attr_types.amp - arg.varname = arg.varname[1:] - return arg - - # Generates the varname. If the attribute has no name in the header file, - # a name is generated - def gen_varname(self): - if self.varname != None: - return self.varname - if self.wtype.name == "void": - return "" - if self.pos == None: - self.pos = Attribute.pos_counter - Attribute.pos_counter = Attribute.pos_counter + 1 - return "gen_varname_" + str(self.pos) - - # Generates the test for the function headers with c++ types - def gen_listitem_cpp(self, include_varname=True): - postfix = self.gen_varname() * include_varname - prefix = "" - if self.is_const: - prefix = "const " - infix = "" - if self.wtype.attr_type == attr_types.star: - infix = "*" - elif self.wtype.attr_type == attr_types.amp: - infix = "&" - elif self.wtype.attr_type == attr_types.ampamp: - infix = "&&" - if self.wtype.name in known_containers: - return ( - prefix - + self.wtype.cont.gen_type_cpp() - + " " - + infix - + postfix - ) - if self.wtype.name in classnames: - return ( - prefix - + class_by_name(self.wtype.name).namespace - + "::" - + self.wtype.name - + " " - + infix - + postfix - ) - return prefix + self.wtype.name + " " + infix + postfix - - def gen_listitem_pyarg(self): - default = "" - if self.default_value is not None: - default = f" = {self.default_value}" - return f'py::arg("{self.varname}"){default}' - - # Generates the listitem withtout the varname, so the signature can be - # compared - def gen_listitem_hash(self): - prefix = "" - if self.is_const: - prefix = "const " - if self.wtype.name in classnames: - return prefix + self.wtype.name + "* " - if self.wtype.name in known_containers: - return self.wtype.cont.gen_identifier() - return prefix + self.wtype.name - - -class WClass: - name = None - namespace = None - link_type = None - base_class = None - id_ = None - string_id = None - hash_id = None - needs_clone = False - found_funs = [] - found_vars = [] - found_constrs = [] - - def __init__( - self, - name, - link_type, - *, - id_=None, - string_id=None, - hash_id=None, - needs_clone=False, - ): - self.name = name - self.namespace = None - self.base_class = None - self.link_type = link_type - self.id_ = id_ - self.string_id = string_id - self.hash_id = hash_id - self.needs_clone = needs_clone - self.found_funs = [] - self.found_vars = [] - self.found_constrs = [] - - @autostring - def gen_boost_py(self, *, stream): - name_qualified = f"{self.namespace}::{self.name}" - tpl_args = [name_qualified] - if self.link_type in [link_types.pointer, link_types.global_list]: - tpl_args.append(f"std::unique_ptr<{name_qualified}, py::nodelete>") - stream.write(f'\t\tpy::class_<{", ".join(tpl_args)}>(m, "{self.name}")\n') - for con in self.found_constrs: - # HACK: skip move constructors - if "&&" in con.orig_text: - continue - con.gen_boost_py(stream=stream) - for fun in sorted(self.found_funs, key=lambda f: (f.is_operator, f.name)): - fun.gen_boost_py(stream=stream) - if self.string_id: - stream.write( - f'\t\t\t.def("__str__", [](const {name_qualified} &self){{ return self.{self.string_id}; }})\n' - ) - if self.hash_id: - hash_member = f".{self.hash_id}" if self.hash_id != "" else "" - stream.write( - f'\t\t\t.def("__hash__", [](const {name_qualified} &self){{ return run_hash(self{hash_member}); }})\n' - ) - for var in self.found_vars: - var.gen_boost_py(stream=stream) - stream.write("\t\t;\n") - - def fully_qualified_name(self) -> str: - return f"{self.namespace}::{self.name}" - - def __repr__(self): - return f"{self.__class__.__qualname__}({repr(self.__dict__)})" - - -# CONFIGURE HEADER-FILES TO BE PARSED AND CLASSES EXPECTED IN THEM HERE - -sources = [ - Source( - "kernel/celltypes", - [ - WClass("CellType", link_types.ref_copy, hash_id="type", needs_clone=True), - WClass("CellTypes", link_types.ref_copy, needs_clone=True), - ], - ), - Source( - "kernel/consteval", [WClass("ConstEval", link_types.ref_copy, needs_clone=True)] - ), - Source("kernel/log", []), - Source( - "kernel/register", - [ - # WClass("Pass", link_types.derive, needs_clone=True), # Manual mapping because of virtual method - ], - ), - Source( - "kernel/rtlil", - [ - WClass("IdString", link_types.ref_copy, string_id="str()", hash_id="str()"), - WClass("Const", link_types.ref_copy, string_id="as_string()"), - WClass("AttrObject", link_types.ref_copy), - WClass("NamedObject", link_types.ref_copy), - WClass("Selection", link_types.ref_copy), - #WClass("Monitor", link_types.derive), # Moved to tpl for virtual methods - WClass("CaseRule", link_types.ref_copy, needs_clone=True), - WClass("SwitchRule", link_types.ref_copy, needs_clone=True), - WClass("SyncRule", link_types.ref_copy, needs_clone=True), - WClass( - "Process", link_types.pointer, string_id="name.c_str()", hash_id="name" - ), - WClass("SigChunk", link_types.ref_copy), - WClass("SigBit", link_types.ref_copy, hash_id=""), - WClass("SigSpec", link_types.ref_copy, hash_id=""), - WClass( - "Cell", - link_types.global_list, - id_=Attribute(WType("unsigned int"), "hashidx_"), - string_id="name.c_str()", - hash_id="", - ), - WClass( - "Wire", - link_types.global_list, - id_=Attribute(WType("unsigned int"), "hashidx_"), - string_id="name.c_str()", - hash_id="", - ), - WClass( - "Memory", - link_types.global_list, - id_=Attribute(WType("unsigned int"), "hashidx_"), - string_id="name.c_str()", - hash_id="", - ), - WClass( - "Module", - link_types.global_list, - id_=Attribute(WType("unsigned int"), "hashidx_"), - string_id="name.c_str()", - hash_id="", - ), - WClass( - "Design", - link_types.ref_copy, - id_=Attribute(WType("unsigned int"), "hashidx_"), - string_id="hashidx_", - hash_id="", - ), - ], - ), - # Source("kernel/satgen",[ - # ] - # ), - # Source("libs/ezsat/ezsat",[ - # ] - # ), - # Source("libs/ezsat/ezminisat",[ - # ] - # ), - Source( - "kernel/sigtools", [WClass("SigMap", link_types.ref_copy, needs_clone=True)] - ), - Source("kernel/yosys", []), - Source("kernel/cost", []), -] - -blacklist_methods = [ - "YOSYS_NAMESPACE::Pass::run_register", - "YOSYS_NAMESPACE::Module::Pow", - "YOSYS_NAMESPACE::RTLIL::Design::selected_whole_modules", - "YOSYS_NAMESPACE::RTLIL::AttrObject::get_blackbox_attribute" -] - -enum_names = ["State", "SyncType", "ConstFlags"] - -enums = [] # Do not edit -glbls = [] - -unowned_functions = [] - -classnames = [] -for source in sources: - for wclass in source.classes: - classnames.append(wclass.name) - - -def class_by_name(name): - for source in sources: - for wclass in source.classes: - if wclass.name == name: - return wclass - return None - - -def enum_by_name(name): - for e in enums: - if e.name == name: - return e - return None - - -def find_closing(text, open_tok, close_tok): - if text.find(open_tok) == -1 or text.find(close_tok) <= text.find(open_tok): - return text.find(close_tok) - return ( - text.find(close_tok) - + find_closing(text[text.find(close_tok) + 1 :], open_tok, close_tok) - + 1 - ) - - -def unpretty_string(s): - s = s.strip() - while s.find(" ") != -1: - s = s.replace(" ", " ") - while s.find("\t") != -1: - s = s.replace("\t", " ") - s = s.replace(" (", "(") - return s - - -class WEnum: - name = None - namespace = None - values = [] - - def from_string(str_def, namespace, line_number): - str_def = str_def.strip() - if not str.startswith(str_def, "enum "): - return None - if str_def.count(";") != 1: - return None - str_def = str_def[5:] - enum = WEnum() - split = str_def.split(":") - if len(split) != 2: - return None - enum.name = split[0].strip() - if enum.name not in enum_names: - return None - str_def = split[1] - if str_def.count("{") != str_def.count("}") != 1: - return None - if ( - len(str_def) < str_def.find("}") + 2 - or str_def[str_def.find("}") + 1] != ";" - ): - return None - str_def = str_def.split("{")[-1].split("}")[0] - enum.values = [] - for val in str_def.split(","): - enum.values.append(val.strip().split("=")[0].strip()) - enum.namespace = namespace - return enum - - @autostring - def gen_boost_py(self, *, stream): - stream.write( - f'\t\tpy::native_enum<{self.namespace}::{self.name}>(m, "{self.name}", "enum.Enum")\n' - ) - for value in self.values: - stream.write(f'\t\t\t.value("{value}", {self.namespace}::{value})\n') - stream.write("\t\t\t.finalize();\n") - - def __str__(self): - ret = "Enum " + self.namespace + "::" + self.name + "(\n" - for val in self.values: - ret = ret + "\t" + val + "\n" - return ret + ")" - - -class WConstructor: - orig_text = None - args = [] - containing_file = None - member_of = None - duplicate = False - protected = False - - def __init__(self, containing_file, class_): - self.orig_text = "Auto generated default constructor" - self.args = [] - self.containing_file = containing_file - self.member_of = class_ - self.protected = False - - def from_string(str_def, containing_file, class_, line_number, protected=False): - if class_ == None: - return None - if str_def.count("delete;") > 0: - return None - con = WConstructor(containing_file, class_) - con.orig_text = str_def - con.args = [] - con.duplicate = False - con.protected = protected - if str.startswith(str_def, "inline "): - str_def = str_def[7:] - if not str.startswith(str_def, class_.name + "("): - return None - str_def = str_def[len(class_.name) + 1 :] - found = find_closing(str_def, "(", ")") - if found == -1: - return None - str_def = str_def[0:found].strip() - if len(str_def) == 0: - return con - args = split_list(str_def, ",") - for i, arg in enumerate(args): - parsed = Attribute.from_string(arg.strip(), containing_file, line_number) - if parsed == None: - return None - # Only allow std::source_location as defaulted last argument, and - # don't append so it takes default value - if parsed.wtype.name in ["std::source_location", "source_location"]: - if parsed.default_value is None or i != len(args) - 1: - debug( - "std::source_location not defaulted last arg of " - + class_.name - + " is unsupported", - 2, - ) - return None - continue - con.args.append(parsed) - return con - - def gen_decl_hash_py(self): - text = self.member_of.name + "(" - for arg in self.args: - text += arg.gen_listitem_hash() + ", " - if len(self.args) > 0: - text = text[:-2] - text += ");" - return text - - def overload_params(self): - return ", ".join([a.gen_listitem_cpp(include_varname=False) for a in self.args]) - - @autostring - def gen_boost_py(self, *, stream): - if self.duplicate or self.protected: - return - stream.write(f"\t\t\t.def(py::init<{self.overload_params()}>())\n") - - -class WFunction: - orig_text = None - is_static = False - is_inline = False - is_virtual = False - is_const = False - ret_attr_type = attr_types.default - is_operator = False - ret_type = None - name = None - alias = None - args = [] - containing_file = None - member_of = None - duplicate = False - namespace = "" - - def from_string(str_def, containing_file, class_, line_number, namespace): - if str_def.count("delete;") > 0: - return None - func = WFunction() - func.is_static = False - func.is_inline = False - func.is_virtual = False - func.is_const = False - func.ret_attr_type = attr_types.default - func.is_operator = False - func.member_of = None - func.orig_text = str_def - func.args = [] - func.containing_file = containing_file - func.member_of = class_ - func.duplicate = False - func.namespace = namespace - str_def = str_def.replace("operator ", "operator") - - # remove attributes from the start - if str.startswith(str_def, "[[") and "]]" in str_def: - str_def = str_def[str_def.find("]]") + 2 :] - - if str.startswith(str_def, "static "): - func.is_static = True - str_def = str_def[7:] - else: - func.is_static = False - if str.startswith(str_def, "inline "): - func.is_inline = True - str_def = str_def[7:] - else: - func.is_inline = False - if str.startswith(str_def, "virtual "): - func.is_virtual = True - str_def = str_def[8:] - else: - func.is_virtual = False - - if str_def.count(" ") == 0: - return None - - parts = split_list(str_def.strip(), " ") - - prefix = "" - i = 0 - for part in parts: - if part in ["unsigned", "long", "short", "const"]: - prefix += part + " " - i += 1 - else: - break - parts = parts[i:] - - if len(parts) <= 1: - return None - - func.ret_type = WType.from_string( - prefix + parts[0], containing_file, line_number - ) - - if func.ret_type == None: - return None - - str_def = parts[1] - for part in parts[2:]: - str_def = str_def + " " + part - - found = str_def.find("(") - if found == -1 or (str_def.find(" ") != -1 and found > str_def.find(" ")): - return None - func.name = str_def[:found] - str_def = str_def[found:] - if func.name.find("operator") != -1 and str.startswith(str_def, "()("): - func.name += "()" - str_def = str_def[2:] - str_def = str_def[1:] - if func.name.find("operator") != -1: - func.is_operator = True - if func.name.find("*") == 0: - func.name = func.name.replace("*", "") - func.ret_type.attr_type = attr_types.star - if func.name.find("&&") == 0: - func.name = func.name.replace("&&", "") - func.ret_type.attr_type = attr_types.ampamp - if func.name.find("&") == 0: - func.name = func.name.replace("&", "") - func.ret_type.attr_type = attr_types.amp - - found = find_closing(str_def, "(", ")") - if found == -1: - return None - - post_qualifiers = str_def[found + 1 :].lstrip() - if post_qualifiers.startswith("const"): - func.is_const = True - - str_def = str_def[0:found] - if func.name in blacklist_methods: - return None - if func.namespace != None and func.namespace != "": - if (func.namespace + "::" + func.name) in blacklist_methods: - return None - if func.member_of != None: - if ( - func.namespace + "::" + func.member_of.name + "::" + func.name - ) in blacklist_methods: - return None - if ( - func.is_operator - and func.name.replace(" ", "").replace("operator", "").split("::")[-1] - not in wrappable_operators - ): - return None - - testname = func.name - if func.is_operator: - testname = testname[: testname.find("operator")] - if ( - testname.count(")") != 0 - or testname.count("(") != 0 - or testname.count("~") != 0 - or testname.count(";") != 0 - or testname.count(">") != 0 - or testname.count("<") != 0 - or testname.count("throw") != 0 - ): - return None - - func.alias = func.name - if func.name in keyword_aliases: - func.alias = keyword_aliases[func.name] - str_def = str_def[:found].strip() - if len(str_def) == 0: - return func - args = split_list(str_def, ",") - for i, arg in enumerate(args): - if arg.strip() == "...": - continue - parsed = Attribute.from_string(arg.strip(), containing_file, line_number) - if parsed == None: - return None - # Only allow std::source_location as defaulted last argument, and - # don't append so it takes default value - if parsed.wtype.name in ["std::source_location", "source_location"]: - if parsed.default_value is None or i != len(args) - 1: - debug( - "std::source_location not defaulted last arg of " - + func.name - + " is unsupported", - 2, - ) - return None - continue - func.args.append(parsed) - # handle (void) parameter declarations - if len(func.args) == 1 and func.args[0].wtype.name == "void": - func.args = [] - return func - - @property - def mangled_name(self): - mangled_typename = ( - lambda code: code.replace("::", "_") - .replace("<", "_") - .replace(">", "_") - .replace(" ", "") - .replace("*", "") - .replace(",", "") - ) - - return self.name + "".join( - f"__{mangled_typename(arg.wtype.gen_text_cpp())}" for arg in self.args - ) - - def gen_alias(self): - self.alias = self.mangled_name - - def gen_post_qualifiers(self, derived=False): - if ( - self.member_of != None - and self.member_of.link_type == link_types.derive - and self.is_virtual - and derived - ): - # we drop the qualifiers when deriving callbacks to be implemented in Python - return "" - return " const" if self.is_const else "" - - def gen_decl_hash_py(self): - text = self.ret_type.gen_text() + " " + self.alias + "(" - for arg in self.args: - text += arg.gen_listitem_hash() + ", " - if len(self.args) > 0: - text = text[:-2] - text += ");" - return text - - def overload_params(self): - return ", ".join([a.gen_listitem_cpp(False) for a in self.args]) - - def py_args(self): - return ", ".join([a.gen_listitem_pyarg() for a in self.args]) - - def wrapper_params(self): - return ", ".join([a.gen_listitem_cpp() for a in self.args]) - - def wrapper_args(self): - varnames = [] - for a in self.args: - if a.varname == "format": - # HACK: handle format strings (by ignoring the format part) - varnames.extend(['"%s"', "format"]) - else: - varnames.append(a.varname) - return ", ".join(varnames) - - @autostring - def gen_boost_py(self, *, stream): - if self.duplicate: - return - - fully_qualify = False - if self.member_of is not None and ( - (self.member_of.name == "IdString" and self.name == "in") - or (self.member_of.name == "Design" and self.name == "selection") - ): - fully_qualify = True - - # HACK: skip methods with inline-related nonsense - if self.alias in [ - "log_id", - "log_dump_val_worker", - "log_dump_args_worker", - "GetSize", - ]: - return - - prefix = "\t\tm" - ns = self.namespace - if self.member_of: - prefix = "\t\t\t" - ns = self.member_of.fully_qualified_name() - - stream.write(f"{prefix}.def") - if self.member_of and self.is_static: - stream.write("_static") - stream.write("(") - - if self.is_operator: - stream.write(f"py::self {self.name[len('operator'):]} py::self") - else: - stream.write(f'"{self.alias}", ') - # HACK: wrap variadics by only allowing a string - if "..." in self.orig_text: - stream.write( - f"[]({self.wrapper_params()}) {{ {self.namespace}::{self.name}({self.wrapper_args()}); }}" - ) - else: - - # HACK: Some of these needs special handling, i.e., a FULL - # overload disambiguation. Not sure why. Something to do with - # inlines and overloads. - # - # In theory, this disambiguation should work for everything but - # WType doesn't properly retain const return types yet. - if fully_qualify: - stream.write( - f"static_cast < {self.ret_type.gen_text_cpp()} ({ns}::*)({self.overload_params()}) {self.gen_post_qualifiers()} >(" - ) - elif not len(self.args) == 0: - stream.write(f"py::overload_cast<{self.overload_params()}>(") - stream.write(f"&{ns}::{self.name}") - if fully_qualify: - stream.write(")") - elif not len(self.args) == 0: - if self.is_const: - stream.write(f", py::const_") - stream.write(")") - py_args = self.py_args() - if len(py_args): - stream.write(", ") - stream.write(py_args) - stream.write(")\n" if self.member_of is not None else ");\n") - - def __repr__(self): - return f"{self.__class__.__qualname__}({repr(self.__dict__)})" - - -class WMember: - opaque_containers: ClassVar[dict] = dict() - orig_text = None - wtype = attr_types.default - name = None - containing_file = None - member_of = None - namespace = "" - is_const = False - - def from_string(str_def, containing_file, class_, line_number, namespace): - member = WMember() - member.orig_text = str_def - member.wtype = None - member.name = "" - member.containing_file = containing_file - member.member_of = class_ - member.namespace = namespace - member.is_const = False - - if str.startswith(str_def, "const "): - member.is_const = True - str_def = str_def[6:] - - if str_def.count(" ") == 0: - return None - - parts = split_list(str_def.strip(), " ") - - prefix = "" - i = 0 - for part in parts: - if part in ["unsigned", "long", "short"]: - prefix += part + " " - i += 1 - else: - break - parts = parts[i:] - - if len(parts) <= 1: - return None - - member.wtype = WType.from_string( - prefix + parts[0], containing_file, line_number - ) - - if member.wtype == None: - return None - - str_def = parts[1] - for part in parts[2:]: - str_def = str_def + " " + part - - if ( - str_def.find("(") != -1 - or str_def.find(")") != -1 - or str_def.find("{") != -1 - or str_def.find("}") != -1 - ): - return None - - found = str_def.find(";") - if found == -1: - return None - - found_eq = str_def.find("=") - if found_eq != -1: - found = found_eq - - member.name = str_def[:found] - str_def = str_def[found + 1 :] - if member.name.find("*") == 0: - member.name = member.name.replace("*", "") - member.wtype.attr_type = attr_types.star - if member.name.find("&&") == 0: - member.name = member.name.replace("&&", "") - member.wtype.attr_type = attr_types.ampamp - if member.name.find("&") == 0: - member.name = member.name.replace("&", "") - member.wtype.attr_type = attr_types.amp - - if len(str_def.strip()) != 0: - return None - - if len(member.name.split(",")) > 1: - member_list = [] - for name in member.name.split(","): - name = name.strip() - member_list.append(WMember()) - member_list[-1].orig_text = member.orig_text - member_list[-1].wtype = member.wtype - member_list[-1].name = name - member_list[-1].containing_file = member.containing_file - member_list[-1].member_of = member.member_of - member_list[-1].namespace = member.namespace - member_list[-1].is_const = member.is_const - return member_list - - if member.wtype.cont: - WMember.opaque_containers[member.wtype.gen_identifier()] = member.wtype - - return member - - @autostring - def gen_boost_py(self, *, stream): - if False and self.wtype.attr_type == attr_types.default: - property_type = self.wtype.gen_text_cpp() - stream.write(f'\t\t\t.def_property("{self.name}",\n') - stream.write(f'\t\t\t\t[]({self.member_of.fully_qualified_name()} &o) -> {property_type}& {{ return o.{self.name}; }},\n') - stream.write(f'\t\t\t\t[]({self.member_of.fully_qualified_name()} &o, {property_type} const &p) {{ o.{self.name} = p; }},\n') - stream.write(f'\t\t\t\tpy::return_value_policy::reference_internal\n') - stream.write(f'\t\t\t)\n') - else: - meth = "def_readonly" if self.is_const else "def_readwrite" - stream.write( - f'\t\t\t.{meth}("{self.name}", &{self.member_of.fully_qualified_name()}::{self.name})\n' - ) - - def __repr__(self): - return f"{self.__class__.__qualname__}({repr(self.__dict__)})" - - -class WGlobal: - orig_text = None - wtype = attr_types.default - name = None - containing_file = None - namespace = "" - is_const = False - - def from_string(str_def, containing_file, line_number, namespace): - glbl = WGlobal() - glbl.orig_text = str_def - glbl.wtype = None - glbl.name = "" - glbl.containing_file = containing_file - glbl.namespace = namespace - glbl.is_const = False - - if not str.startswith(str_def, "extern"): - return None - str_def = str_def[7:] - - if str.startswith(str_def, "const "): - glbl.is_const = True - str_def = str_def[6:] - - if str_def.count(" ") == 0: - return None - - parts = split_list(str_def.strip(), " ") - - prefix = "" - i = 0 - for part in parts: - if part in ["unsigned", "long", "short"]: - prefix += part + " " - i += 1 - else: - break - parts = parts[i:] - - if len(parts) <= 1: - return None - - glbl.wtype = WType.from_string(prefix + parts[0], containing_file, line_number) - - if glbl.wtype == None: - return None - - str_def = parts[1] - for part in parts[2:]: - str_def = str_def + " " + part - - if ( - str_def.find("(") != -1 - or str_def.find(")") != -1 - or str_def.find("{") != -1 - or str_def.find("}") != -1 - ): - return None - - found = str_def.find(";") - if found == -1: - return None - - found_eq = str_def.find("=") - if found_eq != -1: - found = found_eq - - glbl.name = str_def[:found] - str_def = str_def[found + 1 :] - if glbl.name.find("*") == 0: - glbl.name = glbl.name.replace("*", "") - glbl.wtype.attr_type = attr_types.star - if glbl.name.find("&&") == 0: - glbl.name = glbl.name.replace("&&", "") - glbl.wtype.attr_type = attr_types.ampamp - if glbl.name.find("&") == 0: - glbl.name = glbl.name.replace("&", "") - glbl.wtype.attr_type = attr_types.amp - - if len(str_def.strip()) != 0: - return None - - if len(glbl.name.split(",")) > 1: - glbl_list = [] - for name in glbl.name.split(","): - name = name.strip() - glbl_list.append(WGlobal()) - glbl_list[-1].orig_text = glbl.orig_text - glbl_list[-1].wtype = glbl.wtype - glbl_list[-1].name = name - glbl_list[-1].containing_file = glbl.containing_file - glbl_list[-1].namespace = glbl.namespace - glbl_list[-1].is_const = glbl.is_const - return glbl_list - - return glbl - - @autostring - def gen_boost_py(self, *, stream): - args = [ - f'"{self.name}"', - ] - meth = "def_readonly_static" - if not self.is_const: - meth = "def_readwrite_static" - args.append(f"&{self.namespace}::{self.name}") - stream.write(f'\t\t\t.{meth}({", ".join(args)})\n') - - -def concat_namespace(tuple_list): - if len(tuple_list) == 0: - return "" - ret = "" - for namespace in tuple_list: - ret += "::" + namespace[0] - return ret[2:] - - -def calc_ident(text): - if len(text) == 0 or text[0] != " ": - return 0 - return calc_ident(text[1:]) + 1 - - -def assure_length(text, length, left=False): - if len(text) > length: - return text[:length] - if left: - return text + " " * (length - len(text)) - return " " * (length - len(text)) + text - - -def nesting_delta(s): - return s.count("{") - s.count("}") - - -def parse_header(source): - debug("Parsing " + source.name + ".pyh", 1) - source_file = open(source.name + ".pyh", "r") - - source_text = [] - in_line = source_file.readline() - - namespaces = [] - - while in_line: - if len(in_line) > 1: - source_text.append( - in_line.replace("char *", "char_p ").replace("char* ", "char_p ") - ) - in_line = source_file.readline() - - i = 0 - - namespaces = [] - classes = [] - private_segment = False - - while i < len(source_text): - line = ( - source_text[i] - .replace( - "YOSYS_NAMESPACE_BEGIN", - " namespace YOSYS_NAMESPACE{", - ) - .replace("YOSYS_NAMESPACE_END", " }") - ) - ugly_line = unpretty_string(line) - debug(f"READ:>> {line}", 2) - - # for anonymous unions, ignore union enclosure by skipping start line and replacing end line with new line - if "union {" in line: - j = i + 1 - while j < len(source_text): - union_line = source_text[j] - if "};" in union_line: - source_text[j] = "\n" - break - j += 1 - if j != len(source_text): - i += 1 - continue - - if str.startswith( - ugly_line, "namespace " - ): # and ugly_line.find("std") == -1 and ugly_line.find("__") == -1: - namespace_name = ugly_line[10:].replace("{", "").strip() - namespaces.append((namespace_name, ugly_line.count("{"))) - debug("-----NAMESPACE " + concat_namespace(namespaces) + "-----", 3) - i += 1 - continue - - if len(namespaces) != 0: - namespaces[-1] = ( - namespaces[-1][0], - namespaces[-1][1] + nesting_delta(ugly_line), - ) - if namespaces[-1][1] == 0: - debug("-----END NAMESPACE " + concat_namespace(namespaces) + "-----", 3) - namespaces.pop() - i += 1 - continue - - if ( - str.startswith(ugly_line, "struct ") or str.startswith(ugly_line, "class") - ) and ugly_line.count(";") == 0: - # Opening a record declaration which isn't a forward declaration - struct_name = ugly_line.split(" ")[1].split("::")[-1] - impl_namespaces = ugly_line.split(" ")[1].split("::")[:-1] - complete_namespace = concat_namespace(namespaces) - for namespace in impl_namespaces: - complete_namespace += "::" + namespace - debug("\tFound " + struct_name + " in " + complete_namespace, 2) - - base_class_name = None - if len(ugly_line.split(" : ")) > 1: # class is derived - deriv_str = ugly_line.split(" : ")[1] - if len(deriv_str.split("::")) > 1: # namespace of base class is given - base_class_name = deriv_str.split("::", 1)[1] - else: - base_class_name = deriv_str.split(" ")[0] - debug("\t " + struct_name + " is derived from " + base_class_name, 2) - base_class = class_by_name(base_class_name) - - c = (class_by_name(struct_name), ugly_line.count("{")) # calc_ident(line)) - debug(f"switch to {struct_name} in namespace {namespaces}", 2) - if struct_name in classnames: - c[0].namespace = complete_namespace - c[0].base_class = base_class - classes.append(c) - i += 1 - continue - - if len(classes): - c = (classes[-1][0], classes[-1][1] + nesting_delta(ugly_line)) - classes[-1] = c - if c[1] == 0: - if c[0] == None: - debug("\tExiting unknown class", 3) - else: - debug("\tExiting class " + c[0].name, 3) - classes.pop() - private_segment = False - i += 1 - continue - - class_ = classes[-1] if classes else None - - if class_ != None and ( - line.find("private:") != -1 or line.find("protected:") != -1 - ): - private_segment = True - i += 1 - continue - if class_ != None and line.find("public:") != -1: - private_segment = False - i += 1 - continue - - candidate = None - - if private_segment and class_ != None and class_[0] != None: - candidate = WConstructor.from_string( - ugly_line, source.name, class_[0], i, True - ) - if candidate != None: - debug( - '\t\tFound constructor of class "' - + class_[0].name - + '" in namespace ' - + concat_namespace(namespaces), - 2, - ) - class_[0].found_constrs.append(candidate) - i += 1 - continue - - if not private_segment and (class_ == None or class_[0] != None): - if class_ != None: - candidate = WFunction.from_string( - ugly_line, source.name, class_[0], i, concat_namespace(namespaces) - ) - else: - candidate = WFunction.from_string( - ugly_line, source.name, None, i, concat_namespace(namespaces) - ) - if candidate != None and candidate.name.find("::") == -1: - if class_ == None: - debug( - '\tFound unowned function "' - + candidate.name - + '" in namespace ' - + concat_namespace(namespaces), - 2, - ) - unowned_functions.append(candidate) - else: - debug( - '\t\tFound function "' - + candidate.name - + '" of class "' - + class_[0].name - + '" in namespace ' - + concat_namespace(namespaces), - 2, - ) - class_[0].found_funs.append(candidate) - else: - candidate = WEnum.from_string( - ugly_line, concat_namespace(namespaces), i - ) - if candidate != None: - enums.append(candidate) - debug( - '\tFound enum "' - + candidate.name - + '" in namespace ' - + concat_namespace(namespaces), - 2, - ) - elif class_ != None and class_[1] == 1: - candidate = WConstructor.from_string( - ugly_line, source.name, class_[0], i - ) - if candidate != None: - debug( - '\t\tFound constructor of class "' - + class_[0].name - + '" in namespace ' - + concat_namespace(namespaces), - 2, - ) - class_[0].found_constrs.append(candidate) - else: - candidate = WMember.from_string( - ugly_line, - source.name, - class_[0], - i, - concat_namespace(namespaces), - ) - if candidate != None: - if type(candidate) == list: - for c in candidate: - debug( - '\t\tFound member "' - + c.name - + '" of class "' - + class_[0].name - + '" of type "' - + c.wtype.name - + '"', - 2, - ) - class_[0].found_vars.extend(candidate) - else: - debug( - '\t\tFound member "' - + candidate.name - + '" of class "' - + class_[0].name - + '" of type "' - + candidate.wtype.name - + '"', - 2, - ) - class_[0].found_vars.append(candidate) - if candidate == None and class_ == None: - candidate = WGlobal.from_string( - ugly_line, source.name, i, concat_namespace(namespaces) - ) - if candidate != None: - if type(candidate) == list: - for c in candidate: - glbls.append(c) - debug( - '\tFound global "' - + c.name - + '" in namespace ' - + concat_namespace(namespaces), - 2, - ) - else: - glbls.append(candidate) - debug( - '\tFound global "' - + candidate.name - + '" in namespace ' - + concat_namespace(namespaces), - 2, - ) - - j = i - line = unpretty_string(line) - while ( - candidate == None - and j + 1 < len(source_text) - and line.count(";") <= 1 - and line.count("(") >= line.count(")") - ): - j += 1 - line = line + "\n" + unpretty_string(source_text[j]) - if class_ != None: - candidate = WFunction.from_string( - ugly_line, - source.name, - class_[0], - i, - concat_namespace(namespaces), - ) - else: - candidate = WFunction.from_string( - ugly_line, source.name, None, i, concat_namespace(namespaces) - ) - if candidate != None and candidate.name.find("::") == -1: - if class_ == None: - debug( - '\tFound unowned function "' - + candidate.name - + '" in namespace ' - + concat_namespace(namespaces), - 2, - ) - unowned_functions.append(candidate) - else: - debug( - '\t\tFound function "' - + candidate.name - + '" of class "' - + class_[0].name - + '" in namespace ' - + concat_namespace(namespaces), - 2, - ) - class_[0].found_funs.append(candidate) - continue - candidate = WEnum.from_string(line, concat_namespace(namespaces), i) - if candidate != None: - enums.append(candidate) - debug( - '\tFound enum "' - + candidate.name - + '" in namespace ' - + concat_namespace(namespaces), - 2, - ) - continue - if class_ != None: - candidate = WConstructor.from_string( - line, source.name, class_[0], i - ) - if candidate != None: - debug( - '\t\tFound constructor of class "' - + class_[0].name - + '" in namespace ' - + concat_namespace(namespaces), - 2, - ) - class_[0].found_constrs.append(candidate) - continue - if class_ == None: - candidate = WGlobal.from_string( - line, source.name, i, concat_namespace(namespaces) - ) - if candidate != None: - if type(candidate) == list: - for c in candidate: - glbls.append(c) - debug( - '\tFound global "' - + c.name - + '" in namespace ' - + concat_namespace(namespaces), - 2, - ) - else: - glbls.append(candidate) - debug( - '\tFound global "' - + candidate.name - + '" in namespace ' - + concat_namespace(namespaces), - 2, - ) - continue - if candidate != None: - while i < j: - i += 1 - line = ( - source_text[i] - .replace( - "YOSYS_NAMESPACE_BEGIN", - " namespace YOSYS_NAMESPACE{", - ) - .replace("YOSYS_NAMESPACE_END", " }") - ) - ugly_line = unpretty_string(line) - if len(namespaces) != 0: - namespaces[-1] = ( - namespaces[-1][0], - namespaces[-1][1] + nesting_delta(ugly_line), - ) - if namespaces[-1][1] == 0: - debug( - "-----END NAMESPACE " - + concat_namespace(namespaces) - + "-----", - 3, - ) - namespaces.pop() - if len(classes): - c = (classes[-1][0], classes[-1][1] + nesting_delta(ugly_line)) - classes[-1] = c - if c[1] == 0: - if c[0] == None: - debug("\tExiting unknown class", 3) - else: - debug("\tExiting class " + c[0].name, 3) - classes.pop() - private_segment = False - i += 1 - else: - i += 1 - - -def debug(message, level): - if level <= debug.debug_level: - print(message) - - -def expand_functions(): - global unowned_functions - new_funs = [] - for fun in unowned_functions: - new_funs.append(fun) - unowned_functions = new_funs - for source in sources: - for class_ in source.classes: - new_funs = [] - for fun in class_.found_funs: - new_funs.append(fun) - class_.found_funs = new_funs - - -def inherit_members(): - for source in sources: - for class_ in source.classes: - if class_.base_class: - base_funs = copy.deepcopy(class_.base_class.found_funs) - for fun in base_funs: - fun.member_of = class_ - fun.namespace = class_.namespace - base_vars = copy.deepcopy(class_.base_class.found_vars) - for var in base_vars: - var.member_of = class_ - var.namespace = class_.namespace - class_.found_funs.extend(base_funs) - class_.found_vars.extend(base_vars) - - -def clean_duplicates(): - for source in sources: - for class_ in source.classes: - known_decls = {} - for fun in class_.found_funs: - if fun.gen_decl_hash_py() in known_decls: - debug("Multiple declarations of " + fun.gen_decl_hash_py(), 3) - - other = known_decls[fun.gen_decl_hash_py()] - if fun.mangled_name == other.mangled_name: - fun.duplicate = True - debug('Disabled "' + fun.gen_decl_hash_py() + '"', 3) - continue - - other.gen_alias() - fun.gen_alias() - else: - known_decls[fun.gen_decl_hash_py()] = fun - known_decls = [] - for con in class_.found_constrs: - if con.gen_decl_hash_py() in known_decls: - debug("Multiple declarations of " + con.gen_decl_hash_py(), 3) - con.duplicate = True - else: - known_decls.append(con.gen_decl_hash_py()) - known_decls = [] - for fun in unowned_functions: - if fun.gen_decl_hash_py() in known_decls: - debug("Multiple declarations of " + fun.gen_decl_hash_py(), 3) - fun.duplicate = True - else: - known_decls.append(fun.gen_decl_hash_py()) - - -def gen_wrappers(filename, debug_level_=0): - filename = Path(filename) - - debug.debug_level = debug_level_ - for source in sources: - parse_header(source) - - expand_functions() - inherit_members() - clean_duplicates() - - import shutil - import math - - col = shutil.get_terminal_size((80, 20)).columns - debug("-" * col, 1) - debug( - "-" * math.floor((col - 7) / 2) + "SUMMARY" + "-" * math.ceil((col - 7) / 2), 1 - ) - debug("-" * col, 1) - for source in sources: - for class_ in source.classes: - debug( - "Class " - + assure_length(class_.name, len(max(classnames, key=len)), True) - + " contains " - + assure_length(str(len(class_.found_vars)), 3, False) - + " member variables, " - + assure_length(str(len(class_.found_funs)), 3, False) - + " methods and " - + assure_length(str(len(class_.found_constrs)), 2, False) - + " constructors", - 1, - ) - if len(class_.found_constrs) == 0: - class_.found_constrs.append(WConstructor(source.name, class_)) - debug(str(len(unowned_functions)) + " functions are unowned", 1) - debug(str(len(unowned_functions)) + " global variables", 1) - for enum in enums: - debug( - "Enum " - + assure_length(enum.name, len(max(enum_names, key=len)), True) - + " contains " - + assure_length(str(len(enum.values)), 2, False) - + " values", - 1, - ) - debug("-" * col, 1) - - tpl = __file_dir__ / "wrappers_tpl.cc" - with open(tpl, encoding="utf8") as f, open( - filename, "w", encoding="utf8" - ) as wrapper_file: - do_line_directive = True - for i, line in enumerate(f): - if do_line_directive: - wrapper_file.write(f'#line {i + 1} "{tpl}"\n') - do_line_directive = False - if "" in line: - for source in sources: - wrapper_file.write(f'#include "{source.name}.h"\n') - do_line_directive = True - elif "" in line: - wrapper_file.write("// Opaque Container Declaration\n") - for i, (container_identifier, container) in enumerate(WMember.opaque_containers.items()): - wrapper_file.write(f"using {container_identifier} = {container.gen_text_cpp()};\n") - wrapper_file.write(f'PYBIND11_MAKE_OPAQUE({container_identifier})\n') - wrapper_file.write("\n") - do_line_directive = True - elif "" in line: - do_line_directive = True - elif "" in line: - wrapper_file.write("\t\t// Opaque Container Binding\n") - for container in WMember.opaque_containers.values(): - container.cont.gen_boost_py(stream=wrapper_file) - wrapper_file.write("\n") - - wrapper_file.write("\t\t// Enums\n") - for enum in enums: - enum.gen_boost_py(stream=wrapper_file) - wrapper_file.write("\n") - - wrapper_file.write("\t\t// Classes\n") - for source in sources: - for wclass in source.classes: - wclass.gen_boost_py(stream=wrapper_file) - wrapper_file.write("\n") - - wrapper_file.write("\t\t// Global Functions\n") - for fun in sorted(unowned_functions, key=lambda x: x.alias): - fun.gen_boost_py(stream=wrapper_file) - wrapper_file.write("\n") - - wrapper_file.write("\t\t// Global Variables\n") - wrapper_file.write('\t\tpy::class_(m, "Yosys")\n') - for glbl in sorted(glbls, key=lambda x: (not x.is_const, x.name)): - glbl.gen_boost_py(stream=wrapper_file) - wrapper_file.write("\t\t\t;\n") - do_line_directive = True - else: - wrapper_file.write(line) - def print_includes(): - for source in sources: - print(source.name + ".pyh") + for header in pyosys_headers: + print(header.name) if __name__ == "__main__": - ap = argparse.ArgumentParser() - ap.add_argument("--print-includes", action="store_true") - ap.add_argument("--debug", default=0, type=int) - ap.add_argument("output", nargs="?") - ns = ap.parse_args() - if ns.print_includes: - print_includes() - exit(0) - gen_wrappers(ns.output, ns.debug) + 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() diff --git a/pyosys/hashlib.h b/pyosys/hashlib.h index e8ace90c8..2ecadc3f1 100644 --- a/pyosys/hashlib.h +++ b/pyosys/hashlib.h @@ -48,6 +48,7 @@ #include "kernel/hashlib.h" namespace pybind11 { +namespace hashlib { template struct is_pointer { static const bool value = false; }; @@ -59,6 +60,11 @@ bool is_mapping(object obj) { return isinstance(obj, mapping); } +// shim +template +void bind_vector(module &m, const char *name_cstr) { + pybind11::bind_vector(m, name_cstr); +} // also used for std::set because the semantics are close enough template @@ -106,6 +112,13 @@ void bind_pool(module &m, const char *name_cstr) { }); } +// shim +template +void bind_set(module &m, const char *name_cstr) { + bind_pool(m, name_cstr); +} + + template void update_dict(C *target, iterable &iterable_or_mapping) { if (is_mapping(iterable_or_mapping)) { @@ -272,4 +285,5 @@ void bind_idict(module &m, const char *name_cstr) { }); } } +}; // namespace hashlib }; // namespace pybind11 diff --git a/pyosys/wrappers_tpl.cc b/pyosys/wrappers_tpl.cc index e921ae17a..d235b2c55 100644 --- a/pyosys/wrappers_tpl.cc +++ b/pyosys/wrappers_tpl.cc @@ -26,7 +26,12 @@ USING_YOSYS_NAMESPACE -// +using std::set; +using std::regex; +using std::ostream; +using namespace RTLIL; + +#include "wrappers.inc.cc" namespace YOSYS_PYTHON { @@ -37,8 +42,6 @@ namespace YOSYS_PYTHON { struct YosysStatics{}; - // - // Trampolines for Classes with Python-Overridable Virtual Methods // https://pybind11.readthedocs.io/en/stable/advanced/classes.html#overriding-virtual-functions-in-python class PassTrampoline : public Pass { @@ -182,7 +185,18 @@ namespace YOSYS_PYTHON { })); } - m.def("log_to_stream", &log_to_stream, "pipes yosys logs to a Python stream"); + // Logging Methods + m.def("log_header", [](Design *d, std::string s) { log_formatted_header(d, "%s", s); }); + m.def("log", [](std::string s) { log_formatted_string("%s", s); }); + m.def("log_file_info", [](std::string_view file, int line, std::string s) { log_formatted_file_info(file, line, s); }); + m.def("log_warning", [](std::string s) { log_formatted_warning("Warning: ", s); }); + m.def("log_warning_noprefix", [](std::string s) { log_formatted_warning("", s); }); + m.def("log_file_warning", [](std::string_view file, int line, std::string s) { log_formatted_file_warning(file, line, s); }); + m.def("log_error", [](std::string s) { log_formatted_error(s); }); + m.def("log_file_error", [](std::string_view file, int line, std::string s) { log_formatted_file_error(file, line, s); }); + + // Namespace to host global objects + auto global_variables = py::class_(m, "Yosys"); // Trampoline Classes py::class_>(m, "Pass") @@ -241,6 +255,9 @@ namespace YOSYS_PYTHON { .def("notify_blackout", &RTLIL::Monitor::notify_blackout) ; + // Bind Opaque Containers + bind_autogenerated_opaque_containers(m); + // }; }; diff --git a/pyproject.toml b/pyproject.toml index caa620528..5a218c19d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,7 @@ [build-system] requires = [ - "setuptools>=42", - "pybind11>=3,<4", + "setuptools>=42", + "pybind11>=3,<4", + "cxxheaderparser", ] build-backend = "setuptools.build_meta" diff --git a/tests/pyosys/test_logs.py b/tests/pyosys/test_logs.py new file mode 100644 index 000000000..b6bd02222 --- /dev/null +++ b/tests/pyosys/test_logs.py @@ -0,0 +1,8 @@ +from pyosys import libyosys as ys + +d = ys.Design(); ys.log_header(d, "foo\n") +ys.log("foo\n") +ys.log_warning("foo\n") +ys.log_warning_noprefix("foo\n") +ys.log_file_info("foo.ys", 1, "foo\n") +ys.log_file_warning("foo.ys", 1, "foo\n") From 54799bb8be85f784b9682f5643e0f41f93684863 Mon Sep 17 00:00:00 2001 From: Mohamed Gaber Date: Tue, 23 Sep 2025 03:44:34 +0300 Subject: [PATCH 3/9] pyosys: globals, set operators for opaque types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There is so much templating going on that compiling wrappers.cc now takes 1m1.668s on an Apple M4… --- Makefile | 2 +- kernel/yosys.cc | 10 +- pyosys/generator.py | 48 ++++--- pyosys/hashlib.h | 241 +++++++++++++++++++++++++++++++++--- pyosys/wrappers_tpl.cc | 6 - pyproject.toml | 4 + tests/pyosys/test_dict.py | 18 ++- tests/pyosys/test_import.py | 19 ++- tests/pyosys/test_set.py | 42 +++++++ 9 files changed, 343 insertions(+), 47 deletions(-) create mode 100644 tests/pyosys/test_set.py diff --git a/Makefile b/Makefile index c9cbd0810..027f18a7a 100644 --- a/Makefile +++ b/Makefile @@ -102,7 +102,7 @@ all: top-all YOSYS_SRC := $(dir $(firstword $(MAKEFILE_LIST))) VPATH := $(YOSYS_SRC) -CXXSTD ?= c++17 +export CXXSTD ?= c++17 CXXFLAGS := $(CXXFLAGS) -Wall -Wextra -ggdb -I. -I"$(YOSYS_SRC)" -MD -MP -D_YOSYS_ -fPIC -I$(PREFIX)/include LIBS := $(LIBS) -lstdc++ -lm PLUGIN_LINKFLAGS := diff --git a/kernel/yosys.cc b/kernel/yosys.cc index 9cab12bf6..47057c1ca 100644 --- a/kernel/yosys.cc +++ b/kernel/yosys.cc @@ -185,13 +185,12 @@ bool already_setup = false; bool already_shutdown = false; #ifdef WITH_PYTHON -// Include pyosys as a module so 'from pyosys import libyosys' also works -// in interpreter mode. +// Include pyosys as a package for some compatibility with wheels. // // This should not affect using wheels as the dylib has to actually be called -// pyosys.so for this module to be interacted with at all. +// pyosys.so for this function to be interacted with at all. PYBIND11_MODULE(pyosys, m) { - m.add_object("libyosys", m.import("libyosys")); + m.add_object("__path__", py::list()); } #endif @@ -209,7 +208,8 @@ void yosys_setup() // initialized platform fails (such as when libyosys is imported // from a Python interpreter) if (!Py_IsInitialized()) { - PyImport_AppendInittab((char*)"libyosys", PyInit_libyosys); + PyImport_AppendInittab((char*)"pyosys.libyosys", PyInit_libyosys); + // compatibility with wheels PyImport_AppendInittab((char*)"pyosys", PyInit_pyosys); Py_Initialize(); PyRun_SimpleString("import sys"); diff --git a/pyosys/generator.py b/pyosys/generator.py index b8a7aa8dc..b0cec99e3 100644 --- a/pyosys/generator.py +++ b/pyosys/generator.py @@ -18,7 +18,19 @@ # Written by Mohamed Gaber # # 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 @@ -58,15 +70,24 @@ class PyosysClass: :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 - :param hash_expr: A C++ expression that will be fed to ``run_hash`` to declare a __hash__ method for Python + :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) + # in the format s.(method or property) (or just s) string_expr: Optional[str] = None hash_expr: Optional[str] = None @@ -89,9 +110,7 @@ class PyosysHeader: for cls in classes: self.classes_by_name[cls.name] = cls -""" -Add headers and classes here! -""" +# MARK: Inclusion and Exclusion global_denylist = frozenset( { # deprecated @@ -327,10 +346,11 @@ class PyosysWrapperGenerator(object): 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_", "WITH_PYTHON"], - gcc_args=[preprocessor_bin, "-fsyntax-only"], + gcc_args=[preprocessor_bin, "-fsyntax-only", f"-std={cxx_std}"], include_paths=[str(__yosys_root__), py_include, pybind11.get_include()], ), ) @@ -476,7 +496,7 @@ class PyosysWrapperGenerator(object): 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) + 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 ( @@ -496,7 +516,7 @@ class PyosysWrapperGenerator(object): self.register_containers(function) - print(f"\t\t\tm.def({", ".join(self.get_definition_args(function, None))});", file=self.f) + 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": @@ -511,7 +531,8 @@ class PyosysWrapperGenerator(object): unique_ptrs = self.find_containers(("unique_ptr",), field.type) if len(unique_ptrs): - # TODO: figure out how to bridge unique pointers + # TODO: figure out how to bridge unique pointers maybe does anyone + # care return self.register_containers(field) @@ -559,10 +580,10 @@ class PyosysWrapperGenerator(object): self.process_method(method, basename) visited_anonymous_unions = set() - for field in cls.fields: - if field.name in metadata.denylist: + for field_ in cls.fields: + if field_.name in metadata.denylist: continue - self.process_field(field, basename) + self.process_field(field_, basename) # Handle anonymous unions for subclass in cls.classes: @@ -663,7 +684,6 @@ class PyosysWrapperGenerator(object): keyword_aliases = { - "in": "in_", "False": "False_", "None": "None_", "True": "True_", diff --git a/pyosys/hashlib.h b/pyosys/hashlib.h index 2ecadc3f1..b772a2bb3 100644 --- a/pyosys/hashlib.h +++ b/pyosys/hashlib.h @@ -37,7 +37,7 @@ // things like mutating containers that are class properties. // // All methods should be vaguely in the same order as the python reference -// https://docs.python.org/3/library/stdtypes.html +// https://docs.python.org/3.13/library/stdtypes.html // #include // optional maps cleanest to methods that accept None in Python @@ -60,26 +60,188 @@ bool is_mapping(object obj) { return isinstance(obj, mapping); } +// Set Operations +bool is_subset(const iterable &lhs, const iterable &rhs, bool strict = false) { + for (auto &element: lhs) { + if (!rhs.contains(element)) { + return false; + } + } + if (strict) { + return len(rhs) > len(lhs); + } + return true; +} + +template +void unionize(C &lhs, const iterable &rhs) { + for (auto &element: rhs) { + lhs.insert(cast(element)); + } +} + +template +void difference(C &lhs, const iterable &rhs) { + for (auto &element: rhs) { + auto element_cxx = cast(element); + if (lhs.count(element_cxx)) { + lhs.erase(element_cxx); + } + } +} + +template +void intersect(C &lhs, const iterable &rhs) { + // Doing it in-place is a lot slower + // TODO?: Leave modifying lhs to caller (saves a copy) but complicates + // chaining intersections. + C storage(lhs); + + for (auto &element_cxx: lhs) { + if (!rhs.contains(cast(element_cxx))) { + storage.erase(element_cxx); + } + } + lhs = std::move(storage); +} + +template +void symmetric_difference(C &lhs, const iterable &rhs) { + C storage(lhs); + + for (auto &element: rhs) { + auto element_cxx = cast(element); + if (lhs.count(element_cxx)) { + storage.erase(element_cxx); + } else { + storage.insert(element_cxx); + } + } + for (auto &element_cxx: lhs) { + if (rhs.contains(cast(element_cxx))) { + storage.erase(element_cxx); + } + } + lhs = std::move(storage); +} + // shim template void bind_vector(module &m, const char *name_cstr) { pybind11::bind_vector(m, name_cstr); } -// also used for std::set because the semantics are close enough +// also used for hashlib pool because the semantics are close enough template -void bind_pool(module &m, const char *name_cstr) { - std::string {name_cstr}; - +void bind_set(module &m, const char *name_cstr) { class_(m, name_cstr) .def(init<>()) + .def(init()) // copy constructor + .def(init([](const iterable &other){ // copy instructor from arbitrary iterables + auto s = new C(); + unionize(*s, other); + return s; + })) .def("__len__", [](const C &s){ return (size_t)s.size(); }) .def("__contains__", [](const C &s, const T &v){ return s.count(v); }) .def("__delitem__", [](C &s, const T &v) { auto n = s.erase(v); if (n == 0) throw key_error(str(cast(v))); }) - // TODO: disjoint, subset, union, intersection, difference, symdif + .def("disjoint", [](const C &s, const iterable &other) { + for (const auto &element: other) { + if (s.count(cast(element))) { + return false; + } + } + return true; + }) + .def("issubset", [](const iterable &s, const iterable &other) { + return is_subset(s, other); + }) + .def("__eq__", [](const iterable &s, const iterable &other) { + return is_subset(s, other) && len(s) == len(other); + }) + .def("__le__", [](const iterable &s, const iterable &other) { + return is_subset(s, other); + }) + .def("__lt__", [](const iterable &s, const iterable &other) { + return is_subset(s, other, true); + }) + .def("issuperset", [](const iterable &s, const iterable &other) { + return is_subset(other, s); + }) + .def("__ge__", [](const iterable &s, const iterable &other) { + return is_subset(other, s); + }) + .def("__gt__", [](const iterable &s, const iterable &other) { + return is_subset(other, s, true); + }) + .def("union", [](const C &s, const args &others) { + auto result = new C(s); + for (const auto &arg: others) { + auto arg_iterable = reinterpret_borrow(arg); + unionize(*result, arg_iterable); + } + return result; + }) + .def("__or__", [](const C &s, const iterable &other) { + auto result = new C(s); + unionize(*result, other); + return result; + }) + .def("__ior__", [](C &s, const iterable &other) { + unionize(s, other); + return s; + }) + .def("intersection", [](const C &s, const args &others) { + auto result = new C(s); + for (const auto &arg: others) { + auto arg_iterable = reinterpret_borrow(arg); + intersect(*result, arg_iterable); + } + return result; + }) + .def("__and__", [](const C &s, const iterable &other) { + auto result = new C(s); + intersect(*result, other); + return result; + }) + .def("__iand__", [](C &s, const iterable &other) { + intersect(s, other); + return s; + }) + .def("difference", [](const C &s, const args &others) { + auto result = new C(s); + for (const auto &arg: others) { + auto arg_iterable = reinterpret_borrow(arg); + difference(*result, arg_iterable); + } + return result; + }) + .def("__sub__", [](const C &s, const iterable &other) { + auto result = new C(s); + difference(*result, other); + return result; + }) + .def("__isub__", [](C &s, const iterable &other) { + difference(s, other); + return s; + }) + .def("symmetric_difference", [](const C &s, const iterable &other) { + auto result = new C(s); + symmetric_difference(*result, other); + return result; + }) + .def("__xor__", [](const C &s, const iterable &other) { + auto result = new C(s); + symmetric_difference(*result, other); + return result; + }) + .def("__ixor__", [](C &s, const iterable &other) { + symmetric_difference(s, other); + return s; + }) .def("copy", [](const C &s) { return new C(s); }) @@ -107,20 +269,29 @@ void bind_pool(module &m, const char *name_cstr) { .def("__iter__", [](const C &s){ return make_iterator(s.begin(), s.end()); }, keep_alive<0,1>()) - .def("__repr__", [name_cstr](const C &s){ - return std::string("<") + name_cstr + " size=" + std::to_string(s.size()) + ">"; + .def("__repr__", [name_cstr](const py::iterable &s){ + // repr(set(s)) where s is iterable would be more terse/robust + // but are there concerns with copying? + str representation = str(name_cstr) + str("({"); + str comma(", "); + for (const auto &element: s) { + representation += repr(element); + representation += comma; // python supports trailing commas + } + representation += str("})"); + return representation; }); } // shim template -void bind_set(module &m, const char *name_cstr) { - bind_pool(m, name_cstr); +void bind_pool(module &m, const char *name_cstr) { + bind_set(m, name_cstr); } template -void update_dict(C *target, iterable &iterable_or_mapping) { +void update_dict(C *target, const iterable &iterable_or_mapping) { if (is_mapping(iterable_or_mapping)) { for (const auto &key: iterable_or_mapping) { (*target)[cast(key)] = cast(iterable_or_mapping[key]); @@ -137,10 +308,14 @@ void update_dict(C *target, iterable &iterable_or_mapping) { template void bind_dict(module &m, const char *name_cstr) { - std::string {name_cstr}; - - class_(m, name_cstr) + auto cls = class_(m, name_cstr) .def(init<>()) + .def(init()) // copy constructor + .def(init([](const iterable &other){ // copy instructor from arbitrary iterables and mappings + auto s = new C(); + update_dict(s, other); + return s; + })) .def("__len__", [](const C &s){ return (size_t)s.size(); }) .def("__getitem__", [](const C &s, const K &k) { return s.at(k); }) .def("__setitem__", [](C &s, const K &k, const V &v) { s[k] = v; }) @@ -210,9 +385,29 @@ void bind_dict(module &m, const char *name_cstr) { return s; }) .def("__bool__", [](const C &s) { return s.size() != 0; }) - .def("__repr__", [name_cstr](const C &s){ - return std::string("<") + name_cstr + " size=" + std::to_string(s.size()) + ">"; + .def("__repr__", [name_cstr](const C &s) { + // repr(dict(s)) where s is iterable would be more terse/robust + // but are there concerns with copying? + str representation = str(name_cstr) + str("({"); + str colon(": "); + str comma(", "); + for (const auto &item: s) { + representation += repr(cast(item.first)); + representation += colon; + representation += repr(cast(item.second)); + representation += comma; // python supports trailing commas + } + representation += str("})"); + return representation; }); + + // Inherit from collections.abc.Mapping so update operators (and a bunch + // of other things) work. + auto collections_abc = module_::import("collections.abc"); + auto mapping = getattr(collections_abc, "Mapping"); + auto current_bases = list(getattr(cls, "__bases__")); + current_bases.append(mapping); + setattr(cls, "__bases__", tuple(current_bases)); } // idict is a special bijection and doesn't map cleanly to dict @@ -221,10 +416,9 @@ void bind_dict(module &m, const char *name_cstr) { // the hashable as key and the integer as value template void bind_idict(module &m, const char *name_cstr) { - std::string {name_cstr}; - auto cls = class_(m, name_cstr) .def(init<>()) + .def(init()) // copy constructor .def("__len__", [](const C &s){ return (size_t)s.size(); }) .def("__getitem__", [](const C &s, int v) { return s[v]; }) .def("__call__", [](C &s, const K &k) { return s(k); }) @@ -276,7 +470,16 @@ void bind_idict(module &m, const char *name_cstr) { }) .def("__bool__", [](const C &s) { return s.size() != 0; }) .def("__repr__", [name_cstr](const C &s){ - return std::string("<") + name_cstr + " size=" + std::to_string(s.size()) + ">"; + // repr(dict(s)) where s is iterable would be more terse/robust + // but are there concerns with copying? + str representation = str(name_cstr) + str("() | {"); + str comma(", "); + for (const auto &item: s) { + representation += repr(cast(item)); + representation += comma; // python supports trailing commas + } + representation += str("}"); + return representation; }); for (const char *mutator: {"__setitem__", "__delitem__", "pop", "popitem", "setdefault"}) { diff --git a/pyosys/wrappers_tpl.cc b/pyosys/wrappers_tpl.cc index d235b2c55..84e201657 100644 --- a/pyosys/wrappers_tpl.cc +++ b/pyosys/wrappers_tpl.cc @@ -34,12 +34,6 @@ using namespace RTLIL; #include "wrappers.inc.cc" namespace YOSYS_PYTHON { - - [[noreturn]] static void log_python_exception_as_error() { - PyErr_Print(); - log_error("Python interpreter encountered an exception.\\n"); - } - struct YosysStatics{}; // Trampolines for Classes with Python-Overridable Virtual Methods diff --git a/pyproject.toml b/pyproject.toml index 5a218c19d..d5882084c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,3 +5,7 @@ requires = [ "cxxheaderparser", ] build-backend = "setuptools.build_meta" + +[tool.ruff] +target-version = "py38" +lint.ignore = ["F541"] diff --git a/tests/pyosys/test_dict.py b/tests/pyosys/test_dict.py index 81e7b5516..916d69b92 100644 --- a/tests/pyosys/test_dict.py +++ b/tests/pyosys/test_dict.py @@ -1,6 +1,12 @@ +from typing import Mapping from pyosys import libyosys as ys -my_dict = ys.StringToStringDict() +StringToStringDict = ys.StringToStringDict + +my_dict = StringToStringDict() + +assert isinstance(my_dict, Mapping) + my_dict["foo"] = "bar" my_dict.update([("first", "second")]) my_dict.update({"key": "value"}) @@ -11,3 +17,13 @@ new_dict = my_dict | {"tomato": "tomato"} del new_dict["foo"] assert set(my_dict.keys()) == {"first", "key", "foo"} assert set(new_dict.keys()) == {"first", "key", "tomato"} + +constructor_test_1 = ys.StringToStringDict(new_dict) +constructor_test_2 = ys.StringToStringDict([("tomato", "tomato")]) +constructor_test_3 = ys.StringToStringDict({ "im running": "out of string ideas" }) + +the_great_or = constructor_test_1 | constructor_test_2 | constructor_test_3 + +assert set(the_great_or) == {"first", "key", "tomato", "im running"} +repr_test = eval(repr(the_great_or)) +assert repr_test == the_great_or diff --git a/tests/pyosys/test_import.py b/tests/pyosys/test_import.py index 48e911033..b6a36b0c1 100644 --- a/tests/pyosys/test_import.py +++ b/tests/pyosys/test_import.py @@ -1,3 +1,20 @@ +import os +import sys + from pyosys import libyosys as ys -ys.log("Hello, world!") +print(ys) + +ys.log("Hello, world!\n") + +from pyosys.libyosys import log + +print(log) + +log("Goodbye, world!\n") + +import pyosys + +if os.path.basename(sys.executable) == "yosys": + # make sure it's not importing the directory + assert "built-in" in repr(pyosys) diff --git a/tests/pyosys/test_set.py b/tests/pyosys/test_set.py new file mode 100644 index 000000000..d89c5243e --- /dev/null +++ b/tests/pyosys/test_set.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +from pyosys.libyosys import StringSet, StringPool + +for cls in [StringSet, StringPool]: + print(f"Testing {cls.__name__}...") + A = cls() + A.add("a") + + B = cls() + B = A | {"b"} + + assert A < B + assert A <= B + + A.add("b") + + assert A == B + assert A <= B + assert not A < B + + A.add("c") + + assert A > B + + A &= B + assert A == B + + Ø = A - B + assert len(Ø) == 0 + + C = cls({"A", "B", "C"}) + D = cls() + C |= {"A", "B", "C"} + D |= {"C", "D", "E"} + c_symdiff_d = (C ^ D) + assert (c_symdiff_d) == {"A", "B", "D", "E"} + + repr_test = eval(repr(c_symdiff_d)) + c_symdiff_d == repr_test + + +print("Done.") From dc88906c9199e93e89a2d78e6a66acc9567cf834 Mon Sep 17 00:00:00 2001 From: Mohamed Gaber Date: Tue, 23 Sep 2025 08:05:04 +0300 Subject: [PATCH 4/9] tests/pyosys: print log on failed test, fix make clean --- .github/workflows/wheels.yml | 3 ++- Makefile | 7 ++++--- tests/pyosys/run_tests.py | 39 +++++++++++++++++++++--------------- 3 files changed, 29 insertions(+), 20 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index b561b5d32..d6e9832aa 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -2,6 +2,7 @@ name: Build Wheels for PyPI # run every Sunday at 10 AM on: + push: # TODO: REMOVE THIS, DO NOT MERGE TO UPSTREAM THIS IS JUST SO I DON'T HAVE TO MANUALLY RUN THE WORKFLOW WITH EVERY PUSH workflow_dispatch: schedule: - cron: "0 10 * * 0" @@ -107,7 +108,7 @@ jobs: makeFlags='CONFIG=clang' PATH="$PWD/bison/src:$PATH" CIBW_BEFORE_BUILD: bash ./.github/workflows/wheels/cibw_before_build.sh - CIBW_TEST_COMMAND: python3 {project}/tests/pyosys/run_tests.py python3 + CIBW_TEST_COMMAND: python3 {project}/tests/pyosys/run_tests.py - uses: actions/upload-artifact@v4 with: name: python-wheels-${{ matrix.os.runner }} diff --git a/Makefile b/Makefile index 027f18a7a..09d5f055d 100644 --- a/Makefile +++ b/Makefile @@ -344,6 +344,8 @@ ifeq ($(ENABLE_LIBYOSYS),1) TARGETS += libyosys.so endif +PY_WRAPPER_FILE = pyosys/wrappers + ifeq ($(ENABLE_PYOSYS),1) # python-config --ldflags includes -l and -L, but LINKFLAGS is only -L LINKFLAGS += $(filter-out -l%,$(shell $(PYTHON_CONFIG) --ldflags)) @@ -353,7 +355,6 @@ PYBIND11_INCLUDE ?= $(shell $(PYTHON_EXECUTABLE) -m pybind11 --includes) CXXFLAGS += -I$(PYBIND11_INCLUDE) -DWITH_PYTHON CXXFLAGS += $(shell $(PYTHON_CONFIG) --includes) -DWITH_PYTHON -PY_WRAPPER_FILE = pyosys/wrappers OBJS += $(PY_WRAPPER_FILE).o PY_GEN_SCRIPT = pyosys/generator.py PY_WRAP_INCLUDES := $(shell $(PYTHON_EXECUTABLE) $(PY_GEN_SCRIPT) --print-includes) @@ -1110,9 +1111,9 @@ docs: docs/prep clean: rm -rf share rm -rf kernel/*.pyh - rm -f $(OBJS) $(GENFILES) $(TARGETS) $(EXTRA_TARGETS) $(EXTRA_OBJS) $(PY_WRAP_INCLUDES) $(PY_WRAPPER_FILE).cc + rm -f $(OBJS) $(GENFILES) $(TARGETS) $(EXTRA_TARGETS) $(EXTRA_OBJS) $(PY_WRAP_INCLUDES) $(PY_WRAPPER_FILE).inc.cc $(PY_WRAPPER_FILE).cc rm -f kernel/version_*.o kernel/version_*.cc - rm -f kernel/python_wrappers.o + rm -f $(PY_WRAPPER_FILE).o rm -f libs/*/*.d frontends/*/*.d passes/*/*.d backends/*/*.d kernel/*.d techlibs/*/*.d rm -rf tests/asicworld/*.out tests/asicworld/*.log rm -rf tests/hana/*.out tests/hana/*.log diff --git a/tests/pyosys/run_tests.py b/tests/pyosys/run_tests.py index 953462c4f..6c631c507 100644 --- a/tests/pyosys/run_tests.py +++ b/tests/pyosys/run_tests.py @@ -1,39 +1,46 @@ -from pathlib import Path -import shutil -import subprocess import sys +import shutil +import shlex +import subprocess +from pathlib import Path __file_dir__ = Path(__file__).absolute().parent -if len(sys.argv) != 2: - print(f"Usage: {sys.argv[0]} {sys.argv[1]}") +if len(sys.argv) > 2 or len(sys.argv) == 2 and sys.argv[1] != 'yosys': + print(f"Usage: {sys.argv[0]} [yosys]") exit(64) -binary = [] -if sys.argv[1] in ["yosys"]: - binary = [__file_dir__.parents[1] / "yosys", "-Qy"] +if len(sys.argv) == 2: + binary = [str(__file_dir__.parents[1] / "yosys"), "-Qy"] else: - binary = [sys.argv[1]] + binary = [sys.executable or shutil.which("python3") or "python3"] # sys.executable can actually be None tests = __file_dir__.glob("test_*.py") -errors = False + log_dir = __file_dir__ / "logs" try: shutil.rmtree(log_dir) except FileNotFoundError: pass +fail_logs = set() for test in tests: print(f"* {test.name} ", end="") log_dir.mkdir(parents=True, exist_ok=True) log = log_dir / (test.stem + ".log") - result = subprocess.run([ - *binary, - test - ], stdout=open(log, "w"), stderr=subprocess.STDOUT) + cmd = [*binary, str(test)] + log_file = open(log, "w", encoding="utf8") + log_file.write(f"$ {shlex.join(cmd)}\n") + log_file.flush() + result = subprocess.run(cmd, stdout=log_file, stderr=subprocess.STDOUT) if result.returncode == 0: print("OK!") else: print(f"FAILED: {log}") - errors = True -if errors: + fail_logs.add(log) + log_file.close() +for log in fail_logs: + print(f">>> {log}") + with open(log, encoding="utf8") as f: + print(f.read()) +if len(fail_logs): exit(1) From 447a6cb3f00d088c1fd92b304b8ad40ddf28dbd2 Mon Sep 17 00:00:00 2001 From: Mohamed Gaber Date: Tue, 23 Sep 2025 15:29:01 +0300 Subject: [PATCH 5/9] 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. --- .github/workflows/wheels/cibw_before_build.sh | 4 +-- Makefile | 20 +++++++++---- kernel/driver.cc | 16 +++++++---- kernel/rtlil.cc | 28 +++++++++---------- kernel/rtlil.h | 10 +++---- kernel/yosys.cc | 20 ++++++++----- kernel/yosys.h | 4 +-- kernel/yosys_common.h | 7 ----- passes/cmds/plugin.cc | 16 +++++++---- pyosys/generator.py | 2 +- pyosys/hashlib.h | 5 ++-- pyosys/wrappers_tpl.cc | 10 ++++--- 12 files changed, 82 insertions(+), 60 deletions(-) diff --git a/.github/workflows/wheels/cibw_before_build.sh b/.github/workflows/wheels/cibw_before_build.sh index 4e81688d0..1ce96b291 100644 --- a/.github/workflows/wheels/cibw_before_build.sh +++ b/.github/workflows/wheels/cibw_before_build.sh @@ -1,8 +1,8 @@ set -e set -x -# Don't use objects from previous compiles on Windows/macOS -make clean +# Don't use Python objects from previous compiles +make clean-py # DEBUG: show python3 and python3-config outputs if [ "$(uname)" != "Linux" ]; then diff --git a/Makefile b/Makefile index 09d5f055d..22aeee5e8 100644 --- a/Makefile +++ b/Makefile @@ -346,14 +346,18 @@ endif PY_WRAPPER_FILE = pyosys/wrappers +# running make clean on just those and then recompiling saves a lot of +# time when running cibuildwheel +PYTHON_OBJECTS = pyosys/wrappers.o kernel/drivers.o kernel/yosys.o passes/cmds/plugin.o + ifeq ($(ENABLE_PYOSYS),1) # python-config --ldflags includes -l and -L, but LINKFLAGS is only -L LINKFLAGS += $(filter-out -l%,$(shell $(PYTHON_CONFIG) --ldflags)) LIBS += $(shell $(PYTHON_CONFIG) --libs) EXE_LIBS += $(filter-out $(LIBS),$(shell $(PYTHON_CONFIG_FOR_EXE) --libs)) PYBIND11_INCLUDE ?= $(shell $(PYTHON_EXECUTABLE) -m pybind11 --includes) -CXXFLAGS += -I$(PYBIND11_INCLUDE) -DWITH_PYTHON -CXXFLAGS += $(shell $(PYTHON_CONFIG) --includes) -DWITH_PYTHON +CXXFLAGS += -I$(PYBIND11_INCLUDE) -DYOSYS_ENABLE_PYTHON +CXXFLAGS += $(shell $(PYTHON_CONFIG) --includes) -DYOSYS_ENABLE_PYTHON OBJS += $(PY_WRAPPER_FILE).o PY_GEN_SCRIPT = pyosys/generator.py @@ -1108,12 +1112,10 @@ DOC_TARGET ?= html docs: docs/prep $(Q) $(MAKE) -C docs $(DOC_TARGET) -clean: +clean: clean-py rm -rf share - rm -rf kernel/*.pyh - rm -f $(OBJS) $(GENFILES) $(TARGETS) $(EXTRA_TARGETS) $(EXTRA_OBJS) $(PY_WRAP_INCLUDES) $(PY_WRAPPER_FILE).inc.cc $(PY_WRAPPER_FILE).cc + rm -f $(OBJS) $(GENFILES) $(TARGETS) $(EXTRA_TARGETS) $(EXTRA_OBJS) $(PY_WRAP_INCLUDES) rm -f kernel/version_*.o kernel/version_*.cc - rm -f $(PY_WRAPPER_FILE).o rm -f libs/*/*.d frontends/*/*.d passes/*/*.d backends/*/*.d kernel/*.d techlibs/*/*.d rm -rf tests/asicworld/*.out tests/asicworld/*.log rm -rf tests/hana/*.out tests/hana/*.log @@ -1127,8 +1129,14 @@ clean: rm -f $(addsuffix /run-test.mk,$(MK_TEST_DIRS)) -$(MAKE) -C docs clean rm -rf docs/util/__pycache__ + rm -f libyosys.so + +clean-py: + rm -f $(PY_WRAPPER_FILE).inc.cc $(PY_WRAPPER_FILE).cc + rm -f $(PYTHON_OBJECTS) rm -f *.whl rm -f libyosys.so + rm -rf kernel/*.pyh clean-abc: $(MAKE) -C abc DEP= clean diff --git a/kernel/driver.cc b/kernel/driver.cc index a27c0a00f..795fd9fc5 100644 --- a/kernel/driver.cc +++ b/kernel/driver.cc @@ -37,6 +37,12 @@ # include #endif +#ifdef YOSYS_ENABLE_PYTHON +# include +# include +namespace py = pybind11; +#endif + #include #include #include @@ -91,7 +97,7 @@ int main(int argc, char **argv) log_error_stderr = true; yosys_banner(); yosys_setup(); -#ifdef WITH_PYTHON +#ifdef YOSYS_ENABLE_PYTHON py::object sys = py::module_::import("sys"); sys.attr("path").attr("append")(proc_self_dirname()); sys.attr("path").attr("append")(proc_share_dirname()); @@ -227,10 +233,10 @@ int main(int argc, char **argv) cxxopts::value(),"") ("C,tcl-interactive", "enters TCL interactive shell mode") #endif // YOSYS_ENABLE_TCL -#ifdef WITH_PYTHON +#ifdef YOSYS_ENABLE_PYTHON ("y,py-scriptfile", "execute the Python