From 563dc59390cd93616a7d6f5f51339c044aea2a4d Mon Sep 17 00:00:00 2001 From: Sebastian Abrahamsson Date: Wed, 14 Oct 2026 10:37:55 +0200 Subject: [PATCH 1/7] Publish the documentation of each release on Read the Docs A release tag vX.Y.Z makes Read the Docs sync its versions, activates the version of the tag and builds it and stable. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017otfceeeQ9FrS2sXbyLou2 --- .github/workflows/docs.yml | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index d7665f7..1c87f75 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,8 +1,8 @@ name: 'docs' # Builds the documentation with warnings as errors, as Read the Docs does, and keeps the HTML -# as an artifact. A push to main triggers the Read the Docs build of latest, which publishes -# it, see .readthedocs.yaml. +# as an artifact. Read the Docs publishes it, see .readthedocs.yaml: a push to main builds +# latest, and a release tag vX.Y.Z gets a version of its own and updates stable. on: push: @@ -37,14 +37,31 @@ jobs: publish: needs: docs - if: github.event_name == 'push' && github.ref == 'refs/heads/main' && github.repository == 'VUnit/vunit-python-bridge' + if: >- + github.event_name == 'push' && github.repository == 'VUnit/vunit-python-bridge' && + (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) runs-on: ubuntu-24.04 name: '๐Ÿš€ Read the Docs' + env: + API: https://app.readthedocs.org/api/v3/projects/vunit-python-bridge + AUTH: 'Authorization: Token ${{ secrets.READTHEDOCS_TOKEN }}' steps: - - name: '๐Ÿš€ Trigger the build of latest' - env: - READTHEDOCS_TOKEN: ${{ secrets.READTHEDOCS_TOKEN }} + - name: '๐Ÿš€ Build latest' + if: github.ref == 'refs/heads/main' + run: curl --fail-with-body -X POST -H "$AUTH" "$API/versions/latest/builds/" + + # Read the Docs learns about the tag in the background, so wait for its version + - name: '๐Ÿš€ Build the release and stable' + if: startsWith(github.ref, 'refs/tags/v') run: | - curl --fail-with-body -X POST -H "Authorization: Token $READTHEDOCS_TOKEN" \ - https://readthedocs.org/api/v3/projects/vunit-python-bridge/versions/latest/builds/ + curl --fail-with-body -X POST -H "$AUTH" "$API/sync-versions/" + for attempt in $(seq 30); do + curl --fail -s -o /dev/null -H "$AUTH" "$API/versions/$GITHUB_REF_NAME/" && break + [ "$attempt" = 30 ] && { echo "::error::Read the Docs has no version $GITHUB_REF_NAME"; exit 1; } + sleep 10 + done + curl --fail-with-body -X PATCH -H "$AUTH" -H 'Content-Type: application/json' \ + -d '{"active": true, "hidden": false}' "$API/versions/$GITHUB_REF_NAME/" + curl --fail-with-body -X POST -H "$AUTH" "$API/versions/$GITHUB_REF_NAME/builds/" + curl --fail-with-body -X POST -H "$AUTH" "$API/versions/stable/builds/" From 83028b74a4f767e4fdbbb37494310c2ef1911954 Mon Sep 17 00:00:00 2001 From: Sebastian Abrahamsson Date: Wed, 14 Oct 2026 10:56:49 +0200 Subject: [PATCH 2/7] Restyle the documentation with a page per topic The Read the Docs theme with a sidebar grouped into the user guide, simulators and release notes, a landing page with badges, key features and a quick start, and the user guide split into a page per topic. The release notes page is generated from release_notes/*.md. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017otfceeeQ9FrS2sXbyLou2 --- .gitignore | 1 + README.md | 2 +- docs/conf.py | 40 +- docs/errors.rst | 74 +++ docs/file_paths.rst | 214 ++++++++ docs/how_it_works.rst | 114 ++++ docs/index.rst | 128 ++++- docs/other_simulators.rst | 42 ++ docs/python_files.rst | 81 +++ docs/requirements.rst | 34 ++ docs/requirements.txt | 4 +- docs/running_code.rst | 264 +++++++++ docs/sessions.rst | 161 ++++++ docs/types.rst | 64 +++ docs/user_guide.rst | 1087 ------------------------------------- 15 files changed, 1213 insertions(+), 1097 deletions(-) create mode 100644 docs/errors.rst create mode 100644 docs/file_paths.rst create mode 100644 docs/how_it_works.rst create mode 100644 docs/other_simulators.rst create mode 100644 docs/python_files.rst create mode 100644 docs/requirements.rst create mode 100644 docs/running_code.rst create mode 100644 docs/sessions.rst create mode 100644 docs/types.rst delete mode 100644 docs/user_guide.rst diff --git a/.gitignore b/.gitignore index 934b977..619745e 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ __pycache__/ /docs/_build/ /src/vunit_python_bridge/bin/ vunit_out/ +/docs/release_notes.rst diff --git a/README.md b/README.md index bcecb90..f4d42f0 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ environment and its installed packages. ## Documentation -The user guide is in [docs/user_guide.rst](docs/user_guide.rst): sessions, `exec`, `eval`, `call` +The documentation is at [vunit-python-bridge.readthedocs.io](https://vunit-python-bridge.readthedocs.io): sessions, `exec`, `eval`, `call` and its argument forms, `exec_file`, `import_run_script`, the type mapping, `integer_array_t` and NumPy, error reporting, and how the bridge works. A complete example covering all three simulator families is in [examples/embedded_python](examples/embedded_python). diff --git a/docs/conf.py b/docs/conf.py index c88aa1d..7447e8d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -10,11 +10,45 @@ Build with: sphinx-build -b html docs docs/_build """ +import sys +import tomllib +from pathlib import Path + +from packaging.version import Version + +ROOT = Path(__file__).parent.parent +sys.path.insert(0, str(ROOT / "docs")) + project = "vunit-python-bridge" copyright = "2014-2026, Lars Asplund" # pylint: disable=redefined-builtin author = "Lars Asplund" -release = "0.1.0" +with (ROOT / "pyproject.toml").open("rb") as fptr: + release = tomllib.load(fptr)["project"]["version"] -extensions = [] +extensions = ["myst_parser"] +# The release notes start at ## since they are included under a version heading +suppress_warnings = ["myst.header"] exclude_patterns = ["_build"] -html_theme = "alabaster" + +html_theme = "sphinx_rtd_theme" +html_theme_options = { + "prev_next_buttons_location": "both", + "navigation_depth": 3, +} +# The "Edit on GitHub" link of every page +html_context = { + "display_github": True, + "github_user": "VUnit", + "github_repo": "vunit-python-bridge", + "github_version": "main", + "conf_py_path": "/docs/", +} + + +# The release notes page, generated from release_notes/*.md, newest first +_notes = ["Release notes", "=============", ""] +for _path in sorted((ROOT / "release_notes").glob("*.md"), key=lambda path: Version(path.stem), reverse=True): + _title = _path.stem if Version(_path.stem) <= Version(release) else f"{_path.stem} (unreleased)" + _notes += [_title, "-" * len(_title), "", f".. include:: ../release_notes/{_path.name}"] + _notes += [" :parser: myst_parser.sphinx_", ""] +(ROOT / "docs" / "release_notes.rst").write_text("\n".join(_notes)) diff --git a/docs/errors.rst b/docs/errors.rst new file mode 100644 index 0000000..bc5e233 --- /dev/null +++ b/docs/errors.rst @@ -0,0 +1,74 @@ +Errors and semantics +==================== + +Errors +------ + +Python exceptions, syntax errors, type errors and undefined names are +reported as failures on the logger of the :ref:`session +` the operation was performed in, +``get_logger(get_id(session))``, including the Python traceback. For the +default session that logger is named ``python_bridge:python:default`` and +``python_logger`` (named ``python_bridge:python``) is its parent. The test +fails and stops like for any other failure. The logger of the session can be +mocked to test error handling; when mocked, ``eval``/``call`` return a default +value (``0``, ``0.0``, ``""`` or an empty vector, depending on the type) after +the failure. + +.. code-block:: text + + FAILURE - python_bridge:python:default - eval("1 / 0") failed: + Traceback (most recent call last): + File "", line 1, in + 1 / 0 + ~~^~~ + ZeroDivisionError: division by zero + +The default ``verbose`` format of the VUnit display handler indents the lines +of the traceback to the column where the message starts, after the time, the +logger name and the log level. With the long logger name of a session made from +an instance name and the absolute file paths of a traceback, the lines can be +wider than the terminal, which wraps them back to the first column and breaks +the alignment. The ``level`` format leaves out the time and the logger name and +indents the traceback by the width of the log level only: + +.. code-block:: vhdl + + test_runner_setup(runner, runner_cfg); + set_format(display_handler, level); + +.. code-block:: text + + FAILURE - eval("1 / 0") failed: + Traceback (most recent call last): + File "", line 1, in + 1 / 0 + ~~^~~ + ZeroDivisionError: division by zero + +Output of ``print`` is written to the simulator output and flushed after +every operation. + +.. _python_bridge:semantics: + +Semantics to be aware of +------------------------- + +.. important:: + + * ``eval``/``eval_`` are declared ``impure`` since they always read + state from the Python interpreter. + * Errors are reported as failures on the logger of the :ref:`session + `, including the Python traceback, rather than + aborting the simulation. + * ``python_setup`` and ``python_cleanup`` are optional, see + :ref:`python_bridge:setup_and_cleanup`. ``python_cleanup`` does not + finalize the interpreter. + * ``real`` results have no float32 range limit: VHDL ``real`` is double + precision where the Python bridge implements the API, so any finite + Python ``float`` (also double precision) is in range. A value that is not + finite, such as ``float("inf")`` or ``float("nan")``, has no VHDL ``real`` + representation and is reported as a failure. + * ``string`` arguments are passed to Python double-quoted verbatim: a quote + or backslash inside the string is not escaped, so it must be avoided or + already be valid inside a Python double-quoted string. diff --git a/docs/file_paths.rst b/docs/file_paths.rst new file mode 100644 index 0000000..3330d9b --- /dev/null +++ b/docs/file_paths.rst @@ -0,0 +1,214 @@ +.. _python_bridge:file_paths: + +File paths +---------- + +A file name can be resolved against several directories, depending on who +resolves it: ``python_pkg`` in VHDL, or Python code in the model. This section +lists them all, gives recipes for the common cases and describes ``base_path``, +which gives verification components a directory of their own. + +How a file name is resolved +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 40 60 + + * - File name + - Resolved against + * - Relative name given to ``exec_file`` or ``import_module_from_file`` + after ``test_runner_setup`` has completed + - The directory of the testbench file, ``tb_path``. + * - The same before ``test_runner_setup`` has completed, or without a + test runner + - Nothing: it is reported as a failure, which suggests calling it after + ``test_runner_setup``, giving an absolute name or joining it to + ``base_path``. + * - Absolute name given to ``exec_file`` or ``import_module_from_file`` + - Used as it is given. + * - ``join(base_path, "x")``, at any time, time 0 included + - The base directory set by ``set_relative_file_base`` in the run script + or by the environment variable ``VUNIT_PYTHON_BRIDGE_FILE_BASE``, which + ``base_path`` returns. Without a base directory ``base_path`` is + reported as a failure. + * - Relative name opened by Python code: ``open``, ``numpy.load``, + ``pathlib.Path`` and so on + - The current working directory of Python, which is the working + directory of the simulator. See the warning below. + * - Python module imported by name, ``import model`` + - ``sys.path``, which holds the directory of the run script first, like + ``python run.py`` does, the directories of ``PYTHONPATH`` (the + simulator inherits the environment of VUnit, so a ``PYTHONPATH`` set + in the run script before ``vu.main()`` is seen), and, while + ``exec_file`` executes a file, the directory of that file. + +.. warning:: + + The working directory of the simulator differs between simulators. On NVC + and GHDL it is the directory the run script was started from. On + Questa/ModelSim it is the ``modelsim`` directory of the VUnit output path, + for example ``vunit_out/modelsim``. Other simulators may use yet another + directory. Never rely on a relative file name in Python code: build it from + ``Path(__file__).parent`` in the model, or pass an absolute name from VHDL, + built with ``base_path`` or ``tb_path``. + +Recipes +~~~~~~~ + +**A Python file next to the testbench.** Give its name relative to the +testbench file, after ``test_runner_setup``: + +.. code-block:: vhdl + + test_runner_setup(runner, runner_cfg); + exec_file("model.py"); -- /model.py + +**The model of a verification component.** A verification component is not +tied to a testbench and often loads its model at time 0, before +``test_runner_setup``. Give its name relative to a base directory set in the +run script. With this layout: + +.. code-block:: text + + project/ + run.py + vc/ + uart_vc.vhd + models/ + uart_model.py + helpers.py + tb/ + tb_uart.vhd + +the run script sets its own directory as the base directory, which makes the +names independent of the directory the run script is started from: + +.. code-block:: python + + import vunit_python_bridge + + # project/, whatever the working directory + vunit_python_bridge.set_relative_file_base(Path(__file__).parent) + vu.add_package("vunit-python-bridge", allow_setup=True) + +.. code-block:: vhdl + + exec_file(join(base_path, "vc/models/uart_model.py"), session); -- project/vc/models/uart_model.py + import_module_from_file(join(base_path, "vc/models/helpers.py"), "helpers", session); -- project/vc/models/helpers.py + +Alternatively, import the model by module name from a directory on +``sys.path``: the directory of the run script, or one added to ``PYTHONPATH`` +in the run script before ``vu.main()``: + +.. code-block:: python + + # project/vc/models on sys.path of the simulator + os.environ["PYTHONPATH"] = str(Path(__file__).parent / "vc" / "models") + +.. code-block:: vhdl + + exec("import uart_model", session); -- found on sys.path: project/vc/models/uart_model.py + +Or wait for ``test_runner_setup`` to complete and give the name relative to the +testbench file: + +.. code-block:: vhdl + + if get_phase <= test_runner_setup then + wait on runner until get_phase > test_runner_setup; + end if; + exec_file("uart_model.py", session); -- /uart_model.py + +``wait_until(runner, test_runner_setup)`` is not enough, since the phase is +entered before the testbench path is set. + +**Data files used by a model.** Find them relative to the model file in +Python: + +.. code-block:: python + + from pathlib import Path + + # Next to this file, whatever the working directory of the simulator + COEFFICIENTS = np.load(Path(__file__).parent / "coefficients.npy") + +or pass an absolute name from VHDL: + +.. code-block:: vhdl + + -- project/vc/data/coefficients.npy, an absolute name for Python + call("model.load_coefficients", arg(join(base_path, "vc/data/coefficients.npy"))); + +**A run script shared through a git submodule.** When the run script cannot be +changed, set the base directory with the environment variable, for example from +a Makefile at the root of the repository: + +.. code-block:: text + + repo/ + Makefile + sub/ (git submodule) + run.py + vc/ + models/ + vc_model.py + tb/ + tb_vc.vhd + +.. code-block:: make + + # repo/ is the base directory + sim: + VUNIT_PYTHON_BRIDGE_FILE_BASE=$(CURDIR) python sub/run.py + +The file names joined to ``base_path`` are then relative to the repository +root: + +.. code-block:: vhdl + + exec_file(join(base_path, "sub/vc/models/vc_model.py")); -- repo/sub/vc/models/vc_model.py + +base_path and the base directory +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``base_path`` returns the base directory, absolute, for VHDL to join file +names to with ``join`` of VUnit's ``path`` package, which ``vunit_context`` +includes, the same way as ``tb_path(runner_cfg)``. It is a function without +arguments: the base directory is a constant of a package generated when the +package is set up, so it can be used at any time, before ``test_runner_setup`` +too. Without a base directory ``base_path`` is reported as a failure on +``python_logger``, naming both ways of setting it, and returns ``""``. +``base_path`` serves any file name passed to a Python model, not only those of +``exec_file`` and ``import_module_from_file``, whose own relative file names +are still taken from the directory of the testbench. + +``vunit_python_bridge.set_relative_file_base(directory)`` sets the base +directory in the run script. It must be called before +``vu.add_package("vunit-python-bridge", ...)``; a call after it raises a +``RuntimeError``. The directory, a ``str`` or a ``Path``, is resolved when the +function is called, a relative one against the current working directory, and +a directory that does not exist raises a ``ValueError``. + +The environment variable ``VUNIT_PYTHON_BRIDGE_FILE_BASE`` sets the base +directory without changing the run script. It is read when the package is set +up, during ``add_package``. A relative value is resolved against the current +working directory at that time, an empty value counts as unset, and a value +that is not an existing directory makes the setup fail with an error naming +the variable and the value. + +The variable takes precedence, so that whoever runs a shared run script can +override the directory it sets: + +========================== ==================== ========================== +``set_relative_file_base`` Environment variable Base directory +========================== ==================== ========================== +not called unset or empty none, ``base_path`` fails +called unset or empty that of the call +not called set that of the variable +called set that of the variable +========================== ==================== ========================== + +When both are set to different directories, compared after resolving them, +the setup logs a warning naming both and saying that the environment variable +takes precedence. diff --git a/docs/how_it_works.rst b/docs/how_it_works.rst new file mode 100644 index 0000000..1aaa535 --- /dev/null +++ b/docs/how_it_works.rst @@ -0,0 +1,114 @@ +.. _python_bridge:native: + +How it works (NVC, GHDL and Questa) +------------------------------------ + +For NVC, GHDL and Questa, the interpreter is embedded in the simulator process +by a small C library, the VUnit Python bridge (`src/vunit_python_bridge/native +`__). +NVC and GHDL call it through VHPIDIRECT; Questa/ModelSim calls it through the +FLI, using the front end in `native/fli.c +`__ +that converts the FLI parameters of one foreign subprogram per entry point. The generated VHDL +and everything above it is the same for all three. The interpreter is started +on first use, is never restarted within a simulation, and uses no signal +handlers of its own. + +Linux + The bridge is compiled from source against the Python running VUnit the + first time the package is added, and cached in the library directory, see + below. It is rebuilt automatically when the + source, the Python version or the Python installation changes. For Questa + the FLI front end is compiled in too, against the ``mti.h`` of the + simulator, and the simulator installation is part of the cache key. The + package ships no prebuilt Linux library. + +Windows + For NVC and GHDL, the package ships DLLs built with MSVC for each supported + Python minor version (``src/vunit_python_bridge/bin``). The matching DLL is + copied to the library directory, see below, and nothing is compiled. A development checkout of + the package does not contain the DLLs; they can be built with + ``tools/build_python_bridge.py`` from an MSVC developer prompt, and without + them the library is built with gcc like for Questa. For Questa the library + must be linked against the simulator's ``mtipli``, so it is always built on + first use with gcc, against the headers and the import library of the + Python running VUnit. The compiler is ``CC`` if set, else the MinGW gcc + bundled with the simulator (``gcc-*-mingw64*`` next to the Questa + executables' directory), else ``gcc`` on ``PATH``. Many Questa installations + on Windows bundle none. A gcc build imports the Python DLL directly, so the + package adds its directory to ``PATH`` for the simulator processes. These + builds are untested. + +Library directory + The libraries are built or copied to a directory of their own rather than to the + VUnit output path, which may not allow executing files: + ``~/.vunit-python-bridge`` in the home directory of the user, on every + platform (``%USERPROFILE%\.vunit-python-bridge`` on Windows). The environment + variable ``VUNIT_PYTHON_BRIDGE_LIBRARY_DIR`` selects another directory on + every platform. When the directory cannot be written, e.g. a read-only home + directory in a container, the setup fails and asks for the variable to be + set to a writable directory. + The setup also fails, asking for the variable to be set, when the simulator + could not load the library from the directory: on Linux and macOS when it is + on a file system mounted ``noexec``, and on Windows when an execution policy + (AppLocker, Windows Defender Application Control) blocks the library there. + Any other reason the library does not load, for example a missing + ``libpython``, fails the setup with the reason the system gives, which the + simulators leave out. The directory can be shared by projects since every + build has a directory named after what it was built from. The configuration + of a project stays in ``/python_bridge`` and the simulator hooks + pass its path to the library in ``VUNIT_PYTHON_BRIDGE_CONFIG``. + +The bridge uses the full (version specific) CPython ABI rather than the +Stable ABI since embedding the interpreter in the environment VUnit runs in +requires the ``PyConfig`` initialization API, which is not part of the +limited API. + +The package registers simulator hooks that make the simulator find the +library automatically. NVC is given a ``--load`` option. GHDL gets the library +directory in its dynamic library search path, plus a linker search path and an +rpath for the ahead-of-time compiled llvm and gcc backends, which the hook recognizes +from the ``backend`` of the GHDL interface it is called with. + +GHDL mcode and llvm-jit find the library only through that search path +(``LD_LIBRARY_PATH``), since GHDL limits the library name in a VHPIDIRECT +attribute to 32 characters, too short for an absolute path. A ``ghdl`` wrapper +script that resets the environment, or a simulation re-run by hand outside +VUnit, then fails with ``cannot load VHPIDIRECT shared library +'libvunit_python_bridge.so'``. Such a wrapper must keep ``LD_LIBRARY_PATH`` +and ``VUNIT_PYTHON_BRIDGE_CONFIG``. The llvm and gcc backends find the library +through the rpath, but they also need ``VUNIT_PYTHON_BRIDGE_CONFIG``. + +The FLI +attributes generated for Questa name the library by absolute path, and the +``vsim`` process VUnit starts is given ``-noautoldlibpath`` on Linux so that +the C++ runtime Questa bundles, which is often older than the one the Python +extension modules of the environment (NumPy) were built against, does not take +precedence over the one of the system. That option is set up when the process +starts, so it has to go on the command line of the process rather than on the +``vsim`` command of the do-file VUnit generates, and it is only passed to a +``vsim`` listing it in ``vsim -help all``. No simulator options need to be set +by the user. + +Tested configurations +~~~~~~~~~~~~~~~~~~~~~~ + +========================== ======================================================== +Simulator Linux +========================== ======================================================== +NVC 1.22 (and 1.23-devel) +GHDL mcode 6.0.0, 7.0.0-dev +GHDL llvm-jit 6.0.0, 7.0.0-dev +GHDL llvm 6.0.0, 7.0.0-dev +GHDL gcc 6.0.0, 7.0.0-dev +Questa (FLI) Altera Starter FPGA Edition 2025.3 +========================== ======================================================== + +On Windows, CI tests NVC 1.22 and GHDL mcode (nightly) with Python 3.10, 3.12, +3.14 and 3.15. The released wheel is also installed and tested on Linux, macOS and on +Windows machines without a compiler, there with every supported Python, so +each of its prebuilt DLLs is run. + +Questa is tested manually on Linux and is not covered by CI; the Windows build +of its bridge library is untested. Riviera-PRO/Active-HDL (VHPI) is not tested +at all. diff --git a/docs/index.rst b/docs/index.rst index dea8845..6e2b44c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,9 +1,127 @@ -vunit-python-bridge -=================== +.. _python_bridge: -A VUnit package making Python callable from VHDL. +About vunit-python-bridge +========================= + +|docs| |repository| |pypi| |python| |license| |ci| + +.. |docs| image:: https://img.shields.io/readthedocs/vunit-python-bridge?label=website + :alt: Website + :target: https://vunit-python-bridge.readthedocs.io +.. |repository| image:: https://img.shields.io/badge/github-VUnit%2Fvunit--python--bridge-blue?logo=github + :alt: Repository + :target: https://github.com/VUnit/vunit-python-bridge +.. |pypi| image:: https://img.shields.io/pypi/v/vunit-python-bridge?label=pip%20install%20vunit-python-bridge&color=blue + :alt: PyPI + :target: https://pypi.org/project/vunit-python-bridge/ +.. |python| image:: https://img.shields.io/badge/python-3.10%20%E2%80%93%203.15-blue?logo=python&logoColor=white + :alt: Python versions + :target: requirements.html +.. |license| image:: https://img.shields.io/badge/license-MPL--2.0-blue + :alt: License + :target: https://github.com/VUnit/vunit-python-bridge/blob/main/LICENSE +.. |ci| image:: https://github.com/VUnit/vunit-python-bridge/actions/workflows/python_bridge.yml/badge.svg?branch=main + :alt: CI status + :target: https://github.com/VUnit/vunit-python-bridge/actions/workflows/python_bridge.yml + +vunit-python-bridge is a `VUnit `__ package that embeds a Python +interpreter in the simulator, so that a VHDL testbench can execute Python code and call Python +functions -- a NumPy reference model, a constraint solver, a plot of what the design just +produced -- without leaving the simulation. + +Key features +------------ + +* :doc:`Run Python code ` with ``exec``, evaluate expressions with ``eval`` and + call functions with ``call``, with the result converted to the VHDL type the context expects. +* :doc:`Values cross with their VHDL types `: ``integer``, ``real``, ``string``, + ``boolean``, ``std_ulogic``, ``unsigned``/``signed``, the vector types, and ``integer_array_t`` + as a NumPy array. +* :doc:`Sessions ` give models and component instances Python namespaces of their own. +* :doc:`Python files and modules `, including the run script itself, are executed or + imported from VHDL, with :doc:`file paths ` that work for testbenches and + verification components alike. +* :doc:`Python errors ` are VUnit failures with the traceback, on a logger that a test can + mock. +* No build step: the package builds the foreign language interface for the simulator itself, see + :doc:`how_it_works`. + +Supported simulators +-------------------- + +================================= ========== ======================================================== +Simulator Interface Status +================================= ========== ======================================================== +NVC VHPIDIRECT Tested on Linux, macOS and Windows +GHDL (mcode, llvm-jit, llvm, gcc) VHPIDIRECT Tested on Linux, macOS and Windows +Questa/ModelSim FLI Tested manually on Linux; the Windows build is untested +Riviera-PRO, Active-HDL VHPI Untested; a subset of the API, see :doc:`other_simulators` +================================= ========== ======================================================== + +Getting started +--------------- + +.. code-block:: bash + + pip install vunit-python-bridge + +The run script adds the package to the project: + +.. code-block:: python + + from vunit import VUnit + + vu = VUnit.from_argv() + vu.add_vhdl_builtins() + vu.add_package("vunit-python-bridge", allow_setup=True) + + lib = vu.add_library("lib") + lib.add_source_files("*.vhd") + + vu.main() + +``allow_setup=True`` is required because, when added, the package builds its native library and +registers simulator options as part of its setup function, which VUnit only runs when the project +allows it. + +``python_pkg`` and its foreign language interface, ``python_ffi_pkg``, then +become available through ``python_context``: + +.. code-block:: vhdl + + library vunit_lib; + context vunit_lib.vunit_context; + + library python_bridge; + context python_bridge.python_context; + + ... + + exec("import numpy as np"); + result := eval("int(np.sum([1, 2, 3]))"); + +.. toctree:: + :caption: User guide + :hidden: + + requirements + running_code + types + errors + sessions + python_files + file_paths + +.. toctree:: + :caption: Simulators + :hidden: + + how_it_works + other_simulators .. toctree:: - :maxdepth: 2 + :caption: About + :hidden: - user_guide + release_notes + GitHub diff --git a/docs/other_simulators.rst b/docs/other_simulators.rst new file mode 100644 index 0000000..e8ac486 --- /dev/null +++ b/docs/other_simulators.rst @@ -0,0 +1,42 @@ +.. _python_bridge:other_simulators: + +Other simulators +----------------- + +Riviera-PRO/Active-HDL (VHPI) implement ``python_ffi_pkg`` with a VHPI +application, built from the C sources in `src/vunit_python_bridge/native/vhpi +`__ +with their ``ccomp`` driver. On Windows, ``ccomp`` is given the directory of +the gcc bundled with the simulator (``/mingw``) or, when there is +none, of ``gcc`` on ``PATH``. The package builds the application under the +output path (``//libraries``) the first time it is +added and rebuilds it when the sources, the Python running VUnit or the +simulator installation change, so a run script needs nothing beyond +``add_package("vunit-python-bridge", allow_setup=True)``. + +This application differs from the Python bridge in a few ways: only the +default session exists, the operations implemented by the bridge +(``integer_array_t`` values, the ``boolean``, ``std_ulogic``, vector and +``integer_array_t`` results, ``exec_file``) report that they require NVC, GHDL +or Questa, a Python error stops the simulation with the message printed by the +application rather than through the logger of the session, and ``real`` values +outside the single precision float range are rejected. + +The ``embedded_python`` example of this repository, +`examples/embedded_python +`__, +is a complete example covering all three simulator families. + +Its ``tb_example.vhd`` has a test case for each part of the API: ``exec`` and +``eval`` with the types they convert, calls with positional, keyword and group +arguments, a 20 register status dump, wide +``arg_unsigned``/``arg_signed`` and ``std_ulogic`` argument values, a 2-D +``integer_array_t`` image transposed by NumPy, the result types of ``eval`` and +``call``, Python files executed with ``exec_file`` or imported with +``import_module_from_file``, two models loaded into a session each, and a +Python model failing with the logger of the default session mocked. Its last +test case drives ``python_model``, a verification component whose behaviour is +the Python function in ``python_model.py`` rather than VHDL. The component finds +that file by joining its name to ``base_path``, the directory of the run script, which +``run.py`` sets as the base directory, while the testbench gives the files it +loads itself relative to its own directory. See :ref:`python_bridge:file_paths`. diff --git a/docs/python_files.rst b/docs/python_files.rst new file mode 100644 index 0000000..5712d0c --- /dev/null +++ b/docs/python_files.rst @@ -0,0 +1,81 @@ +Python files and modules +======================== + +exec_file +--------- + +``exec_file`` executes a Python file (where the Python bridge implements the +API: NVC, GHDL and Questa), with the equivalent of + +.. code-block:: python + + with open(path, encoding="utf-8") as file: + source = file.read() + exec(compile(source, str(path), "exec"), namespace, namespace) + +which means that tracebacks show the real file name and line numbers, +``__file__`` is set to the absolute path of the file while it executes (and +restored afterwards), and the directory of the file is added to ``sys.path`` +while it executes so that it can import sibling modules without setting +``PYTHONPATH``. Imports are therefore expected at the top level of the file. +Executing the same file twice executes it twice. + +A relative file name is relative to the directory of the testbench file, +``tb_path``, and needs ``test_runner_setup`` to have completed; an absolute +file name is used as given. See :ref:`python_bridge:file_paths` for loading a +file at any other time, for example from a verification component. Like the +other operations, it takes an optional trailing :ref:`session +` parameter. + +import_run_script +------------------ + +``import_run_script`` imports the VUnit run script as a Python module, so +that functions and classes defined in it (for example a plot helper using +matplotlib) can be called from VHDL without duplicating them in a separate +file: + +.. code-block:: vhdl + + import_run_script; + exec("run.hello_world()"); + + import_run_script("my_run_script"); + exec("my_run_script.hello_world()"); + +Without an explicit name, the module is named after the run script's file +name (without extension), so the run script is normally named ``run.py``. The +run script is found by its absolute path, and its directory is on ``sys.path`` +so that it can import its siblings, see :ref:`python_bridge:file_paths`. +Because the run script is imported as a module, it must be import-safe: code +that is only meant to run when the script is invoked directly (typically the +call to ``vu.main()``) must be behind +``if __name__ == "__main__":``; a run script without such a guard is +rejected before it is imported. Like the other operations, it takes an +optional trailing :ref:`session ` parameter. + +import_module_from_file and to_py_list_str +-------------------------------------------- + +``import_module_from_file`` imports any Python file as a module by path (also +taking an optional :ref:`session ` parameter), and is +what ``import_run_script`` uses internally. Like the file name of +``exec_file``, a relative path is relative to the directory of the testbench +file, ``tb_path``, and needs ``test_runner_setup`` to have completed; an +absolute path is used as it is given. See :ref:`python_bridge:file_paths` for +importing a file at any other time, for example from a verification +component: + +.. code-block:: vhdl + + import_module_from_file("reference_model.py", "reference_model"); -- /reference_model.py + exec("reference_model.configure(gain=4)"); + +``to_py_list_str`` converts an ``integer_vector``, ``integer_vector_ptr_t`` or +``real_vector`` to the string representation of the equivalent Python list, +for use in ``exec``/``eval`` strings: + +.. code-block:: vhdl + + exec("a_list = " & to_py_list_str(integer_vector'(1, 2, 3, 4))); + check_equal(eval("sum(a_list)"), 10); diff --git a/docs/requirements.rst b/docs/requirements.rst new file mode 100644 index 0000000..cbd520a --- /dev/null +++ b/docs/requirements.rst @@ -0,0 +1,34 @@ +Requirements +------------ + +* A VUnit with support for packages and simulator hooks, see the `README + `__. +* NVC, GHDL or Questa/ModelSim, through the VUnit Python bridge, see + :ref:`python_bridge:native`, or Riviera-PRO/Active-HDL (VHPI) through the VHPI application, which + the package builds in the same way, see + :ref:`python_bridge:other_simulators`. Any other simulator raises a + ``RuntimeError``. +* VHDL-2008 or later. +* CPython 3.10 or later with the standard (GIL) build. Free-threaded builds + are rejected with an error. +* Linux: a C compiler (``cc``, ``gcc`` or ``clang``, or ``CC``) and + the Python development headers (for example the ``python3-dev`` package) + since the bridge library is compiled on first use, see + :ref:`python_bridge:native`. Python must provide a shared ``libpython`` + (``--enable-shared``), which is the case for distribution Pythons, + ``actions/setup-python``, ``uv`` and ``pyenv`` builds with default settings. +* Windows: a 64-bit CPython from python.org (or compatible, such as + ``actions/setup-python`` and ``uv``). No compiler is needed for NVC and GHDL + when the package includes the prebuilt DLLs. Otherwise, and always for + Questa and Riviera-PRO/Active-HDL, a MinGW-w64 ``gcc`` is needed: ``CC``, the + one bundled with the simulator if there is one, or ``gcc`` on ``PATH``. + MSYS2/MinGW Pythons are not supported. The simulator must be a 64-bit (x64) + build too, since the Python DLL is loaded into its process; the package + rejects 32-bit and ARM64 simulators and Pythons. + +The simulator runs Python in the same environment as VUnit itself, including +an active virtual environment and its installed packages. + +A missing prerequisite, for example a missing C compiler or missing Python +headers, is reported when the package is added: ``add_package`` raises an error +explaining what to install. diff --git a/docs/requirements.txt b/docs/requirements.txt index 770fd46..d142c67 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,2 +1,4 @@ -# Pinned so that a new Sphinx release doesn't break the -W build. +# Pinned so that a new release doesn't break the -W build. sphinx==9.1.0 +sphinx-rtd-theme==3.1.0 +myst-parser==5.1.0 diff --git a/docs/running_code.rst b/docs/running_code.rst new file mode 100644 index 0000000..ed3b1de --- /dev/null +++ b/docs/running_code.rst @@ -0,0 +1,264 @@ +Running Python code +=================== + +.. _python_bridge:setup_and_cleanup: + +python_setup and python_cleanup +-------------------------------- + +Both are optional. ``python_setup`` starts the interpreter eagerly, otherwise +it starts on first use. ``python_cleanup`` releases the values staged for +``integer_array_t`` arguments early; they are released anyway when the +simulator process ends. It does *not* finalize the interpreter, so it is safe +to call even if more Python code runs afterwards (for example in another test +case of the same simulation). Output is flushed by every operation and does not +depend on ``python_cleanup``. + +.. code-block:: vhdl + + test_runner_setup(runner, runner_cfg); + python_setup; -- Optional, the interpreter starts on first use + + ... + + python_cleanup; -- Optional, releases the staged values early + test_runner_cleanup(runner); + +exec +---- + +``exec`` executes a string of Python source code in a persistent namespace +(``__main__`` by default, or another :ref:`session `) +that lives until the simulation ends. Names defined by one ``exec`` are +therefore visible to later calls to ``exec`` and ``eval`` on the same session. + +.. code-block:: vhdl + + exec("a = -17"); + check_equal(eval("abs(a)"), 17); + +Multiline code can be written with an explicit ``LF``, or with the ``+`` +operator that ``python_pkg`` defines for strings as a shorthand for ``& LF &``: + +.. code-block:: vhdl + + exec( + "def scale(x, gain):" & LF & + " return x * gain" + ); + + exec( + "import numpy as np" + + "" + + "def fibonacci(n):" + + " a, b = 0, 1" + + " for _ in range(n):" + + " a, b = b, a + b" + + " return a" + ); + +Indentation and blank lines (``""``) are preserved exactly. ``+`` only applies +where a ``string`` is expected; ``numeric_std`` arithmetic such as +``unsigned'("0011") + "0001"`` is unaffected. + +eval +---- + +``eval`` evaluates a string containing a Python expression, in the default +session or another :ref:`session `, and converts the +result to the VHDL type expected by the context, through one of these +functions (all aliased ``eval``): + +* ``eval_integer``, returning ``integer`` +* ``eval_real``, returning ``real`` +* ``eval_string``, returning ``string`` +* ``eval_integer_vector``, returning ``integer_vector`` +* ``eval_real_vector``, returning ``real_vector`` +* ``eval_integer_vector_ptr``, returning ``integer_vector_ptr_t`` + +.. code-block:: vhdl + + variable answer : integer; + variable ratio : real; + variable list : integer_vector_ptr_t; + begin + exec("from math import pi"); + answer := eval("6 * 7"); + ratio := eval("pi"); + list := eval("[1, 1, 2, 3, 5, 8]"); + +When the result type is not clear from the context (for example when it is +passed as an argument, or logged with ``info``), the explicit ``eval_`` +name selects the overload: + +.. code-block:: vhdl + + info("pi = " & to_string(eval_real("pi"))); + +On NVC, GHDL and Questa, where the API is implemented by the VUnit Python +bridge, ``eval`` has more result types: + +* ``eval_boolean``, returning ``boolean``. Only ``bool``/``numpy.bool_`` is + accepted. It also makes the result of ``eval`` usable as a condition: + ``if eval("model.is_done()") then``. +* ``eval_std_ulogic``, returning ``std_ulogic``. +* ``eval_std_ulogic_vector``, returning an unconstrained ``std_ulogic_vector``. +* ``eval_integer_array``, returning ``integer_array_t``, see + :ref:`python_bridge:integer_array`. + +``eval_boolean`` and ``eval_std_ulogic`` are aliased ``eval`` like the other +result types. ``eval_std_ulogic_vector`` and ``eval_integer_array`` are not, +since that would make ``check_equal(eval("17"), 17)`` and +``length(eval("[1, 2]"))`` ambiguous: use their explicit names. + +``std_ulogic_vector``, ``signed`` and ``unsigned`` results are also available +as the procedures ``eval_std_ulogic_vector``, ``eval_signed`` and +``eval_unsigned``, that take the result as an ``out`` parameter. The width is +given by the actual, and a Python value that does not fit that width is an +error. ``signed`` and ``unsigned`` results are only available in the +procedure form, since a function cannot know the width of the result. + +.. code-block:: vhdl + + variable byte : std_ulogic_vector(7 downto 0); + variable level : signed(15 downto 0); + begin + eval_std_ulogic_vector("format(value, '08b')", byte); + eval_signed("model.level()", level); + +call, arg, kwarg and to_call_str +--------------------------------- + +``call`` calls a Python function (or a callable expression such as a dotted +name, ``"np.sum"``, or a method, ``"model.run"``) with up to 10 arguments, +built with ``arg`` (positional) and ``kwarg`` (keyword), and converts the +returned value like ``eval``. Like every other operation, it accepts an +optional trailing :ref:`session ` parameter, normally +given by name since it follows the positional arguments: + +.. code-block:: vhdl + + variable gcd, count : integer; + variable ratio : real; + begin + exec("from math import gcd as py_gcd"); + gcd := call("py_gcd", arg(35), arg(77), arg(119)); + ratio := call("round", arg(3.14159), kwarg("ndigits", 3)); + + -- No return value: the procedure form of call + call("print", arg(35), arg(77), arg(119)); + +``arg`` and ``kwarg`` accept these value types: + +========================== ======================================================== +VHDL Python +========================== ======================================================== +``integer`` ``int`` +``real`` ``float`` +``boolean`` ``bool`` +``string`` ``str`` +``integer_vector`` ``list`` of ``int`` +``real_vector`` ``list`` of ``float`` +``integer_vector_ptr_t`` ``list`` of ``int`` +``std_ulogic`` ``bool``, 1 and H give ``True``, 0 and L give ``False`` +``unsigned`` ``int``, passed as ``arg_unsigned``/``kwarg_unsigned`` +``signed`` ``int``, passed as ``arg_signed``/``kwarg_signed`` +``integer_array_t`` NumPy array, not on Riviera-PRO/Active-HDL +========================== ======================================================== + +An ``unsigned`` or ``signed`` value of any width becomes an exact Python +integer, so it is not limited to the range of a VHDL ``integer``. The two have +names of their own rather than ``arg`` overloads, since a string literal +belongs to every character array type and an overload would therefore make +``arg("hello")`` ambiguous. ``H`` and ``L`` are read as 1 and 0 in a +``std_ulogic``, ``unsigned`` or ``signed`` value; any other metavalue is an +error. + +A value that cannot be converted has no session and is reported as a failure +on ``python_logger`` itself. The argument becomes an expression raising the +same message in Python. The call it is used in therefore fails with that +message as well instead of being made without the argument, which is what would +otherwise happen when the logger is mocked and the simulation continues: + +.. code-block:: text + + FAILURE - python_bridge:python - arg cannot convert 'X'; expected '0', '1', 'L' or 'H' + FAILURE - python_bridge:python:default - eval("model(__vunit__.error("arg cannot convert 'X'; ..."))") failed: + Traceback (most recent call last): + ... + RuntimeError: arg cannot convert 'X'; expected '0', '1', 'L' or 'H' + +An aggregate or a literal does not select an overload by itself and needs a +qualified expression: ``arg(real_vector'(1.0, 2.0))``, +``arg(integer_vector'(1, 2, 3))``. There is no ``std_ulogic_vector`` value, +which would be ambiguous for the same reason as ``unsigned``. Such a value is +passed as a number or as the string of its characters: + +.. code-block:: vhdl + + call("model.scale", arg_unsigned(unsigned(gain))); + call("model.push", arg(to_string(slv))); + +On NVC, GHDL and Questa, ``call`` returns the same types as ``eval``: +``call_boolean``, ``call_std_ulogic``, ``call_std_ulogic_vector``, +``call_integer_array``, ``call_string``, ``call_real_vector`` and +``call_integer_vector_ptr``, plus the procedures ``call_std_ulogic_vector``, +``call_signed`` and ``call_unsigned`` taking the result as an ``out`` +parameter. All of the functions but ``call_std_ulogic_vector`` are aliased +``call``. + +``to_call_str`` builds the Python call expression itself, as a string, which +is useful to embed a call inside a larger ``exec``/``eval`` string: + +.. code-block:: vhdl + + exec("gcd = " & to_call_str("py_gcd", arg(35), arg(77))); + +Every unused trailing argument of ``call``/``to_call_str`` defaults to an +ignored placeholder, so calls with fewer than 10 arguments need no padding. + +Argument groups +~~~~~~~~~~~~~~~~ + +Arguments combined with ``&`` become a single argument, which makes it +possible to pass more than 10 arguments and to build the arguments of a call +in steps: + +.. code-block:: vhdl + + -- Calls plot(data, **dict(title="Step response", grid=True)) + call("plot", arg(data), kwarg("title", string'("Step response")) & kwarg("grid", true)); + + -- Calls scale(*(1, 2,), **dict(gain=3)) + call("scale", arg(1) & arg(2) & kwarg("gain", 3)); + +A group of positional arguments is passed to Python as ``*(1, 2,)``, a group +of keyword arguments as ``**dict(a=1, b=2)`` and a group of both as +``*(1,), **dict(a=1)``. It uses one of the 10 argument slots, no matter how +many arguments it holds, and keeps Python's own rules: a group with keyword +arguments must come after the positional arguments of the call and a keyword +must not be repeated within it. Appending a positional argument to a group +that already has keyword arguments is an error for the same reason, reported +as ``positional argument after keyword arguments``. + +``null_arg`` is the identity of the operation. That is what makes a group +possible to build one argument at a time, ending with ``null_arg``, and what +lets a helper that contributes an optional argument return ``null_arg`` when +it has none. An empty group is a call with no arguments rather than an error. +Since ``arg_t`` holds unconstrained strings, a group is grown by a function +rather than by assigning to a variable in a loop: + +.. code-block:: vhdl + + -- The registers from idx and up, an empty group when there are none + impure function registers(idx : natural) return arg_t is + begin + if idx = num_registers then + return null_arg; + end if; + return kwarg(name(idx), value(idx)) & registers(idx + 1); + end; + + ... + + call("model.configure", registers(0)); diff --git a/docs/sessions.rst b/docs/sessions.rst new file mode 100644 index 0000000..8050975 --- /dev/null +++ b/docs/sessions.rst @@ -0,0 +1,161 @@ +.. _python_bridge:sessions: + +Sessions +-------- + +Every operation that runs Python code or evaluates a Python expression -- +``exec``, ``eval``/``eval_``, ``call``, ``exec_file``, +``import_run_script`` and ``import_module_from_file`` -- takes an optional +trailing parameter, +``session : python_session_t := default_session``, that selects the namespace +the operation runs in. A session is made with ``new_session`` and its Python +namespace is created the first time it is used; the ``default_session`` +constant used when the parameter is omitted runs in ``__main__``. + +.. code-block:: vhdl + + constant golden : python_session_t := new_session("golden"); + constant fixed_point : python_session_t := new_session("fixed_point"); + + ... + + exec("x = 1", session => golden); + exec_file("fixed_point.py", session => fixed_point); -- /fixed_point.py + + expected := eval("model(x)", session => golden); + got := call("model", arg(x), session => fixed_point); + +Since ``session`` comes after up to 10 positional arguments in ``call``, it is +normally given by name, as above. Both sessions in the example can define +``model`` without interfering with each other. + +A session is a VUnit object with an `identity +`__ of its own, returned by +``get_id(session)``. ``new_session(name)`` creates the identity ``name`` under +``python_bridge:python``, the identity of ``python_logger``, while +``new_session(id)`` takes an identity that already exists and can therefore +place the session anywhere in the identity tree, for example under the identity +of what it serves: + +.. code-block:: vhdl + + constant model_id : id_t := get_id("model", parent => get_id("my_testbench")); + constant model : python_session_t := new_session(model_id); + +Two sessions with the same identity are the same session, so a session does not +have to be passed around to be used in several places. ``name(session)`` is the +name of its identity without hierarchy, while the full name of the identity is +the key of its Python namespace. Two sessions with the same name but different +identities are therefore namespaces of their own. + +Errors of an operation are reported on the logger of the identity of the +session it was performed in, ``get_logger(get_id(session))``. The loggers of +the sessions created from a name are children of ``python_logger``, so log +levels and log handler settings made on ``python_logger`` apply to them, but +mocking a logger does not capture what its children log. A test that mocks the +errors of a session mocks the logger of that session, the default session +included: + +.. code-block:: vhdl + + constant default_logger : logger_t := get_logger(get_id(default_session)); + constant golden_logger : logger_t := get_logger(get_id(golden)); + + ... + + mock(golden_logger, failure); + +On Riviera-PRO/Active-HDL (VHPI), only the default session is supported: +passing any other session fails with a clear error. + +.. _python_bridge:instance_sessions: + +Independent models of component instances +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The bridge runs one Python interpreter per simulation, so a component with a +Python model that is instantiated more than once needs a session per instance +to give every instance a model state of its own. Without sessions, or with a +session made from a fixed name, all instances execute the model in the same +namespace and overwrite or share its functions and variables. The +``'instance_name`` attribute gives every instance a distinct identity to make +the session from: + +.. code-block:: vhdl + + architecture python of accumulator_model is + begin + model : process is + variable session : python_session_t; + begin + session := new_session(accumulator_model'instance_name); + + wait on x; + exec_file(model_file, session); + + loop + y <= call("accumulate", arg(x), session => session); + wait on x; + end loop; + end process; + end architecture; + +Every instance executes the same file in its own session and passes that +session to every operation. The session is made in the statement part of the +process since GHDL leaves the instance labels out of ``'instance_name`` in a +declaration, which would give all instances the same session. A relative ``model_file`` is relative to the +testbench file (``tb_path``), which is only known after +``test_runner_setup``, so the file is executed when the first input arrives +rather than at the start of the simulation. The ``Test independent models of +two instances`` test case of the `embedded_python example +`__ +drives two instances of this component, ``accumulator_model.vhd``, with +different inputs and checks that each keeps a total of its own. + +An alternative is to define the model as a class and create one object per +session, keeping the mutable model state on ``self`` rather than in globals: + +.. code-block:: python + + # accumulator.py + class Accumulator: + def __init__(self): + self.total = 0 + + def accumulate(self, x): + self.total += x + return self.total + +.. code-block:: vhdl + + import_module_from_file(join(tb_path(runner_cfg), "accumulator.py"), "accumulator", session); + exec("model = accumulator.Accumulator()", session); + + y <= call("model.accumulate", arg(x), session => session); + +The module, and so the class, is shared by all sessions (see the caveats +below), while every session has a ``model`` object of its own. Like all +non-default sessions, this needs the Python bridge: NVC, GHDL or Questa. + +Caveats +~~~~~~~ + +Sessions are separate namespaces in *one* Python interpreter, not separate +interpreters (which would not work with NumPy and many other extension +modules). Consequently, only the names defined by the executed code, such as +functions, classes and variables, are separate. Everything else is shared: + +* Imported modules are loaded once and shared by all sessions. A module + imported in one session is the same module object in another session, so + changes to module state, such as ``np.random.seed(...)`` or attributes set + on a module, are visible in all sessions. The same applies to sibling + modules imported by a Python file executed in several sessions. State kept + in the globals of an imported module is therefore not isolated by sessions. +* ``sys.path``, ``sys.modules``, environment variables, the current directory + and open files are process wide. The current directory is the working + directory of the simulator, see :ref:`python_bridge:file_paths`. +* Only the default session runs in ``__main__``. Classes defined in other + sessions report ``__main__`` as their module but cannot be found there, + which matters for example when pickling their instances. +* All sessions end with the simulation. Test cases run in the same simulation + (``run_all_in_same_sim``) share the sessions. diff --git a/docs/types.rst b/docs/types.rst new file mode 100644 index 0000000..b3caeb4 --- /dev/null +++ b/docs/types.rst @@ -0,0 +1,64 @@ +Types +===== + +Type mapping +------------ + +* ``integer`` โ†” ``int``. Results outside the VHDL integer range fail. +* ``real`` โ†” ``float``. Strict: an ``int`` result is not accepted. +* ``string`` โ†” ``str``. UTF-8. +* ``boolean`` โ†” ``bool``/``numpy.bool_`` (Python bridge only). +* ``integer_vector`` โ†” ``list`` of ``int``. +* ``real_vector`` โ†” ``list`` of ``float`` (``call_real_vector`` needs the bridge). +* ``integer_vector_ptr_t`` โ†” ``list`` of ``int``. +* ``std_ulogic``/``std_ulogic_vector`` โ†” ``str``, one character per element + out of ``U X 0 1 Z W L H -``, left to right (results only, Python bridge). +* ``signed``/``unsigned`` โ†” ``int`` (procedure results only, Python bridge). +* ``integer_array_t`` โ†” ``numpy.ndarray`` (Python bridge), see + :ref:`python_bridge:integer_array`. + +The types marked as needing the Python bridge are available on NVC, GHDL and +Questa, but not on Riviera-PRO/Active-HDL. + +Results are strict: a value that does not fit the VHDL type, or is of the +wrong Python type, is an error. Values are never silently truncated or +wrapped. + +.. _python_bridge:integer_array: + +integer_array_t and NumPy +-------------------------- + +.. note:: + + ``integer_array_t`` values are transferred by the VUnit Python bridge and + are therefore available on NVC, GHDL and Questa, but not on + Riviera-PRO/Active-HDL. + +An ``integer_array_t`` argument is transferred to Python and referred to by +the expression as a NumPy array of dtype ``int32``, so it can be reused in +several calls. The shape follows VUnit's indexing so that elements correspond +directly: + +============= ================================ =========================== +Array VHDL Python +============= ================================ =========================== +1D ``get(a, i)`` ``a[i]``, shape ``(length,)`` +2D ``get(a, x, y)`` ``a[y, x]``, shape ``(height, width)`` +3D ``get(a, x, y, z)`` ``a[y, x, z]``, shape ``(height, width, depth)`` +============= ================================ =========================== + +``integer_array_t`` does not record its number of dimensions. An argument is +treated as 3D if its depth is larger than one, else as 2D if its height is +larger than one, else as 1D. + +A returned array (or nested list of integers) becomes a new +``integer_array_t`` with the corresponding width, height and depth. Its +``bit_width`` and ``is_signed`` are those of the argument, if the function +returns one of its ``integer_array_t`` arguments (possibly modified in +place), else given by the dtype: ``bool`` is 1 bit unsigned, ``int8``/``uint8`` +8 bit, ``int16``/``uint16`` 16 bit and all other integer dtypes 32 bit +signed. All values must fit that word size, otherwise the call fails. +Floating point arrays are rejected; convert them explicitly with ``astype``. +As usual, the returned ``integer_array_t`` is owned by the caller and can be +freed with ``deallocate``. diff --git a/docs/user_guide.rst b/docs/user_guide.rst deleted file mode 100644 index 81d247f..0000000 --- a/docs/user_guide.rst +++ /dev/null @@ -1,1087 +0,0 @@ -.. _python_bridge: - -Calling Python from VHDL -======================== - -``vunit-python-bridge`` is a VUnit package that embeds a Python interpreter in -the simulator so that VHDL testbenches can execute Python code and call Python -functions, for example reference models written with NumPy. The VHDL API, -``python_pkg``/``python_context``, is compiled into the ``python_bridge`` -library and is added to a project with ``add_package()``: - -.. code-block:: python - - from vunit import VUnit - - vu = VUnit.from_argv() - vu.add_vhdl_builtins() - vu.add_package("vunit-python-bridge", allow_setup=True) - - lib = vu.add_library("lib") - lib.add_source_files("*.vhd") - - vu.main() - -``allow_setup=True`` is required because, when added, the package builds its native library and -registers simulator options as part of its setup function, which VUnit only runs when the project -allows it. - -``python_pkg`` and its foreign language interface, ``python_ffi_pkg``, then -become available through ``python_context``: - -.. code-block:: vhdl - - library vunit_lib; - context vunit_lib.vunit_context; - - library python_bridge; - context python_bridge.python_context; - - ... - - exec("import numpy as np"); - result := eval("int(np.sum([1, 2, 3]))"); - -Requirements ------------- - -* A VUnit with support for packages and simulator hooks, see the README. -* NVC, GHDL or Questa/ModelSim, through the VUnit Python bridge described - below, or Riviera-PRO/Active-HDL (VHPI) through the VHPI application, which - the package builds in the same way, see - :ref:`python_bridge:other_simulators`. Any other simulator raises a - ``RuntimeError``. -* VHDL-2008 or later. -* CPython 3.10 or later with the standard (GIL) build. Free-threaded builds - are rejected with an error. -* Linux: a C compiler (``cc``, ``gcc`` or ``clang``, or ``CC``) and - the Python development headers (for example the ``python3-dev`` package) - since the bridge library is compiled on first use, see - :ref:`python_bridge:native`. Python must provide a shared ``libpython`` - (``--enable-shared``), which is the case for distribution Pythons, - ``actions/setup-python``, ``uv`` and ``pyenv`` builds with default settings. -* Windows: a 64-bit CPython from python.org (or compatible, such as - ``actions/setup-python`` and ``uv``). No compiler is needed for NVC and GHDL - when the package includes the prebuilt DLLs. Otherwise, and always for - Questa and Riviera-PRO/Active-HDL, a MinGW-w64 ``gcc`` is needed: ``CC``, the - one bundled with the simulator if there is one, or ``gcc`` on ``PATH``. - MSYS2/MinGW Pythons are not supported. The simulator must be a 64-bit (x64) - build too, since the Python DLL is loaded into its process; the package - rejects 32-bit and ARM64 simulators and Pythons. - -The simulator runs Python in the same environment as VUnit itself, including -an active virtual environment and its installed packages. - -A missing prerequisite, for example a missing C compiler or missing Python -headers, is reported when the package is added: ``add_package`` raises an error -explaining what to install. - -.. _python_bridge:setup_and_cleanup: - -python_setup and python_cleanup --------------------------------- - -Both are optional. ``python_setup`` starts the interpreter eagerly, otherwise -it starts on first use. ``python_cleanup`` releases the values staged for -``integer_array_t`` arguments early; they are released anyway when the -simulator process ends. It does *not* finalize the interpreter, so it is safe -to call even if more Python code runs afterwards (for example in another test -case of the same simulation). Output is flushed by every operation and does not -depend on ``python_cleanup``. - -.. code-block:: vhdl - - test_runner_setup(runner, runner_cfg); - python_setup; -- Optional, the interpreter starts on first use - - ... - - python_cleanup; -- Optional, releases the staged values early - test_runner_cleanup(runner); - -.. _python_bridge:sessions: - -Sessions --------- - -Every operation that runs Python code or evaluates a Python expression -- -``exec``, ``eval``/``eval_``, ``call``, ``exec_file``, -``import_run_script`` and ``import_module_from_file`` -- takes an optional -trailing parameter, -``session : python_session_t := default_session``, that selects the namespace -the operation runs in. A session is made with ``new_session`` and its Python -namespace is created the first time it is used; the ``default_session`` -constant used when the parameter is omitted runs in ``__main__``. - -.. code-block:: vhdl - - constant golden : python_session_t := new_session("golden"); - constant fixed_point : python_session_t := new_session("fixed_point"); - - ... - - exec("x = 1", session => golden); - exec_file("fixed_point.py", session => fixed_point); -- /fixed_point.py - - expected := eval("model(x)", session => golden); - got := call("model", arg(x), session => fixed_point); - -Since ``session`` comes after up to 10 positional arguments in ``call``, it is -normally given by name, as above. Both sessions in the example can define -``model`` without interfering with each other. - -A session is a VUnit object with an `identity -`__ of its own, returned by -``get_id(session)``. ``new_session(name)`` creates the identity ``name`` under -``python_bridge:python``, the identity of ``python_logger``, while -``new_session(id)`` takes an identity that already exists and can therefore -place the session anywhere in the identity tree, for example under the identity -of what it serves: - -.. code-block:: vhdl - - constant model_id : id_t := get_id("model", parent => get_id("my_testbench")); - constant model : python_session_t := new_session(model_id); - -Two sessions with the same identity are the same session, so a session does not -have to be passed around to be used in several places. ``name(session)`` is the -name of its identity without hierarchy, while the full name of the identity is -the key of its Python namespace. Two sessions with the same name but different -identities are therefore namespaces of their own. - -Errors of an operation are reported on the logger of the identity of the -session it was performed in, ``get_logger(get_id(session))``. The loggers of -the sessions created from a name are children of ``python_logger``, so log -levels and log handler settings made on ``python_logger`` apply to them, but -mocking a logger does not capture what its children log. A test that mocks the -errors of a session mocks the logger of that session, the default session -included: - -.. code-block:: vhdl - - constant default_logger : logger_t := get_logger(get_id(default_session)); - constant golden_logger : logger_t := get_logger(get_id(golden)); - - ... - - mock(golden_logger, failure); - -On Riviera-PRO/Active-HDL (VHPI), only the default session is supported: -passing any other session fails with a clear error. - -.. _python_bridge:instance_sessions: - -Independent models of component instances -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The bridge runs one Python interpreter per simulation, so a component with a -Python model that is instantiated more than once needs a session per instance -to give every instance a model state of its own. Without sessions, or with a -session made from a fixed name, all instances execute the model in the same -namespace and overwrite or share its functions and variables. The -``'instance_name`` attribute gives every instance a distinct identity to make -the session from: - -.. code-block:: vhdl - - architecture python of accumulator_model is - begin - model : process is - variable session : python_session_t; - begin - session := new_session(accumulator_model'instance_name); - - wait on x; - exec_file(model_file, session); - - loop - y <= call("accumulate", arg(x), session => session); - wait on x; - end loop; - end process; - end architecture; - -Every instance executes the same file in its own session and passes that -session to every operation. The session is made in the statement part of the -process since GHDL leaves the instance labels out of ``'instance_name`` in a -declaration, which would give all instances the same session. A relative ``model_file`` is relative to the -testbench file (``tb_path``), which is only known after -``test_runner_setup``, so the file is executed when the first input arrives -rather than at the start of the simulation. The ``Test independent models of -two instances`` test case of the `embedded_python example -`__ -drives two instances of this component, ``accumulator_model.vhd``, with -different inputs and checks that each keeps a total of its own. - -An alternative is to define the model as a class and create one object per -session, keeping the mutable model state on ``self`` rather than in globals: - -.. code-block:: python - - # accumulator.py - class Accumulator: - def __init__(self): - self.total = 0 - - def accumulate(self, x): - self.total += x - return self.total - -.. code-block:: vhdl - - import_module_from_file(join(tb_path(runner_cfg), "accumulator.py"), "accumulator", session); - exec("model = accumulator.Accumulator()", session); - - y <= call("model.accumulate", arg(x), session => session); - -The module, and so the class, is shared by all sessions (see the caveats -below), while every session has a ``model`` object of its own. Like all -non-default sessions, this needs the Python bridge: NVC, GHDL or Questa. - -Caveats -~~~~~~~ - -Sessions are separate namespaces in *one* Python interpreter, not separate -interpreters (which would not work with NumPy and many other extension -modules). Consequently, only the names defined by the executed code, such as -functions, classes and variables, are separate. Everything else is shared: - -* Imported modules are loaded once and shared by all sessions. A module - imported in one session is the same module object in another session, so - changes to module state, such as ``np.random.seed(...)`` or attributes set - on a module, are visible in all sessions. The same applies to sibling - modules imported by a Python file executed in several sessions. State kept - in the globals of an imported module is therefore not isolated by sessions. -* ``sys.path``, ``sys.modules``, environment variables, the current directory - and open files are process wide. The current directory is the working - directory of the simulator, see :ref:`python_bridge:file_paths`. -* Only the default session runs in ``__main__``. Classes defined in other - sessions report ``__main__`` as their module but cannot be found there, - which matters for example when pickling their instances. -* All sessions end with the simulation. Test cases run in the same simulation - (``run_all_in_same_sim``) share the sessions. - -exec ----- - -``exec`` executes a string of Python source code in a persistent namespace -(``__main__`` by default, or another :ref:`session `) -that lives until the simulation ends. Names defined by one ``exec`` are -therefore visible to later calls to ``exec`` and ``eval`` on the same session. - -.. code-block:: vhdl - - exec("a = -17"); - check_equal(eval("abs(a)"), 17); - -Multiline code can be written with an explicit ``LF``, or with the ``+`` -operator that ``python_pkg`` defines for strings as a shorthand for ``& LF &``: - -.. code-block:: vhdl - - exec( - "def scale(x, gain):" & LF & - " return x * gain" - ); - - exec( - "import numpy as np" + - "" + - "def fibonacci(n):" + - " a, b = 0, 1" + - " for _ in range(n):" + - " a, b = b, a + b" + - " return a" - ); - -Indentation and blank lines (``""``) are preserved exactly. ``+`` only applies -where a ``string`` is expected; ``numeric_std`` arithmetic such as -``unsigned'("0011") + "0001"`` is unaffected. - -eval ----- - -``eval`` evaluates a string containing a Python expression, in the default -session or another :ref:`session `, and converts the -result to the VHDL type expected by the context, through one of these -functions (all aliased ``eval``): - -* ``eval_integer``, returning ``integer`` -* ``eval_real``, returning ``real`` -* ``eval_string``, returning ``string`` -* ``eval_integer_vector``, returning ``integer_vector`` -* ``eval_real_vector``, returning ``real_vector`` -* ``eval_integer_vector_ptr``, returning ``integer_vector_ptr_t`` - -.. code-block:: vhdl - - variable answer : integer; - variable ratio : real; - variable list : integer_vector_ptr_t; - begin - exec("from math import pi"); - answer := eval("6 * 7"); - ratio := eval("pi"); - list := eval("[1, 1, 2, 3, 5, 8]"); - -When the result type is not clear from the context (for example when it is -passed as an argument, or logged with ``info``), the explicit ``eval_`` -name selects the overload: - -.. code-block:: vhdl - - info("pi = " & to_string(eval_real("pi"))); - -On NVC, GHDL and Questa, where the API is implemented by the VUnit Python -bridge, ``eval`` has more result types: - -* ``eval_boolean``, returning ``boolean``. Only ``bool``/``numpy.bool_`` is - accepted. It also makes the result of ``eval`` usable as a condition: - ``if eval("model.is_done()") then``. -* ``eval_std_ulogic``, returning ``std_ulogic``. -* ``eval_std_ulogic_vector``, returning an unconstrained ``std_ulogic_vector``. -* ``eval_integer_array``, returning ``integer_array_t``, see - :ref:`python_bridge:integer_array`. - -``eval_boolean`` and ``eval_std_ulogic`` are aliased ``eval`` like the other -result types. ``eval_std_ulogic_vector`` and ``eval_integer_array`` are not, -since that would make ``check_equal(eval("17"), 17)`` and -``length(eval("[1, 2]"))`` ambiguous: use their explicit names. - -``std_ulogic_vector``, ``signed`` and ``unsigned`` results are also available -as the procedures ``eval_std_ulogic_vector``, ``eval_signed`` and -``eval_unsigned``, that take the result as an ``out`` parameter. The width is -given by the actual, and a Python value that does not fit that width is an -error. ``signed`` and ``unsigned`` results are only available in the -procedure form, since a function cannot know the width of the result. - -.. code-block:: vhdl - - variable byte : std_ulogic_vector(7 downto 0); - variable level : signed(15 downto 0); - begin - eval_std_ulogic_vector("format(value, '08b')", byte); - eval_signed("model.level()", level); - -call, arg, kwarg and to_call_str ---------------------------------- - -``call`` calls a Python function (or a callable expression such as a dotted -name, ``"np.sum"``, or a method, ``"model.run"``) with up to 10 arguments, -built with ``arg`` (positional) and ``kwarg`` (keyword), and converts the -returned value like ``eval``. Like every other operation, it accepts an -optional trailing :ref:`session ` parameter, normally -given by name since it follows the positional arguments: - -.. code-block:: vhdl - - variable gcd, count : integer; - variable ratio : real; - begin - exec("from math import gcd as py_gcd"); - gcd := call("py_gcd", arg(35), arg(77), arg(119)); - ratio := call("round", arg(3.14159), kwarg("ndigits", 3)); - - -- No return value: the procedure form of call - call("print", arg(35), arg(77), arg(119)); - -``arg`` and ``kwarg`` accept these value types: - -========================== ======================================================== -VHDL Python -========================== ======================================================== -``integer`` ``int`` -``real`` ``float`` -``boolean`` ``bool`` -``string`` ``str`` -``integer_vector`` ``list`` of ``int`` -``real_vector`` ``list`` of ``float`` -``integer_vector_ptr_t`` ``list`` of ``int`` -``std_ulogic`` ``bool``, 1 and H give ``True``, 0 and L give ``False`` -``unsigned`` ``int``, passed as ``arg_unsigned``/``kwarg_unsigned`` -``signed`` ``int``, passed as ``arg_signed``/``kwarg_signed`` -``integer_array_t`` NumPy array, not on Riviera-PRO/Active-HDL -========================== ======================================================== - -An ``unsigned`` or ``signed`` value of any width becomes an exact Python -integer, so it is not limited to the range of a VHDL ``integer``. The two have -names of their own rather than ``arg`` overloads, since a string literal -belongs to every character array type and an overload would therefore make -``arg("hello")`` ambiguous. ``H`` and ``L`` are read as 1 and 0 in a -``std_ulogic``, ``unsigned`` or ``signed`` value; any other metavalue is an -error. - -A value that cannot be converted has no session and is reported as a failure -on ``python_logger`` itself. The argument becomes an expression raising the -same message in Python. The call it is used in therefore fails with that -message as well instead of being made without the argument, which is what would -otherwise happen when the logger is mocked and the simulation continues: - -.. code-block:: text - - FAILURE - python_bridge:python - arg cannot convert 'X'; expected '0', '1', 'L' or 'H' - FAILURE - python_bridge:python:default - eval("model(__vunit__.error("arg cannot convert 'X'; ..."))") failed: - Traceback (most recent call last): - ... - RuntimeError: arg cannot convert 'X'; expected '0', '1', 'L' or 'H' - -An aggregate or a literal does not select an overload by itself and needs a -qualified expression: ``arg(real_vector'(1.0, 2.0))``, -``arg(integer_vector'(1, 2, 3))``. There is no ``std_ulogic_vector`` value, -which would be ambiguous for the same reason as ``unsigned``. Such a value is -passed as a number or as the string of its characters: - -.. code-block:: vhdl - - call("model.scale", arg_unsigned(unsigned(gain))); - call("model.push", arg(to_string(slv))); - -On NVC, GHDL and Questa, ``call`` returns the same types as ``eval``: -``call_boolean``, ``call_std_ulogic``, ``call_std_ulogic_vector``, -``call_integer_array``, ``call_string``, ``call_real_vector`` and -``call_integer_vector_ptr``, plus the procedures ``call_std_ulogic_vector``, -``call_signed`` and ``call_unsigned`` taking the result as an ``out`` -parameter. All of the functions but ``call_std_ulogic_vector`` are aliased -``call``. - -``to_call_str`` builds the Python call expression itself, as a string, which -is useful to embed a call inside a larger ``exec``/``eval`` string: - -.. code-block:: vhdl - - exec("gcd = " & to_call_str("py_gcd", arg(35), arg(77))); - -Every unused trailing argument of ``call``/``to_call_str`` defaults to an -ignored placeholder, so calls with fewer than 10 arguments need no padding. - -Argument groups -~~~~~~~~~~~~~~~~ - -Arguments combined with ``&`` become a single argument, which makes it -possible to pass more than 10 arguments and to build the arguments of a call -in steps: - -.. code-block:: vhdl - - -- Calls plot(data, **dict(title="Step response", grid=True)) - call("plot", arg(data), kwarg("title", string'("Step response")) & kwarg("grid", true)); - - -- Calls scale(*(1, 2,), **dict(gain=3)) - call("scale", arg(1) & arg(2) & kwarg("gain", 3)); - -A group of positional arguments is passed to Python as ``*(1, 2,)``, a group -of keyword arguments as ``**dict(a=1, b=2)`` and a group of both as -``*(1,), **dict(a=1)``. It uses one of the 10 argument slots, no matter how -many arguments it holds, and keeps Python's own rules: a group with keyword -arguments must come after the positional arguments of the call and a keyword -must not be repeated within it. Appending a positional argument to a group -that already has keyword arguments is an error for the same reason, reported -as ``positional argument after keyword arguments``. - -``null_arg`` is the identity of the operation. That is what makes a group -possible to build one argument at a time, ending with ``null_arg``, and what -lets a helper that contributes an optional argument return ``null_arg`` when -it has none. An empty group is a call with no arguments rather than an error. -Since ``arg_t`` holds unconstrained strings, a group is grown by a function -rather than by assigning to a variable in a loop: - -.. code-block:: vhdl - - -- The registers from idx and up, an empty group when there are none - impure function registers(idx : natural) return arg_t is - begin - if idx = num_registers then - return null_arg; - end if; - return kwarg(name(idx), value(idx)) & registers(idx + 1); - end; - - ... - - call("model.configure", registers(0)); - -import_run_script ------------------- - -``import_run_script`` imports the VUnit run script as a Python module, so -that functions and classes defined in it (for example a plot helper using -matplotlib) can be called from VHDL without duplicating them in a separate -file: - -.. code-block:: vhdl - - import_run_script; - exec("run.hello_world()"); - - import_run_script("my_run_script"); - exec("my_run_script.hello_world()"); - -Without an explicit name, the module is named after the run script's file -name (without extension), so the run script is normally named ``run.py``. The -run script is found by its absolute path, and its directory is on ``sys.path`` -so that it can import its siblings, see :ref:`python_bridge:file_paths`. -Because the run script is imported as a module, it must be import-safe: code -that is only meant to run when the script is invoked directly (typically the -call to ``vu.main()``) must be behind -``if __name__ == "__main__":``; a run script without such a guard is -rejected before it is imported. Like the other operations, it takes an -optional trailing :ref:`session ` parameter. - -import_module_from_file and to_py_list_str --------------------------------------------- - -``import_module_from_file`` imports any Python file as a module by path (also -taking an optional :ref:`session ` parameter), and is -what ``import_run_script`` uses internally. Like the file name of -``exec_file``, a relative path is relative to the directory of the testbench -file, ``tb_path``, and needs ``test_runner_setup`` to have completed; an -absolute path is used as it is given. See :ref:`python_bridge:file_paths` for -importing a file at any other time, for example from a verification -component: - -.. code-block:: vhdl - - import_module_from_file("reference_model.py", "reference_model"); -- /reference_model.py - exec("reference_model.configure(gain=4)"); - -``to_py_list_str`` converts an ``integer_vector``, ``integer_vector_ptr_t`` or -``real_vector`` to the string representation of the equivalent Python list, -for use in ``exec``/``eval`` strings: - -.. code-block:: vhdl - - exec("a_list = " & to_py_list_str(integer_vector'(1, 2, 3, 4))); - check_equal(eval("sum(a_list)"), 10); - -Type mapping ------------- - -* ``integer`` โ†” ``int``. Results outside the VHDL integer range fail. -* ``real`` โ†” ``float``. Strict: an ``int`` result is not accepted. -* ``string`` โ†” ``str``. UTF-8. -* ``boolean`` โ†” ``bool``/``numpy.bool_`` (Python bridge only). -* ``integer_vector`` โ†” ``list`` of ``int``. -* ``real_vector`` โ†” ``list`` of ``float`` (``call_real_vector`` needs the bridge). -* ``integer_vector_ptr_t`` โ†” ``list`` of ``int``. -* ``std_ulogic``/``std_ulogic_vector`` โ†” ``str``, one character per element - out of ``U X 0 1 Z W L H -``, left to right (results only, Python bridge). -* ``signed``/``unsigned`` โ†” ``int`` (procedure results only, Python bridge). -* ``integer_array_t`` โ†” ``numpy.ndarray`` (Python bridge), see - :ref:`python_bridge:integer_array`. - -The types marked as needing the Python bridge are available on NVC, GHDL and -Questa, but not on Riviera-PRO/Active-HDL. - -Results are strict: a value that does not fit the VHDL type, or is of the -wrong Python type, is an error. Values are never silently truncated or -wrapped. - -Errors ------- - -Python exceptions, syntax errors, type errors and undefined names are -reported as failures on the logger of the :ref:`session -` the operation was performed in, -``get_logger(get_id(session))``, including the Python traceback. For the -default session that logger is named ``python_bridge:python:default`` and -``python_logger`` (named ``python_bridge:python``) is its parent. The test -fails and stops like for any other failure. The logger of the session can be -mocked to test error handling; when mocked, ``eval``/``call`` return a default -value (``0``, ``0.0``, ``""`` or an empty vector, depending on the type) after -the failure. - -.. code-block:: text - - FAILURE - python_bridge:python:default - eval("1 / 0") failed: - Traceback (most recent call last): - File "", line 1, in - 1 / 0 - ~~^~~ - ZeroDivisionError: division by zero - -The default ``verbose`` format of the VUnit display handler indents the lines -of the traceback to the column where the message starts, after the time, the -logger name and the log level. With the long logger name of a session made from -an instance name and the absolute file paths of a traceback, the lines can be -wider than the terminal, which wraps them back to the first column and breaks -the alignment. The ``level`` format leaves out the time and the logger name and -indents the traceback by the width of the log level only: - -.. code-block:: vhdl - - test_runner_setup(runner, runner_cfg); - set_format(display_handler, level); - -.. code-block:: text - - FAILURE - eval("1 / 0") failed: - Traceback (most recent call last): - File "", line 1, in - 1 / 0 - ~~^~~ - ZeroDivisionError: division by zero - -Output of ``print`` is written to the simulator output and flushed after -every operation. - -.. _python_bridge:integer_array: - -integer_array_t and NumPy --------------------------- - -.. note:: - - ``integer_array_t`` values are transferred by the VUnit Python bridge and - are therefore available on NVC, GHDL and Questa, but not on - Riviera-PRO/Active-HDL. - -An ``integer_array_t`` argument is transferred to Python and referred to by -the expression as a NumPy array of dtype ``int32``, so it can be reused in -several calls. The shape follows VUnit's indexing so that elements correspond -directly: - -============= ================================ =========================== -Array VHDL Python -============= ================================ =========================== -1D ``get(a, i)`` ``a[i]``, shape ``(length,)`` -2D ``get(a, x, y)`` ``a[y, x]``, shape ``(height, width)`` -3D ``get(a, x, y, z)`` ``a[y, x, z]``, shape ``(height, width, depth)`` -============= ================================ =========================== - -``integer_array_t`` does not record its number of dimensions. An argument is -treated as 3D if its depth is larger than one, else as 2D if its height is -larger than one, else as 1D. - -A returned array (or nested list of integers) becomes a new -``integer_array_t`` with the corresponding width, height and depth. Its -``bit_width`` and ``is_signed`` are those of the argument, if the function -returns one of its ``integer_array_t`` arguments (possibly modified in -place), else given by the dtype: ``bool`` is 1 bit unsigned, ``int8``/``uint8`` -8 bit, ``int16``/``uint16`` 16 bit and all other integer dtypes 32 bit -signed. All values must fit that word size, otherwise the call fails. -Floating point arrays are rejected; convert them explicitly with ``astype``. -As usual, the returned ``integer_array_t`` is owned by the caller and can be -freed with ``deallocate``. - -exec_file ---------- - -``exec_file`` executes a Python file (where the Python bridge implements the -API: NVC, GHDL and Questa), with the equivalent of - -.. code-block:: python - - with open(path, encoding="utf-8") as file: - source = file.read() - exec(compile(source, str(path), "exec"), namespace, namespace) - -which means that tracebacks show the real file name and line numbers, -``__file__`` is set to the absolute path of the file while it executes (and -restored afterwards), and the directory of the file is added to ``sys.path`` -while it executes so that it can import sibling modules without setting -``PYTHONPATH``. Imports are therefore expected at the top level of the file. -Executing the same file twice executes it twice. - -A relative file name is relative to the directory of the testbench file, -``tb_path``, and needs ``test_runner_setup`` to have completed; an absolute -file name is used as given. See :ref:`python_bridge:file_paths` for loading a -file at any other time, for example from a verification component. Like the -other operations, it takes an optional trailing :ref:`session -` parameter. - -.. _python_bridge:file_paths: - -File paths ----------- - -A file name can be resolved against several directories, depending on who -resolves it: ``python_pkg`` in VHDL, or Python code in the model. This section -lists them all, gives recipes for the common cases and describes ``base_path``, -which gives verification components a directory of their own. - -How a file name is resolved -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. list-table:: - :header-rows: 1 - :widths: 40 60 - - * - File name - - Resolved against - * - Relative name given to ``exec_file`` or ``import_module_from_file`` - after ``test_runner_setup`` has completed - - The directory of the testbench file, ``tb_path``. - * - The same before ``test_runner_setup`` has completed, or without a - test runner - - Nothing: it is reported as a failure, which suggests calling it after - ``test_runner_setup``, giving an absolute name or joining it to - ``base_path``. - * - Absolute name given to ``exec_file`` or ``import_module_from_file`` - - Used as it is given. - * - ``join(base_path, "x")``, at any time, time 0 included - - The base directory set by ``set_relative_file_base`` in the run script - or by the environment variable ``VUNIT_PYTHON_BRIDGE_FILE_BASE``, which - ``base_path`` returns. Without a base directory ``base_path`` is - reported as a failure. - * - Relative name opened by Python code: ``open``, ``numpy.load``, - ``pathlib.Path`` and so on - - The current working directory of Python, which is the working - directory of the simulator. See the warning below. - * - Python module imported by name, ``import model`` - - ``sys.path``, which holds the directory of the run script first, like - ``python run.py`` does, the directories of ``PYTHONPATH`` (the - simulator inherits the environment of VUnit, so a ``PYTHONPATH`` set - in the run script before ``vu.main()`` is seen), and, while - ``exec_file`` executes a file, the directory of that file. - -.. warning:: - - The working directory of the simulator differs between simulators. On NVC - and GHDL it is the directory the run script was started from. On - Questa/ModelSim it is the ``modelsim`` directory of the VUnit output path, - for example ``vunit_out/modelsim``. Other simulators may use yet another - directory. Never rely on a relative file name in Python code: build it from - ``Path(__file__).parent`` in the model, or pass an absolute name from VHDL, - built with ``base_path`` or ``tb_path``. - -Recipes -~~~~~~~ - -**A Python file next to the testbench.** Give its name relative to the -testbench file, after ``test_runner_setup``: - -.. code-block:: vhdl - - test_runner_setup(runner, runner_cfg); - exec_file("model.py"); -- /model.py - -**The model of a verification component.** A verification component is not -tied to a testbench and often loads its model at time 0, before -``test_runner_setup``. Give its name relative to a base directory set in the -run script. With this layout: - -.. code-block:: text - - project/ - run.py - vc/ - uart_vc.vhd - models/ - uart_model.py - helpers.py - tb/ - tb_uart.vhd - -the run script sets its own directory as the base directory, which makes the -names independent of the directory the run script is started from: - -.. code-block:: python - - import vunit_python_bridge - - # project/, whatever the working directory - vunit_python_bridge.set_relative_file_base(Path(__file__).parent) - vu.add_package("vunit-python-bridge", allow_setup=True) - -.. code-block:: vhdl - - exec_file(join(base_path, "vc/models/uart_model.py"), session); -- project/vc/models/uart_model.py - import_module_from_file(join(base_path, "vc/models/helpers.py"), "helpers", session); -- project/vc/models/helpers.py - -Alternatively, import the model by module name from a directory on -``sys.path``: the directory of the run script, or one added to ``PYTHONPATH`` -in the run script before ``vu.main()``: - -.. code-block:: python - - # project/vc/models on sys.path of the simulator - os.environ["PYTHONPATH"] = str(Path(__file__).parent / "vc" / "models") - -.. code-block:: vhdl - - exec("import uart_model", session); -- found on sys.path: project/vc/models/uart_model.py - -Or wait for ``test_runner_setup`` to complete and give the name relative to the -testbench file: - -.. code-block:: vhdl - - if get_phase <= test_runner_setup then - wait on runner until get_phase > test_runner_setup; - end if; - exec_file("uart_model.py", session); -- /uart_model.py - -``wait_until(runner, test_runner_setup)`` is not enough, since the phase is -entered before the testbench path is set. - -**Data files used by a model.** Find them relative to the model file in -Python: - -.. code-block:: python - - from pathlib import Path - - # Next to this file, whatever the working directory of the simulator - COEFFICIENTS = np.load(Path(__file__).parent / "coefficients.npy") - -or pass an absolute name from VHDL: - -.. code-block:: vhdl - - -- project/vc/data/coefficients.npy, an absolute name for Python - call("model.load_coefficients", arg(join(base_path, "vc/data/coefficients.npy"))); - -**A run script shared through a git submodule.** When the run script cannot be -changed, set the base directory with the environment variable, for example from -a Makefile at the root of the repository: - -.. code-block:: text - - repo/ - Makefile - sub/ (git submodule) - run.py - vc/ - models/ - vc_model.py - tb/ - tb_vc.vhd - -.. code-block:: make - - # repo/ is the base directory - sim: - VUNIT_PYTHON_BRIDGE_FILE_BASE=$(CURDIR) python sub/run.py - -The file names joined to ``base_path`` are then relative to the repository -root: - -.. code-block:: vhdl - - exec_file(join(base_path, "sub/vc/models/vc_model.py")); -- repo/sub/vc/models/vc_model.py - -base_path and the base directory -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -``base_path`` returns the base directory, absolute, for VHDL to join file -names to with ``join`` of VUnit's ``path`` package, which ``vunit_context`` -includes, the same way as ``tb_path(runner_cfg)``. It is a function without -arguments: the base directory is a constant of a package generated when the -package is set up, so it can be used at any time, before ``test_runner_setup`` -too. Without a base directory ``base_path`` is reported as a failure on -``python_logger``, naming both ways of setting it, and returns ``""``. -``base_path`` serves any file name passed to a Python model, not only those of -``exec_file`` and ``import_module_from_file``, whose own relative file names -are still taken from the directory of the testbench. - -``vunit_python_bridge.set_relative_file_base(directory)`` sets the base -directory in the run script. It must be called before -``vu.add_package("vunit-python-bridge", ...)``; a call after it raises a -``RuntimeError``. The directory, a ``str`` or a ``Path``, is resolved when the -function is called, a relative one against the current working directory, and -a directory that does not exist raises a ``ValueError``. - -The environment variable ``VUNIT_PYTHON_BRIDGE_FILE_BASE`` sets the base -directory without changing the run script. It is read when the package is set -up, during ``add_package``. A relative value is resolved against the current -working directory at that time, an empty value counts as unset, and a value -that is not an existing directory makes the setup fail with an error naming -the variable and the value. - -The variable takes precedence, so that whoever runs a shared run script can -override the directory it sets: - -========================== ==================== ========================== -``set_relative_file_base`` Environment variable Base directory -========================== ==================== ========================== -not called unset or empty none, ``base_path`` fails -called unset or empty that of the call -not called set that of the variable -called set that of the variable -========================== ==================== ========================== - -When both are set to different directories, compared after resolving them, -the setup logs a warning naming both and saying that the environment variable -takes precedence. - -.. _python_bridge:semantics: - -Semantics to be aware of -------------------------- - -.. important:: - - * ``eval``/``eval_`` are declared ``impure`` since they always read - state from the Python interpreter. - * Errors are reported as failures on the logger of the :ref:`session - `, including the Python traceback, rather than - aborting the simulation. - * ``python_setup`` and ``python_cleanup`` are optional, see - :ref:`python_bridge:setup_and_cleanup`. ``python_cleanup`` does not - finalize the interpreter. - * ``real`` results have no float32 range limit: VHDL ``real`` is double - precision where the Python bridge implements the API, so any finite - Python ``float`` (also double precision) is in range. A value that is not - finite, such as ``float("inf")`` or ``float("nan")``, has no VHDL ``real`` - representation and is reported as a failure. - * ``string`` arguments are passed to Python double-quoted verbatim: a quote - or backslash inside the string is not escaped, so it must be avoided or - already be valid inside a Python double-quoted string. - -.. _python_bridge:other_simulators: - -Other simulators ------------------ - -Riviera-PRO/Active-HDL (VHPI) implement ``python_ffi_pkg`` with a VHPI -application, built from the C sources in `src/vunit_python_bridge/native/vhpi -`__ -with their ``ccomp`` driver. On Windows, ``ccomp`` is given the directory of -the gcc bundled with the simulator (``/mingw``) or, when there is -none, of ``gcc`` on ``PATH``. The package builds the application under the -output path (``//libraries``) the first time it is -added and rebuilds it when the sources, the Python running VUnit or the -simulator installation change, so a run script needs nothing beyond -``add_package("vunit-python-bridge", allow_setup=True)``. - -This application differs from the Python bridge in a few ways: only the -default session exists, the operations implemented by the bridge -(``integer_array_t`` values, the ``boolean``, ``std_ulogic``, vector and -``integer_array_t`` results, ``exec_file``) report that they require NVC, GHDL -or Questa, a Python error stops the simulation with the message printed by the -application rather than through the logger of the session, and ``real`` values -outside the single precision float range are rejected. - -The ``embedded_python`` example of this repository, -`examples/embedded_python -`__, -is a complete example covering all three simulator families. - -Its ``tb_example.vhd`` has a test case for each part of the API: ``exec`` and -``eval`` with the types they convert, calls with positional, keyword and group -arguments, a 20 register status dump, wide -``arg_unsigned``/``arg_signed`` and ``std_ulogic`` argument values, a 2-D -``integer_array_t`` image transposed by NumPy, the result types of ``eval`` and -``call``, Python files executed with ``exec_file`` or imported with -``import_module_from_file``, two models loaded into a session each, and a -Python model failing with the logger of the default session mocked. Its last -test case drives ``python_model``, a verification component whose behaviour is -the Python function in ``python_model.py`` rather than VHDL. The component finds -that file by joining its name to ``base_path``, the directory of the run script, which -``run.py`` sets as the base directory, while the testbench gives the files it -loads itself relative to its own directory. See :ref:`python_bridge:file_paths`. - -.. _python_bridge:native: - -How it works (NVC, GHDL and Questa) ------------------------------------- - -For NVC, GHDL and Questa, the interpreter is embedded in the simulator process -by a small C library, the VUnit Python bridge (`src/vunit_python_bridge/native -`__). -NVC and GHDL call it through VHPIDIRECT; Questa/ModelSim calls it through the -FLI, using the front end in `native/fli.c -`__ -that converts the FLI parameters of one foreign subprogram per entry point. The generated VHDL -and everything above it is the same for all three. The interpreter is started -on first use, is never restarted within a simulation, and uses no signal -handlers of its own. - -Linux - The bridge is compiled from source against the Python running VUnit the - first time the package is added, and cached in the library directory, see - below. It is rebuilt automatically when the - source, the Python version or the Python installation changes. For Questa - the FLI front end is compiled in too, against the ``mti.h`` of the - simulator, and the simulator installation is part of the cache key. The - package ships no prebuilt Linux library. - -Windows - For NVC and GHDL, the package ships DLLs built with MSVC for each supported - Python minor version (``src/vunit_python_bridge/bin``). The matching DLL is - copied to the library directory, see below, and nothing is compiled. A development checkout of - the package does not contain the DLLs; they can be built with - ``tools/build_python_bridge.py`` from an MSVC developer prompt, and without - them the library is built with gcc like for Questa. For Questa the library - must be linked against the simulator's ``mtipli``, so it is always built on - first use with gcc, against the headers and the import library of the - Python running VUnit. The compiler is ``CC`` if set, else the MinGW gcc - bundled with the simulator (``gcc-*-mingw64*`` next to the Questa - executables' directory), else ``gcc`` on ``PATH``. Many Questa installations - on Windows bundle none. A gcc build imports the Python DLL directly, so the - package adds its directory to ``PATH`` for the simulator processes. These - builds are untested. - -Library directory - The libraries are built or copied to a directory of their own rather than to the - VUnit output path, which may not allow executing files: - ``~/.vunit-python-bridge`` in the home directory of the user, on every - platform (``%USERPROFILE%\.vunit-python-bridge`` on Windows). The environment - variable ``VUNIT_PYTHON_BRIDGE_LIBRARY_DIR`` selects another directory on - every platform. When the directory cannot be written, e.g. a read-only home - directory in a container, the setup fails and asks for the variable to be - set to a writable directory. - The setup also fails, asking for the variable to be set, when the simulator - could not load the library from the directory: on Linux and macOS when it is - on a file system mounted ``noexec``, and on Windows when an execution policy - (AppLocker, Windows Defender Application Control) blocks the library there. - Any other reason the library does not load, for example a missing - ``libpython``, fails the setup with the reason the system gives, which the - simulators leave out. The directory can be shared by projects since every - build has a directory named after what it was built from. The configuration - of a project stays in ``/python_bridge`` and the simulator hooks - pass its path to the library in ``VUNIT_PYTHON_BRIDGE_CONFIG``. - -The bridge uses the full (version specific) CPython ABI rather than the -Stable ABI since embedding the interpreter in the environment VUnit runs in -requires the ``PyConfig`` initialization API, which is not part of the -limited API. - -The package registers simulator hooks that make the simulator find the -library automatically. NVC is given a ``--load`` option. GHDL gets the library -directory in its dynamic library search path, plus a linker search path and an -rpath for the ahead-of-time compiled llvm and gcc backends, which the hook recognizes -from the ``backend`` of the GHDL interface it is called with. - -GHDL mcode and llvm-jit find the library only through that search path -(``LD_LIBRARY_PATH``), since GHDL limits the library name in a VHPIDIRECT -attribute to 32 characters, too short for an absolute path. A ``ghdl`` wrapper -script that resets the environment, or a simulation re-run by hand outside -VUnit, then fails with ``cannot load VHPIDIRECT shared library -'libvunit_python_bridge.so'``. Such a wrapper must keep ``LD_LIBRARY_PATH`` -and ``VUNIT_PYTHON_BRIDGE_CONFIG``. The llvm and gcc backends find the library -through the rpath, but they also need ``VUNIT_PYTHON_BRIDGE_CONFIG``. - -The FLI -attributes generated for Questa name the library by absolute path, and the -``vsim`` process VUnit starts is given ``-noautoldlibpath`` on Linux so that -the C++ runtime Questa bundles, which is often older than the one the Python -extension modules of the environment (NumPy) were built against, does not take -precedence over the one of the system. That option is set up when the process -starts, so it has to go on the command line of the process rather than on the -``vsim`` command of the do-file VUnit generates, and it is only passed to a -``vsim`` listing it in ``vsim -help all``. No simulator options need to be set -by the user. - -Tested configurations -~~~~~~~~~~~~~~~~~~~~~~ - -========================== ======================================================== -Simulator Linux -========================== ======================================================== -NVC 1.22 (and 1.23-devel) -GHDL mcode 6.0.0, 7.0.0-dev -GHDL llvm-jit 6.0.0, 7.0.0-dev -GHDL llvm 6.0.0, 7.0.0-dev -GHDL gcc 6.0.0, 7.0.0-dev -Questa (FLI) Altera Starter FPGA Edition 2025.3 -========================== ======================================================== - -On Windows, CI tests NVC 1.22 and GHDL mcode (nightly) with Python 3.10, 3.12, -3.14 and 3.15. The released wheel is also installed and tested on Linux, macOS and on -Windows machines without a compiler, there with every supported Python, so -each of its prebuilt DLLs is run. - -Questa is tested manually on Linux and is not covered by CI; the Windows build -of its bridge library is untested. Riviera-PRO/Active-HDL (VHPI) is not tested -at all. From 32ee6645bac63b33c75b287683a63944e698ec8d Mon Sep 17 00:00:00 2001 From: Sebastian Abrahamsson Date: Wed, 14 Oct 2026 10:57:05 +0200 Subject: [PATCH 3/7] Add a VHDL API reference A page per package of python_context, generated from the package declarations by docs/vhdl_api.py so that it follows the code: every public declaration with the comment above it, under the sections of the source, and a linked list of the names at the top. Names starting with p_ and private sections are left out. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017otfceeeQ9FrS2sXbyLou2 --- .gitignore | 1 + docs/conf.py | 3 + docs/index.rst | 10 +++ docs/vhdl_api.py | 155 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 169 insertions(+) create mode 100644 docs/vhdl_api.py diff --git a/.gitignore b/.gitignore index 619745e..badea8c 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ __pycache__/ /src/vunit_python_bridge/bin/ vunit_out/ /docs/release_notes.rst +/docs/api/ diff --git a/docs/conf.py b/docs/conf.py index 7447e8d..b80aaed 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -18,6 +18,7 @@ ROOT = Path(__file__).parent.parent sys.path.insert(0, str(ROOT / "docs")) +import vhdl_api # noqa: E402 project = "vunit-python-bridge" copyright = "2014-2026, Lars Asplund" # pylint: disable=redefined-builtin @@ -52,3 +53,5 @@ _notes += [_title, "-" * len(_title), "", f".. include:: ../release_notes/{_path.name}"] _notes += [" :parser: myst_parser.sphinx_", ""] (ROOT / "docs" / "release_notes.rst").write_text("\n".join(_notes)) + +vhdl_api.write(ROOT / "docs" / "api") diff --git a/docs/index.rst b/docs/index.rst index 6e2b44c..14d5402 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -43,6 +43,8 @@ Key features verification components alike. * :doc:`Python errors ` are VUnit failures with the traceback, on a logger that a test can mock. +* A complete :doc:`VHDL API reference `, generated from the package + declarations. * No build step: the package builds the foreign language interface for the simulator itself, see :doc:`how_it_works`. @@ -112,6 +114,14 @@ become available through ``python_context``: python_files file_paths +.. toctree:: + :caption: VHDL API reference + :hidden: + + api/python_context + api/python_ffi_pkg + api/python_pkg + .. toctree:: :caption: Simulators :hidden: diff --git a/docs/vhdl_api.py b/docs/vhdl_api.py new file mode 100644 index 0000000..1a175ee --- /dev/null +++ b/docs/vhdl_api.py @@ -0,0 +1,155 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. +# +# Copyright (c) 2014-2026, Lars Asplund lars.anders.asplund@gmail.com + +""" +The VHDL API reference, generated from the package declarations so that it follows the code. + +A declaration is documented by the comment right above it. A comment between two lines of +dashes starts a section, titled by its first line. Names starting with p_ and sections +titled "Private" are left out. +""" + +import re +from pathlib import Path + +HDL = Path(__file__).parent.parent / "src" / "vunit_python_bridge" / "hdl" / "src" + +# The documented packages: name, file, introduction +PACKAGES = [ + ( + "python_ffi_pkg", + "python_ffi_pkg_bridge.vhd", + "Sessions, ``exec`` and the basic ``eval`` result types. The package is implemented for " + "the selected simulator: by the Python bridge for NVC, GHDL and Questa and by a VHPI " + "application for Riviera-PRO/Active-HDL, which declares the same API, see " + ":doc:`../other_simulators`.", + ), + ( + "python_pkg", + "python_pkg.vhd", + "Arguments, ``call``, the result types implemented by the Python bridge, Python files " + "and modules, and ``base_path``.", + ), +] + +BANNER = re.compile(r"^\s*-{10,}\s*$") +PRIVATE_DECLARATION = re.compile(r"^\s*(constant|signal|variable|type|subtype|alias)\s+p_\w+") +DECLARED_NAME = re.compile( + r'^\s*(?:impure\s+|pure\s+)?(?:function|procedure|type|subtype|constant|alias)\s+("[^"]+"|\w+)' +) +PRIVATE_SUBPROGRAM = re.compile(r"^\s*(impure\s+|pure\s+)?(function|procedure)\s+p_\w+") + + +def _declarations(path, package): + """ + The comment and code chunks of the package declaration, separated by blank lines. + """ + lines = path.read_text(encoding="utf-8").splitlines() + start = next(idx for idx, line in enumerate(lines) if re.match(rf"package {package} is\b", line)) + end = next(idx for idx in range(start, len(lines)) if re.match(r"end package", lines[idx])) + chunks, chunk = [], [] + for line in lines[start + 1 : end]: + if line.strip(): + chunk.append(line[2:] if line.startswith(" ") else line) + elif chunk: + chunks.append(chunk) + chunk = [] + return chunks + [chunk] if chunk else chunks + + +def _comment_text(lines): + """ + Comment lines as reStructuredText, an indented comment line being a code example. + """ + out, in_code = [], False + for line in lines: + text = line.strip()[2:] + text = text[1:] if text.startswith(" ") else text + code = text.startswith(" ") + if code and not in_code: + out += ["", ".. code-block:: vhdl", ""] + elif not code and in_code: + out.append("") + in_code = code + out.append(f" {text}" if code else re.sub(r"([\\*`|_])", r"\\\1", text)) + return out + [""] + + +def _public_code(code): + """ + The code of a chunk without its private declarations, which end at the first ; outside + parentheses. + """ + out, skipping, depth = [], False, 0 + for line in code: + skipping = skipping or bool(PRIVATE_DECLARATION.match(line) or PRIVATE_SUBPROGRAM.match(line)) + if not skipping: + out.append(line) + continue + depth += line.count("(") - line.count(")") + skipping = not (depth == 0 and line.rstrip().endswith(";")) + return out + + +def package_page(package, file_name, introduction): + """ + The reference page of a package. + """ + body, names, private = [], {}, False + for chunk in _declarations(HDL / file_name, package): + comment = [line for line in chunk if line.lstrip().startswith("--")] + code = [line for line in chunk if not line.lstrip().startswith("--")] + if BANNER.match(chunk[0]): + banners = [idx for idx, line in enumerate(chunk) if BANNER.match(line)] + heading = chunk[banners[0] + 1].strip()[2:].strip() + private = heading.startswith("Private") + if private: + continue + body += [heading, "-" * len(heading), ""] + comment = [line for line in chunk[banners[1] + 1 :] if line.lstrip().startswith("--")] + if private: + continue + code = _public_code(code) + # A target for the first declaration of every name, which the contents link to + for line in code: + match = DECLARED_NAME.match(line) + if match and match.group(1) not in names: + names[match.group(1)] = f"{package}-{len(names)}" + body += [f".. _{names[match.group(1)]}:", ""] + if comment: + body += _comment_text(comment) + if code: + body += [".. code-block:: vhdl", ""] + [f" {line}" for line in code] + [""] + contents = " ยท ".join(f":ref:`{name} <{label}>`" for name, label in sorted(names.items())) + title = package + out = [title, "=" * len(title), "", introduction, "", f"Declared in ``{file_name}``.", ""] + return "\n".join(out + ["**Contents:** " + contents.replace("*", "\\*"), ""] + body) + + +def context_page(): + """ + The reference page of python_context. + """ + text = (HDL / "python_context.vhd").read_text(encoding="utf-8") + context = text[text.index("context python_context") :].strip() + packages = ", ".join(f":doc:`{package}`" for package, _, _ in PACKAGES) + out = ["python_context", "==============", ""] + out += [f"The context that makes the whole API visible: {packages}.", ""] + out += [".. code-block:: vhdl", ""] + [f" {line}" for line in context.splitlines()] + [""] + out += ["A testbench uses it next to ``vunit_context``:", "", ".. code-block:: vhdl", ""] + out += [" library vunit_lib;", " context vunit_lib.vunit_context;", ""] + out += [" library python_bridge;", " context python_bridge.python_context;", ""] + return "\n".join(out) + + +def write(directory): + """ + Write the reference pages to directory. + """ + directory.mkdir(exist_ok=True) + (directory / "python_context.rst").write_text(context_page(), encoding="utf-8") + for package, file_name, introduction in PACKAGES: + (directory / f"{package}.rst").write_text(package_page(package, file_name, introduction), encoding="utf-8") From c1385a279b7fd98803b26fa66a466cef050728f9 Mon Sep 17 00:00:00 2001 From: Sebastian Abrahamsson Date: Wed, 14 Oct 2026 10:57:42 +0200 Subject: [PATCH 4/7] Title the release notes of the next version Unreleased Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017otfceeeQ9FrS2sXbyLou2 --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index b80aaed..c137c1d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -49,7 +49,7 @@ # The release notes page, generated from release_notes/*.md, newest first _notes = ["Release notes", "=============", ""] for _path in sorted((ROOT / "release_notes").glob("*.md"), key=lambda path: Version(path.stem), reverse=True): - _title = _path.stem if Version(_path.stem) <= Version(release) else f"{_path.stem} (unreleased)" + _title = _path.stem if Version(_path.stem) <= Version(release) else "Unreleased" _notes += [_title, "-" * len(_title), "", f".. include:: ../release_notes/{_path.name}"] _notes += [" :parser: myst_parser.sphinx_", ""] (ROOT / "docs" / "release_notes.rst").write_text("\n".join(_notes)) From 4f9d000da6d92054fd9607b2d7c92f514fe1475a Mon Sep 17 00:00:00 2001 From: Sebastian Abrahamsson Date: Wed, 14 Oct 2026 10:59:41 +0200 Subject: [PATCH 5/7] Add the badges of the landing page to the README Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017otfceeeQ9FrS2sXbyLou2 --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index f4d42f0..d9df9cf 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,12 @@ # vunit-python-bridge +[![Website](https://img.shields.io/readthedocs/vunit-python-bridge?label=website)](https://vunit-python-bridge.readthedocs.io) +[![Repository](https://img.shields.io/badge/github-VUnit%2Fvunit--python--bridge-blue?logo=github)](https://github.com/VUnit/vunit-python-bridge) +[![PyPI](https://img.shields.io/pypi/v/vunit-python-bridge?label=pip%20install%20vunit-python-bridge&color=blue)](https://pypi.org/project/vunit-python-bridge/) +[![Python versions](https://img.shields.io/badge/python-3.10%20%E2%80%93%203.15-blue?logo=python&logoColor=white)](https://vunit-python-bridge.readthedocs.io/en/latest/requirements.html) +[![License](https://img.shields.io/badge/license-MPL--2.0-blue)](https://github.com/VUnit/vunit-python-bridge/blob/main/LICENSE) +[![CI status](https://github.com/VUnit/vunit-python-bridge/actions/workflows/python_bridge.yml/badge.svg?branch=main)](https://github.com/VUnit/vunit-python-bridge/actions/workflows/python_bridge.yml) + A VUnit package making Python callable from VHDL. ## Overview From 6743977efb052b6ac0119b3d80fa81902af5cd2d Mon Sep 17 00:00:00 2001 From: Sebastian Abrahamsson Date: Wed, 14 Oct 2026 11:02:48 +0200 Subject: [PATCH 6/7] Label the documentation badge documentation Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017otfceeeQ9FrS2sXbyLou2 --- README.md | 2 +- docs/index.rst | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d9df9cf..349728a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # vunit-python-bridge -[![Website](https://img.shields.io/readthedocs/vunit-python-bridge?label=website)](https://vunit-python-bridge.readthedocs.io) +[![Documentation](https://img.shields.io/readthedocs/vunit-python-bridge?label=documentation)](https://vunit-python-bridge.readthedocs.io) [![Repository](https://img.shields.io/badge/github-VUnit%2Fvunit--python--bridge-blue?logo=github)](https://github.com/VUnit/vunit-python-bridge) [![PyPI](https://img.shields.io/pypi/v/vunit-python-bridge?label=pip%20install%20vunit-python-bridge&color=blue)](https://pypi.org/project/vunit-python-bridge/) [![Python versions](https://img.shields.io/badge/python-3.10%20%E2%80%93%203.15-blue?logo=python&logoColor=white)](https://vunit-python-bridge.readthedocs.io/en/latest/requirements.html) diff --git a/docs/index.rst b/docs/index.rst index 14d5402..d58a554 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -5,8 +5,8 @@ About vunit-python-bridge |docs| |repository| |pypi| |python| |license| |ci| -.. |docs| image:: https://img.shields.io/readthedocs/vunit-python-bridge?label=website - :alt: Website +.. |docs| image:: https://img.shields.io/readthedocs/vunit-python-bridge?label=documentation + :alt: Documentation :target: https://vunit-python-bridge.readthedocs.io .. |repository| image:: https://img.shields.io/badge/github-VUnit%2Fvunit--python--bridge-blue?logo=github :alt: Repository From 59f52a3793dba680f743f4af1cac5845903bd40b Mon Sep 17 00:00:00 2001 From: Sebastian Abrahamsson Date: Wed, 14 Oct 2026 11:03:53 +0200 Subject: [PATCH 7/7] Shorten the documentation section of the README to a link Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017otfceeeQ9FrS2sXbyLou2 --- README.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/README.md b/README.md index 349728a..e7d6582 100644 --- a/README.md +++ b/README.md @@ -95,10 +95,7 @@ environment and its installed packages. ## Documentation -The documentation is at [vunit-python-bridge.readthedocs.io](https://vunit-python-bridge.readthedocs.io): sessions, `exec`, `eval`, `call` -and its argument forms, `exec_file`, `import_run_script`, the type mapping, `integer_array_t` and -NumPy, error reporting, and how the bridge works. A complete example covering all three simulator -families is in [examples/embedded_python](examples/embedded_python). +The documentation is at [vunit-python-bridge.readthedocs.io](https://vunit-python-bridge.readthedocs.io). ## License