3
0
Fork 0
mirror of https://github.com/YosysHQ/yosys synced 2025-10-08 17:01:57 +00:00

Merge pull request #5370 from donn/pyosys_pybind11

pyosys: rewrite using pybind11
This commit is contained in:
Miodrag Milanović 2025-10-08 13:07:59 +02:00 committed by GitHub
commit ba1a347d59
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 2382 additions and 2722 deletions

View file

@ -11,6 +11,10 @@ indent_style = space
indent_size = 2
trim_trailing_whitespace = false
[*.rst]
indent_style = space
indent_size = 3
[*.yml]
indent_style = space
indent_size = 2

View file

@ -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
- uses: actions/upload-artifact@v4
with:
name: python-wheels-${{ matrix.os.runner }}

View file

@ -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
@ -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

View file

@ -38,7 +38,8 @@ techlibs/gowin/ @pepijndevos
techlibs/gatemate/ @pu-cc
# pyosys
misc/*.py @btut
pyosys/* @donn
setup.py @donn
backends/firrtl @ucbjrl @azidar

View file

@ -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 :=
@ -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)
@ -348,31 +344,24 @@ ifeq ($(ENABLE_LIBYOSYS),1)
TARGETS += libyosys.so
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))
CXXFLAGS += $(shell $(PYTHON_CONFIG) --includes) -DWITH_PYTHON
PYBIND11_INCLUDE ?= $(shell $(PYTHON_EXECUTABLE) -m pybind11 --includes)
CXXFLAGS += -I$(PYBIND11_INCLUDE) -DYOSYS_ENABLE_PYTHON
CXXFLAGS += $(shell $(PYTHON_CONFIG) --includes) -DYOSYS_ENABLE_PYTHON
# Detect name of boost_python library. Some distros use boost_python-py<version>, other boost_python<version>, 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
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)
@ -786,9 +775,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
@ -1035,12 +1024,12 @@ ifeq ($(ENABLE_LIBYOSYS),1)
if [ -n "$(STRIP)" ]; then $(INSTALL_SUDO) $(STRIP) -S $(DESTDIR)$(LIBDIR)/libyosys.so; fi
ifeq ($(ENABLE_PYOSYS),1)
$(INSTALL_SUDO) mkdir -p $(DESTDIR)$(PYTHON_DESTDIR)/$(subst -,_,$(PROGRAM_PREFIX))pyosys
$(INSTALL_SUDO) cp pyosys/__init__.py $(DESTDIR)$(PYTHON_DESTDIR)/$(subst -,_,$(PROGRAM_PREFIX))pyosys/__init__.py
$(INSTALL_SUDO) cp libyosys.so $(DESTDIR)$(PYTHON_DESTDIR)/$(subst -,_,$(PROGRAM_PREFIX))pyosys/libyosys.so
$(INSTALL_SUDO) cp -r share $(DESTDIR)$(PYTHON_DESTDIR)/$(subst -,_,$(PROGRAM_PREFIX))pyosys
ifeq ($(ENABLE_ABC),1)
$(INSTALL_SUDO) cp yosys-abc $(DESTDIR)$(PYTHON_DESTDIR)/$(subst -,_,$(PROGRAM_PREFIX))pyosys/yosys-abc
endif
$(INSTALL_SUDO) cp misc/__init__.py $(DESTDIR)$(PYTHON_DESTDIR)/$(subst -,_,$(PROGRAM_PREFIX))pyosys/
endif
endif
ifeq ($(ENABLE_PLUGINS),1)
@ -1130,12 +1119,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).cc
rm -f $(OBJS) $(GENFILES) $(TARGETS) $(EXTRA_TARGETS) $(EXTRA_OBJS) $(PY_WRAP_INCLUDES)
rm -f kernel/version_*.o kernel/version_*.cc
rm -f kernel/python_wrappers.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
@ -1149,8 +1136,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

1
docs/.gitignore vendored
View file

@ -6,3 +6,4 @@
/source/_images/**/*.svg
/source/_images/**/*.dot
/source/_images/code_examples
/venv

View file

@ -0,0 +1,37 @@
from pyosys import libyosys as ys
class AllEnablePass(ys.Pass):
def __init__(self):
super().__init__(
"all_enable",
"makes all _DFF_P_ registers require an enable signal"
)
def execute(self, args, design):
ys.log_header(design, "Adding enable signals\n")
ys.log_push()
top_module = design.top_module()
if "\\enable" not in top_module.wires_:
enable_line = top_module.addWire("\\enable")
enable_line.port_input = True
top_module.fixup_ports()
for cell in top_module.cells_.values():
if cell.type != "$_DFF_P_":
continue
cell.type = "$_DFFE_PP_"
cell.setPort("\\E", ys.SigSpec(enable_line))
ys.log_pop()
p = AllEnablePass() # register the pass
# using the pass
design = ys.Design()
ys.run_pass("read_verilog tests/simple/fiedler-cooley.v", design)
ys.run_pass("hierarchy -check -auto-top", design)
ys.run_pass("synth", design)
ys.run_pass("all_enable", design)
ys.run_pass("write_verilog out.v", design)
ys.run_pass("synth_ice40 -json out.json", design)

View file

@ -0,0 +1,51 @@
from pyosys import libyosys as ys
# loading design
design = ys.Design()
ys.run_pass("read_verilog tests/simple/fiedler-cooley.v", design)
ys.run_pass("hierarchy -check -auto-top", design)
# top module inspection
top_module = design.top_module()
for id, wire in top_module.wires_.items():
if not wire.port_input and not wire.port_output:
continue
description = "input" if wire.port_input else "output"
description += " " + wire.name.str()
if wire.width != 1:
frm = wire.start_offset
to = wire.start_offset + wire.width
if wire.upto:
to, frm = frm, to
description += f" [{to}:{frm}]"
print(description)
# synth
ys.run_pass("synth", design)
# adding the enable line
enable_line = top_module.addWire("\\enable")
enable_line.port_input = True
top_module.fixup_ports()
# hooking the enable line to the internal dff cells
for cell in top_module.cells_.values():
if cell.type != "$_DFF_P_":
continue
cell.type = "$_DFFE_PP_"
cell.setPort("\\E", ys.SigSpec(enable_line))
# run check
top_module.check()
ys.run_pass("stat", design)
# write outputs
ys.run_pass("write_verilog out.v", design)
ys.run_pass("synth_ice40 -json out.json", design)

View file

@ -17,3 +17,4 @@ ways Yosys can interact with designs for a deeper investigation.
more_scripting/index
bugpoint
verilog
pyosys

View file

@ -0,0 +1,208 @@
Scripting with Pyosys
=====================
Pyosys is a limited subset of the Yosys C++ API (aka "libyosys") made available
using the Python programming language.
Like ``.ys`` and ``.tcl`` scripts, Pyosys provides an interface to write Yosys
scripts in the Python programming language, giving you the benefits of
a type system, control flow, object-oriented programming, and more; especially
that the other options lack a type system and control flow/OOP in Tcl is
limited.
Though unlike these two, Pyosys goes a bit further, allowing you to use the
Yosys API to implement advanced functionality that would otherwise require
custom passes written in C++.
Getting Pyosys
--------------
Pyosys supports CPython 3.8 or higher. You can access Pyosys using one of two
methods:
1. Compiling Yosys with the Makefile flag ``ENABLE_PYOSYS=1``
This adds the flag ``-y`` to the Yosys binary, which allows you to execute
Python scripts using an interpreter embedded in Yosys itself:
``yosys -y ./my_pyosys_script.py``
2. Installing the Pyosys wheels
On macOS and GNU/Linux you can install pre-built wheels of Yosys using
``pip``:
``python3 -m pip install pyosys``
Which then allows you to run your scripts as follows:
``python3 ./my_pyosys_script.py``
Scripting and Database Inspection
---------------------------------
To start with, you have to import libyosys as follows:
.. code-block:: python
from pyosys import libyosys
As a reminder, Python allows you to alias imported modules and objects, so
this import may be preferable for terseness:
.. code-block:: python
from pyosys import libyosys as ys
Now, scripting is actually quite similar to ``.ys`` and ``.tcl`` script in that
you can provide mostly text commands. Albeit, you can construct your scripts
to use Python's amenities like conditional execution, loops, and functions:
.. code-block:: python
do_flatten = True
ys.run_pass("read_verilog tests/simple/fiedler-cooley.v")
ys.run_pass("hierarchy -check -auto-top")
if do_flatten:
ys.run_pass("flatten")
…but this does not strictly provide anything that Tcl scripts do not provide you
with. The real power of using Pyosys comes from the fact you can manually
instantiate, manage, and interact with the design database.
As an example, here is the same script with a manually instantiated design.
.. literalinclude:: /code_examples/pyosys/simple_database.py
:start-after: loading design
:end-before: top module inspection
:language: python
What's new here is that you can manually inspect the design's database. This
gives you access to a huge chunk of the design database API as declared in
the ``kernel/rtlil.h`` header.
For example, here's how to list the input and output ports of the top module
of your design:
.. literalinclude:: /code_examples/pyosys/simple_database.py
:start-after: top module inspection
:end-before: # synth
:language: python
.. tip::
C++ data structures in Yosys are bridged to Python such that they have a
pretty similar API to Python objects, for example:
- ``std::vector`` supports the same methods as
`iterables <https://docs.python.org/3/glossary.html#term-iterable>`_ in
Python.
- ``std::set`` and hashlib ``pool`` support the same methods as ``set``\s in
Python. While ``set`` is ordered, ``pool`` is not and modifications may
cause a complete reordering of the set.
- ``dict`` supports the same methods as ``dict``\s in Python, albeit it is
unordered, and modifications may cause a complete reordering of the
dictionary.
- ``idict`` uses a custom set of methods because it doesn't map very cleanly
to an existing Python data structure. See ``pyosys/hashlib.h`` for more
info.
For most operations, the Python equivalents are also supported as arguments
where they will automatically be cast to the right type, so you do not have
to manually instantiate the right underlying C++ object(s) yourself.
Modifying the Database
----------------------
.. warning::
Any modifications to the database may invalidate previous references held
by Python, just as if you were writing C++. Pyosys does not currently attempt
to keep deleted objects alive if a reference is held by Python.
You are not restricted to inspecting the database either: you have the ability
to modify it, and introduce new elements and/or changes to your design.
As a demonstrative example, let's assume we want to add an enable line to all
flip-flops in our fiedler-cooley design.
First of all, we will run :yoscrypt:`synth` to convert all of the logic to
Yosys's internal cell structure (see :ref:`sec:celllib_gates`):
.. literalinclude:: /code_examples/pyosys/simple_database.py
:start-after: # synth
:end-before: adding the enable line
:language: python
Next, we need to add the new port. The method for this is ``Module::addWire``\.
.. tip::
IdString is Yosys's internal representation of strings used as identifiers
within Verilog designs. They are efficient as only integers are stored and
passed around, but they can be translated to and from normal strings at will.
Pyosys will automatically cast Python strings to IdStrings for you, but the
rules around IdStrings apply, namely that *broadly*:
- Identifiers for internal cells must start with ``$``\.
- All other identifiers must start with ``\``\.
.. literalinclude:: /code_examples/pyosys/simple_database.py
:start-after: adding the enable line
:end-before: hooking the enable line
:language: python
Notice how we modified the wire then called a method to make Yosys re-process
the ports.
Next, we can iterate over all constituent cells, and if they are of the type
``$_DFF_P_``, we do two things:
1. Change their type to ``$_DFFE_PP_`` to enable hooking up an enable signal.
2. Hooking up the enable signal.
.. literalinclude:: /code_examples/pyosys/simple_database.py
:start-after: hooking the enable line
:end-before: run check
:language: python
To verify that you did everything correctly, it is prudent to call ``.check()``
on the module you're manipulating as follows after you're done with a set of
changes:
.. literalinclude:: /code_examples/pyosys/simple_database.py
:start-after: run check
:end-before: write output
:language: python
And then finally, write your outputs. Here, I choose an intermediate Verilog
file and :yoscrypt:`synth_ice40` to map it to the iCE40 architecture.
.. literalinclude:: /code_examples/pyosys/simple_database.py
:start-after: write output
:language: python
And voilà, you will note that in the intermediate output, all ``always @``
statements should have an ``if (enable)``\.
Encapsulating as Passes
-----------------------
Just like when writing C++, you can encapsulate routines in terms of "passes",
which adds your Pass to a global registry of commands accessible using
``run_pass``\.
.. literalinclude:: /code_examples/pyosys/pass.py
:language: python
In general, abstract classes and virtual methods are not really supported by
Pyosys due to their complexity, but there are two exceptions which are:
- ``Pass`` in ``kernel/register.h``
- ``Monitor`` in ``kernel/rtlil.h``

View file

@ -36,7 +36,7 @@ The main characteristics are:
all compilers, standard libraries and architectures.
In addition to ``dict<K, T>`` and ``pool<T>`` there is also an ``idict<K>`` that
creates a bijective map from ``K`` to the integers. For example:
creates a bijective map from ``K`` to incrementing integers. For example:
::

View file

@ -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
@ -26,7 +29,14 @@ class CellStatsPass(ys.Pass):
plt.xticks(range(len(cell_stats)), list(cell_stats.keys()))
plt.show()
def py_clear_flags(self):
def 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)

View file

@ -37,6 +37,12 @@
# include <tcl.h>
#endif
#ifdef YOSYS_ENABLE_PYTHON
# include <Python.h>
# include <pybind11/pybind11.h>
namespace py = pybind11;
#endif
#include <stdio.h>
#include <string.h>
#include <limits.h>
@ -91,9 +97,10 @@ int main(int argc, char **argv)
log_error_stderr = true;
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());
#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());
#endif
if (argc == 2)
@ -226,10 +233,10 @@ int main(int argc, char **argv)
cxxopts::value<std::string>(),"<tcl_scriptfile>")
("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 <script>",
cxxopts::value<std::string>(), "<script>")
#endif // WITH_PYTHON
#endif // YOSYS_ENABLE_PYTHON
("p,commands", "execute <commands> (to chain commands, separate them with semicolon + whitespace: 'cmd1; cmd2')",
cxxopts::value<std::vector<std::string>>(), "<commands>")
("r,top", "elaborate the specified HDL <top> module",
@ -515,9 +522,10 @@ int main(int argc, char **argv)
#endif
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());
#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());
#endif
log_error_atexit = yosys_atexit;
@ -566,22 +574,19 @@ int main(int argc, char **argv)
log_error("Can't execute TCL script: this version of yosys is not built with TCL support enabled.\n");
#endif
} else if (scriptfile_python) {
#ifdef WITH_PYTHON
PyObject *sys = PyImport_ImportModule("sys");
#ifdef YOSYS_ENABLE_PYTHON
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) {

View file

@ -1083,7 +1083,7 @@ RTLIL::Design::Design()
refcount_modules_ = 0;
push_full_selection();
#ifdef WITH_PYTHON
#ifdef YOSYS_ENABLE_PYTHON
RTLIL::Design::get_all_designs()->insert(std::pair<unsigned int, RTLIL::Design*>(hashidx_, this));
#endif
}
@ -1094,12 +1094,12 @@ RTLIL::Design::~Design()
delete pr.second;
for (auto n : bindings_)
delete n;
#ifdef WITH_PYTHON
#ifdef YOSYS_ENABLE_PYTHON
RTLIL::Design::get_all_designs()->erase(hashidx_);
#endif
}
#ifdef WITH_PYTHON
#ifdef YOSYS_ENABLE_PYTHON
static std::map<unsigned int, RTLIL::Design*> all_designs;
std::map<unsigned int, RTLIL::Design*> *RTLIL::Design::get_all_designs(void)
{
@ -1430,7 +1430,7 @@ RTLIL::Module::Module()
refcount_wires_ = 0;
refcount_cells_ = 0;
#ifdef WITH_PYTHON
#ifdef YOSYS_ENABLE_PYTHON
RTLIL::Module::get_all_modules()->insert(std::pair<unsigned int, RTLIL::Module*>(hashidx_, this));
#endif
}
@ -1447,12 +1447,12 @@ RTLIL::Module::~Module()
delete pr.second;
for (auto binding : bindings_)
delete binding;
#ifdef WITH_PYTHON
#ifdef YOSYS_ENABLE_PYTHON
RTLIL::Module::get_all_modules()->erase(hashidx_);
#endif
}
#ifdef WITH_PYTHON
#ifdef YOSYS_ENABLE_PYTHON
static std::map<unsigned int, RTLIL::Module*> all_modules;
std::map<unsigned int, RTLIL::Module*> *RTLIL::Module::get_all_modules(void)
{
@ -4109,19 +4109,19 @@ RTLIL::Wire::Wire()
upto = false;
is_signed = false;
#ifdef WITH_PYTHON
#ifdef YOSYS_ENABLE_PYTHON
RTLIL::Wire::get_all_wires()->insert(std::pair<unsigned int, RTLIL::Wire*>(hashidx_, this));
#endif
}
RTLIL::Wire::~Wire()
{
#ifdef WITH_PYTHON
#ifdef YOSYS_ENABLE_PYTHON
RTLIL::Wire::get_all_wires()->erase(hashidx_);
#endif
}
#ifdef WITH_PYTHON
#ifdef YOSYS_ENABLE_PYTHON
static std::map<unsigned int, RTLIL::Wire*> all_wires;
std::map<unsigned int, RTLIL::Wire*> *RTLIL::Wire::get_all_wires(void)
{
@ -4138,7 +4138,7 @@ RTLIL::Memory::Memory()
width = 1;
start_offset = 0;
size = 0;
#ifdef WITH_PYTHON
#ifdef YOSYS_ENABLE_PYTHON
RTLIL::Memory::get_all_memorys()->insert(std::pair<unsigned int, RTLIL::Memory*>(hashidx_, this));
#endif
}
@ -4159,19 +4159,19 @@ RTLIL::Cell::Cell() : module(nullptr)
// log("#memtrace# %p\n", this);
memhasher();
#ifdef WITH_PYTHON
#ifdef YOSYS_ENABLE_PYTHON
RTLIL::Cell::get_all_cells()->insert(std::pair<unsigned int, RTLIL::Cell*>(hashidx_, this));
#endif
}
RTLIL::Cell::~Cell()
{
#ifdef WITH_PYTHON
#ifdef YOSYS_ENABLE_PYTHON
RTLIL::Cell::get_all_cells()->erase(hashidx_);
#endif
}
#ifdef WITH_PYTHON
#ifdef YOSYS_ENABLE_PYTHON
static std::map<unsigned int, RTLIL::Cell*> all_cells;
std::map<unsigned int, RTLIL::Cell*> *RTLIL::Cell::get_all_cells(void)
{
@ -5957,7 +5957,7 @@ RTLIL::Process *RTLIL::Process::clone() const
return new_proc;
}
#ifdef WITH_PYTHON
#ifdef YOSYS_ENABLE_PYTHON
RTLIL::Memory::~Memory()
{
RTLIL::Memory::get_all_memorys()->erase(hashidx_);

View file

@ -1680,7 +1680,7 @@ struct RTLIL::Design
// returns all selected unboxed whole modules, warning the user if any
// partially selected or boxed modules have been ignored
std::vector<RTLIL::Module*> selected_unboxed_whole_modules_warn() const { return selected_modules(SELECT_WHOLE_WARN, SB_UNBOXED_WARN); }
#ifdef WITH_PYTHON
#ifdef YOSYS_ENABLE_PYTHON
static std::map<unsigned int, RTLIL::Design*> *get_all_designs(void);
#endif
};
@ -2040,7 +2040,7 @@ public:
RTLIL::SigSpec OriginalTag (RTLIL::IdString name, const std::string &tag, const RTLIL::SigSpec &sig_a, const std::string &src = "");
RTLIL::SigSpec FutureFF (RTLIL::IdString name, const RTLIL::SigSpec &sig_e, const std::string &src = "");
#ifdef WITH_PYTHON
#ifdef YOSYS_ENABLE_PYTHON
static std::map<unsigned int, RTLIL::Module*> *get_all_modules(void);
#endif
};
@ -2093,7 +2093,7 @@ public:
return zero_index + start_offset;
}
#ifdef WITH_PYTHON
#ifdef YOSYS_ENABLE_PYTHON
static std::map<unsigned int, RTLIL::Wire*> *get_all_wires(void);
#endif
};
@ -2110,7 +2110,7 @@ struct RTLIL::Memory : public RTLIL::NamedObject
Memory();
int width, start_offset, size;
#ifdef WITH_PYTHON
#ifdef YOSYS_ENABLE_PYTHON
~Memory();
static std::map<unsigned int, RTLIL::Memory*> *get_all_memorys(void);
#endif
@ -2168,7 +2168,7 @@ public:
template<typename T> void rewrite_sigspecs(T &functor);
template<typename T> void rewrite_sigspecs2(T &functor);
#ifdef WITH_PYTHON
#ifdef YOSYS_ENABLE_PYTHON
static std::map<unsigned int, RTLIL::Cell*> *get_all_cells(void);
#endif

View file

@ -37,6 +37,12 @@
# include <dlfcn.h>
#endif
#ifdef YOSYS_ENABLE_PYTHON
# include <Python.h>
# include <pybind11/pybind11.h>
namespace py = pybind11;
#endif
#if defined(_WIN32)
# include <windows.h>
# include <io.h>
@ -63,14 +69,9 @@
# include <sys/sysctl.h>
#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
#ifdef YOSYS_ENABLE_PYTHON
extern "C" PyObject* PyInit_libyosys();
extern "C" PyObject* PyInit_pyosys();
#include <signal.h>
#endif
@ -189,6 +190,25 @@ int run_command(const std::string &command, std::function<void(const std::string
bool already_setup = false;
bool already_shutdown = false;
#ifdef YOSYS_ENABLE_PYTHON
// 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 function to be interacted with at all.
PYBIND11_MODULE(pyosys, m) {
m.add_object("__path__", py::list());
}
// Catch uses of 'import libyosys' which can import libyosys.so, causing a ton
// of symbol collisions and overall weird behavior.
//
// This should not affect using wheels as the dylib has to actually be called
// libyosys_dummy.so for this function to be interacted with at all.
PYBIND11_MODULE(libyosys_dummy, _) {
throw py::import_error("Change your import from 'import libyosys' to 'from pyosys import libyosys'.");
}
#endif
void yosys_setup()
{
if(already_setup)
@ -198,12 +218,16 @@ void yosys_setup()
IdString::ensure_prepopulated();
#ifdef WITH_PYTHON
// With Python 3.12, calling PyImport_AppendInittab on an already
#ifdef YOSYS_ENABLE_PYTHON
// Starting Python 3.12, calling PyImport_AppendInittab on an already
// initialized platform fails (such as when libyosys is imported
// from a Python interpreter)
if (!Py_IsInitialized()) {
PyImport_AppendInittab((char*)"libyosys", INIT_MODULE);
PyImport_AppendInittab((char*)"pyosys.libyosys", PyInit_libyosys);
// compatibility with wheels
PyImport_AppendInittab((char*)"pyosys", PyInit_pyosys);
// prevent catastrophes
PyImport_AppendInittab((char*)"libyosys", PyInit_libyosys_dummy);
Py_Initialize();
PyRun_SimpleString("import sys");
signal(SIGINT, SIG_DFL);
@ -260,13 +284,13 @@ void yosys_shutdown()
dlclose(it.second);
loaded_plugins.clear();
#ifdef WITH_PYTHON
#ifdef YOSYS_ENABLE_PYTHON
loaded_python_plugins.clear();
#endif
loaded_plugin_aliases.clear();
#endif
#ifdef WITH_PYTHON
#ifdef YOSYS_ENABLE_PYTHON
Py_Finalize();
#endif
}
@ -542,7 +566,7 @@ void init_share_dirname()
#else
void init_share_dirname()
{
# ifdef WITH_PYTHON
# ifdef YOSYS_ENABLE_PYTHON
PyObject *sys_obj = PyImport_ImportModule("sys");
if (PyObject_HasAttrString(sys_obj, "_pyosys_share_dirname")) {
@ -602,7 +626,7 @@ void init_abc_executable_name()
yosys_abc_executable = proc_self_dirname() + "..\\" + proc_program_prefix() + "yosys-abc";
# endif
# ifdef WITH_PYTHON
# ifdef YOSYS_ENABLE_PYTHON
PyObject *sys_obj = PyImport_ImportModule("sys");
if (PyObject_HasAttrString(sys_obj, "_pyosys_abc")) {

View file

@ -51,7 +51,7 @@ YOSYS_NAMESPACE_BEGIN
void yosys_setup();
#ifdef WITH_PYTHON
#ifdef YOSYS_ENABLE_PYTHON
bool yosys_already_setup();
#endif
@ -89,7 +89,7 @@ extern std::vector<RTLIL::Design*> pushed_designs;
// from passes/cmds/pluginc.cc
extern std::map<std::string, void*> loaded_plugins;
#ifdef WITH_PYTHON
#ifdef YOSYS_ENABLE_PYTHON
extern std::map<std::string, void*> loaded_python_plugins;
#endif
extern std::map<std::string, std::string> loaded_plugin_aliases;

View file

@ -53,10 +53,6 @@
#include <sys/stat.h>
#include <errno.h>
#ifdef WITH_PYTHON
#include <Python.h>
#endif
#ifndef _YOSYS_
# error It looks like you are trying to build Yosys without the config defines set. \
When building Yosys with a custom make system, make sure you set all the \

File diff suppressed because it is too large Load diff

View file

@ -24,16 +24,16 @@
# include <dlfcn.h>
#endif
#ifdef WITH_PYTHON
# include <boost/algorithm/string/predicate.hpp>
#ifdef YOSYS_ENABLE_PYTHON
# include <Python.h>
# include <boost/filesystem.hpp>
# include <pybind11/pybind11.h>
namespace py = pybind11;
#endif
YOSYS_NAMESPACE_BEGIN
std::map<std::string, void*> loaded_plugins;
#ifdef WITH_PYTHON
#ifdef YOSYS_ENABLE_PYTHON
std::map<std::string, void*> loaded_python_plugins;
#endif
std::map<std::string, std::string> loaded_plugin_aliases;
@ -49,7 +49,7 @@ void load_plugin(std::string filename, std::vector<std::string> aliases)
filename = "./" + filename;
#ifdef WITH_PYTHON
#ifdef YOSYS_ENABLE_PYTHON
const bool is_loaded = loaded_plugins.count(orig_filename) && loaded_python_plugins.count(orig_filename);
#else
const bool is_loaded = loaded_plugins.count(orig_filename);
@ -57,23 +57,23 @@ void load_plugin(std::string filename, std::vector<std::string> aliases)
if (!is_loaded) {
// Check if we're loading a python script
if(filename.find(".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());
if (filename.rfind(".py") != std::string::npos) {
#ifdef YOSYS_ENABLE_PYTHON
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<std::string>(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(
@ -177,7 +177,7 @@ struct PluginPass : public Pass {
if (list_mode)
{
log("\n");
#ifdef WITH_PYTHON
#ifdef YOSYS_ENABLE_PYTHON
if (loaded_plugins.empty() and loaded_python_plugins.empty())
#else
if (loaded_plugins.empty())
@ -189,7 +189,7 @@ struct PluginPass : public Pass {
for (auto &it : loaded_plugins)
log(" %s\n", it.first);
#ifdef WITH_PYTHON
#ifdef YOSYS_ENABLE_PYTHON
for (auto &it : loaded_python_plugins)
log(" %s\n", it.first);
#endif

2
pyosys/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
wrappers.cc
wrappers.inc.cc

788
pyosys/generator.py Normal file
View file

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

529
pyosys/hashlib.h Normal file
View file

@ -0,0 +1,529 @@
// -------------------------------------------------------
// Written by Mohamed Gaber in 2025 <me@donn.website>
// Based on kernel/hashlib.h by Claire Xenia Wolf <claire@yosyshq.com>
// -------------------------------------------------------
// 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 <https://unlicense.org/>
// -------------------------------------------------------
//
// 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.13/library/stdtypes.html
//
#include <optional> // optional maps cleanest to methods that accept None in Python
#include <pybind11/stl.h> // std::optional
#include <pybind11/pybind11.h> // base
#include <pybind11/operators.h> // easier operator binding
#include <pybind11/stl_bind.h> // vector
#include "kernel/hashlib.h"
namespace pybind11 {
namespace hashlib {
// "traits"
template <typename T> struct is_pointer: std::false_type {};
template <typename T> struct is_pointer<T*>: std::true_type {};
template <typename T> struct is_optional: std::false_type {};
template <typename T> struct is_optional< std::optional<T> >: std::true_type {};
bool is_mapping(object obj) {
object mapping = module_::import("collections.abc").attr("Mapping");
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 <typename C, typename T>
void unionize(C &lhs, const iterable &rhs) {
for (auto &element: rhs) {
lhs.insert(cast<T>(element));
}
}
template <typename C, typename T>
void difference(C &lhs, const iterable &rhs) {
for (auto &element: rhs) {
auto element_cxx = cast<T>(element);
if (lhs.count(element_cxx)) {
lhs.erase(element_cxx);
}
}
}
template <typename C, typename T>
void intersect(C &lhs, const iterable &rhs) {
// Doing it in-place is a lot slower
// TODO?: Leave modifying lhs to caller (saves a copy in some cases)
// 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 <typename C, typename T>
void symmetric_difference(C &lhs, const iterable &rhs) {
C storage(lhs);
for (auto &element: rhs) {
auto element_cxx = cast<T>(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 <typename C, typename V>
void bind_vector(module &m, const char *name_cstr) {
pybind11::bind_vector<C>(m, name_cstr);
}
// also used for hashlib pool because the semantics are close enough
template <typename C, typename T>
void bind_set(module &m, const char *name_cstr) {
class_<C>(m, name_cstr)
.def(init<>())
.def(init<const C &>()) // copy constructor
.def(init([](const iterable &other){ // copy instructor from arbitrary iterables
auto s = new C();
unionize<C, T>(*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)));
})
.def("disjoint", [](const C &s, const iterable &other) {
for (const auto &element: other) {
if (s.count(cast<T>(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<iterable>(arg);
unionize<C, T>(*result, arg_iterable);
}
return result;
})
.def("__or__", [](const C &s, const iterable &other) {
auto result = new C(s);
unionize<C, T>(*result, other);
return result;
})
.def("__ior__", [](C &s, const iterable &other) {
unionize<C, T>(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<iterable>(arg);
intersect<C, T>(*result, arg_iterable);
}
return result;
})
.def("__and__", [](const C &s, const iterable &other) {
auto result = new C(s);
intersect<C, T>(*result, other);
return result;
})
.def("__iand__", [](C &s, const iterable &other) {
intersect<C, T>(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<iterable>(arg);
difference<C, T>(*result, arg_iterable);
}
return result;
})
.def("__sub__", [](const C &s, const iterable &other) {
auto result = new C(s);
difference<C, T>(*result, other);
return result;
})
.def("__isub__", [](C &s, const iterable &other) {
difference<C, T>(s, other);
return s;
})
.def("symmetric_difference", [](const C &s, const iterable &other) {
auto result = new C(s);
symmetric_difference<C, T>(*result, other);
return result;
})
.def("__xor__", [](const C &s, const iterable &other) {
auto result = new C(s);
symmetric_difference<C, T>(*result, other);
return result;
})
.def("__ixor__", [](C &s, const iterable &other) {
symmetric_difference<C, T>(s, other);
return s;
})
.def("copy", [](const C &s) {
return new C(s);
})
.def("update", [](C &s, iterable iterable) {
for (auto item: iterable) {
s.insert(item.cast<T>());
}
})
.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("__eq__", [](const C &s, const C &other) { return s == other; })
.def("__eq__", [](const C &s, const iterable &other) {
C other_cast;
unionize<C, T>(other_cast, other);
return s == other_cast;
})
.def("__repr__", [name_cstr](const 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 <typename C, typename T>
void bind_pool(module &m, const char *name_cstr) {
bind_set<C, T>(m, name_cstr);
}
template <typename C, typename K, typename V>
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<K>(key)] = cast<V>(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<K>(pair[cast(0)])] = cast<V>(pair[cast(1)]);
}
}
}
template <typename C, typename K, typename V>
void bind_dict(module &m, const char *name_cstr) {
auto cls = class_<C>(m, name_cstr)
.def(init<>())
.def(init<const C &>()) // copy constructor
.def(init([](const iterable &other){ // copy instructor from arbitrary iterables and mappings
auto s = new C();
update_dict<C, K, V>(*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; })
.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<const V> &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<const V> &default_) {
if (default_.has_value()) {
return s.at(k, *default_);
} else {
return s.at(k);
}
}, arg("key"), arg("default") = std::nullopt)
.def("popitem", [](C &s) {
auto it = s.begin();
if (it == s.end()) {
throw key_error("dict is empty");
}
auto copy = *it;
s.erase(it);
return copy;
})
.def("setdefault", [name_cstr](C &s, const K& k, std::optional<const V> &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<V>::value) {
s[k] = nullptr;
return (V)nullptr;
}
if constexpr (is_optional<V>::value) {
s[k] = std::nullopt;
return std::nullopt;
}
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<C, K, V>(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<C, K, V>(*result, iterable_or_mapping);
return result;
})
.def("__ior__", [](C &s, iterable iterable_or_mapping) {
update_dict<C, K, V>(s, iterable_or_mapping);
return s;
})
.def("__bool__", [](const C &s) { return s.size() != 0; })
.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;
});
// K is always comparable
// Python implements `is` as a fallback to check if it's the same object
if constexpr (detail::is_comparable<V>::value) {
cls.def("__eq__", [](const C &s, const C &other) { return s == other; });
cls.def("__eq__", [](const C &s, const iterable &other) {
C other_cast;
update_dict<C, K, V>(other_cast, other);
return s == other_cast;
});
}
// 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
//
// it's cleanest, despite the inconsistency with __getitem__, to just think of
// the hashable as key and the integer as value
template <typename C, typename K>
void bind_idict(module &m, const char *name_cstr) {
auto cls = class_<C>(m, name_cstr)
.def(init<>())
.def(init<const C &>()) // copy constructor
.def(init([](const iterable &other){ // copy instructor from arbitrary iterables
auto s = new C();
for (auto &e: other) {
(*s)(cast<K>(e));
}
return s;
}))
.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<int> &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 other) {
for (auto &e: other) {
s(cast<K>(e));
}
})
.def("__or__", [](const C &s, iterable other) {
auto result = new C(s);
for (auto &e: other) {
(*result)(cast<K>(e));
}
return result;
})
.def("__ior__", [](C &s, iterable other) {
for (auto &e: other) {
s(cast<K>(e));
}
return s;
})
.def("__bool__", [](const C &s) { return s.size() != 0; })
.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 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"}) {
cls.def(mutator, [](args _) {
throw type_error("idicts do not support arbitrary element mutation");
});
}
}
}; // namespace hashlib
}; // namespace pybind11

257
pyosys/wrappers_tpl.cc Normal file
View file

@ -0,0 +1,257 @@
/*
* yosys -- Yosys Open SYnthesis Suite
*
* Copyright (C) 2012 Claire Xenia Wolf <claire@yosyshq.com>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#ifdef YOSYS_ENABLE_PYTHON
// <!-- generated includes -->
#include <pybind11/pybind11.h>
#include <pybind11/native_enum.h>
#include "pyosys/hashlib.h"
namespace py = pybind11;
USING_YOSYS_NAMESPACE
using std::set;
using std::regex;
using std::ostream;
using namespace RTLIL;
#include "wrappers.inc.cc"
namespace pyosys {
struct Globals {};
// 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<std::string> 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<RTLIL::SigSig> &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
);
}
};
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();
}));
}
// 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_<Globals>(m, "Globals");
// Trampoline Classes
py::class_<Pass, pyosys::PassTrampoline, std::unique_ptr<Pass, py::nodelete>>(m, "Pass")
.def(py::init([](std::string name, std::string short_help) {
auto created = new pyosys::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<RTLIL::Design *,std::string>(&Pass::call))
.def("call", py::overload_cast<RTLIL::Design *,std::vector<std::string>>(&Pass::call))
;
py::class_<RTLIL::Monitor, pyosys::MonitorTrampoline>(m, "Monitor")
.def(py::init([]() {
return new pyosys::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::SigSig> &
>(&RTLIL::Monitor::notify_connect)
)
.def("notify_blackout", &RTLIL::Monitor::notify_blackout)
;
// Bind Opaque Containers
bind_autogenerated_opaque_containers(m);
// <!-- generated pymod-level code -->
py::implicitly_convertible<std::string, RTLIL::IdString>();
py::implicitly_convertible<const char *, RTLIL::IdString>();
};
};
#endif // YOSYS_ENABLE_PYTHON

11
pyproject.toml Normal file
View file

@ -0,0 +1,11 @@
[build-system]
requires = [
"setuptools>=42",
"pybind11>=3,<4",
"cxxheaderparser",
]
build-backend = "setuptools.build_meta"
[tool.ruff]
target-version = "py38"
lint.ignore = ["F541"]

View file

@ -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={

46
tests/pyosys/run_tests.py Normal file
View file

@ -0,0 +1,46 @@
import sys
import shutil
import shlex
import subprocess
from pathlib import Path
__file_dir__ = Path(__file__).absolute().parent
if len(sys.argv) > 2 or len(sys.argv) == 2 and sys.argv[1] != 'yosys':
print(f"Usage: {sys.argv[0]} [yosys]")
exit(64)
if len(sys.argv) == 2:
binary = [str(__file_dir__.parents[1] / "yosys"), "-Qy"]
else:
binary = [sys.executable or shutil.which("python3") or "python3"] # sys.executable can actually be None
tests = __file_dir__.glob("test_*.py")
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")
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}")
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)

BIN
tests/pyosys/spm.cut.v.gz Normal file

Binary file not shown.

View file

@ -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!")

44
tests/pyosys/test_dict.py Normal file
View file

@ -0,0 +1,44 @@
from typing import Mapping
from pyosys import libyosys as ys
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"})
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"}
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 # compare dicts
assert repr_test == {'tomato': 'tomato', 'first': 'second', 'key': 'value', 'im running': 'out of string ideas', } # compare dict with mapping
before = len(repr_test)
print(repr_test.popitem())
assert before - 1 == len(repr_test)
# test noncomparable
## if ys.CellType ever gets an == operator just disable this section
uncomparable_value = ys.Globals.yosys_celltypes.cell_types[ys.IdString("$not")]
x = ys.IdstringToCelltypeDict({ ys.IdString("\\a"): uncomparable_value})
y = ys.IdstringToCelltypeDict({ ys.IdString("\\a"): uncomparable_value})
assert x != y # not comparable

View file

@ -0,0 +1,31 @@
from pyosys import libyosys as ys
my_idict = ys.IdstringIdict()
print(my_idict(ys.IdString("\\hello"))) # test explicit IdString construction
print(my_idict("\\world"))
print(my_idict.get("\\world"))
try:
print(my_idict.get("\\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("\\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 |= ("\\the", "\\world") # 1 new element
assert len(my_copy) == current_copy_len + 1, "or operator returned unexpected result"

View file

@ -0,0 +1,20 @@
import os
import sys
from pyosys import libyosys as ys
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)

View file

@ -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")

View file

@ -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"]

34
tests/pyosys/test_pass.py Normal file
View file

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

View file

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

42
tests/pyosys/test_set.py Normal file
View file

@ -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"} # compare against iterable
repr_test = eval(repr(c_symdiff_d))
assert c_symdiff_d == repr_test # compare against self
print("Done.")