From b36e497ce251bf5599cf1bfec6679ff1eabffaa2 Mon Sep 17 00:00:00 2001 From: Sebastian Abrahamsson Date: Thu, 29 Oct 2026 14:07:31 +0100 Subject: [PATCH 01/10] Support yavs through VHPIDIRECT yavs calls a VHPIDIRECT foreign subprogram by the last word of its attribute and loads libraries given with --load, like NVC. The bridge serves it with the NVC attributes, a --load run flag and the configuration in the run environment. yavs is provided by the vunit-yavs package. Its default source encoding takes a non-ASCII character of a UTF-8 source as one character, not as UTF-8 bytes, which the docs point out. Co-Authored-By: Claude Opus 5.5 --- README.md | 3 ++- docs/how_it_works.rst | 14 ++++++++++-- docs/index.rst | 1 + docs/requirements.rst | 2 +- release_notes/news.d/41.feature.md | 1 + src/vunit_python_bridge/__init__.py | 9 ++++---- src/vunit_python_bridge/bridge.py | 6 ++--- .../hdl/src/python_bridge_pkg.vhd.in | 2 +- src/vunit_python_bridge/native_library.py | 8 ++++--- src/vunit_python_bridge/simulator_hooks.py | 3 ++- tests/test_python_bridge.py | 22 ++++++++++++++----- 11 files changed, 49 insertions(+), 22 deletions(-) create mode 100644 release_notes/news.d/41.feature.md diff --git a/README.md b/README.md index 16eeb07..239eb4e 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ can execute Python code and call Python functions — a NumPy reference model, a plot of what the design just produced — without leaving the simulation. The VHDL API, `python_pkg`/`python_context`, is compiled into the `python_bridge` library and is implemented for the selected simulator by a small C library the package builds itself, called through VHPIDIRECT -(NVC, GHDL), the FLI (Questa/ModelSim) or VHPI (Riviera-PRO). Values cross the interface with their +(NVC, GHDL, yavs), the FLI (Questa/ModelSim) or VHPI (Riviera-PRO). Values cross the interface with their VHDL types: `integer`, `real`, `string`, `boolean`, `std_ulogic`, `unsigned`/`signed`, the vector types, `integer_array_t` as a NumPy array, and VUnit's `dict_t` as a Python `dict`. A `python_object_t` is an instance of a Python class owned by VHDL, for example the backend of a @@ -74,6 +74,7 @@ check(call_integer_vector("gain", arg(integer_vector'(1, 2))) = integer_vector'( | --- | --- | --- | | NVC | VHPIDIRECT | Tested on Linux, macOS and Windows | | GHDL (mcode, llvm-jit, llvm, gcc) | VHPIDIRECT | Tested on Linux, macOS and Windows | +| yavs | VHPIDIRECT | Tested manually on Linux | | Questa/ModelSim | FLI | Tested manually on Linux; the Windows build is untested | | Riviera-PRO | VHPI | Tested manually on Linux and Windows | diff --git a/docs/how_it_works.rst b/docs/how_it_works.rst index 2572c67..812da24 100644 --- a/docs/how_it_works.rst +++ b/docs/how_it_works.rst @@ -6,7 +6,7 @@ How it works 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 +NVC, GHDL and yavs 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. @@ -75,7 +75,7 @@ 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 +library automatically. NVC and yavs are 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. @@ -100,6 +100,15 @@ starts, so it has to go on the command line of the process rather than on the ``vsim`` listing it in ``vsim -help all``. No simulator options need to be set by the user. +yavs is provided by the ``vunit-yavs`` package, which the run script adds before +the VUnit builtins, ``vu.add_package("vunit-yavs", allow_setup=True)``, so that +``VUNIT_SIMULATOR=yavs`` can be selected. The bridge takes the characters of a +VHDL string as UTF-8 bytes, which is what NVC and GHDL make of a UTF-8 source +file, one character per byte. yavs reads it as one character per code point +by default, so a non-ASCII character of a string literal fails in Python unless +the sources are analyzed and run with ``--encoding=latin1``, for example with +the ``yavs.global_flags`` compile and simulation options. + The VHPI attributes generated for Riviera-PRO name the library by absolute path without its suffix, which the simulator adds. The attribute separates its parts by spaces, so the setup fails, asking for @@ -120,6 +129,7 @@ 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 Riviera-PRO (VHPI) 2026.04 +yavs 0.10.0, tested manually ========================== ======================================================== On Windows, CI tests NVC 1.22 and GHDL mcode (nightly) with Python 3.10, 3.12, diff --git a/docs/index.rst b/docs/index.rst index abc2cd2..eb10984 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -58,6 +58,7 @@ Simulator Interface Status ================================= ========== ======================================================== NVC VHPIDIRECT Tested on Linux, macOS and Windows GHDL (mcode, llvm-jit, llvm, gcc) VHPIDIRECT Tested on Linux, macOS and Windows +yavs VHPIDIRECT Tested manually on Linux Questa/ModelSim FLI Tested manually on Linux; the Windows build is untested Riviera-PRO VHPI Tested manually on Linux and Windows ================================= ========== ======================================================== diff --git a/docs/requirements.rst b/docs/requirements.rst index cf0a35e..e9fe423 100644 --- a/docs/requirements.rst +++ b/docs/requirements.rst @@ -3,7 +3,7 @@ Requirements * A VUnit with support for packages and simulator hooks, see the `README `__. -* NVC, GHDL, Questa/ModelSim or Riviera-PRO, see :ref:`python_bridge:native`. +* NVC, GHDL, yavs, Questa/ModelSim or Riviera-PRO, see :ref:`python_bridge:native`. Any other simulator raises a ``RuntimeError``. * VHDL-2008 or later. * CPython 3.10 or later with the standard (GIL) build. Free-threaded builds diff --git a/release_notes/news.d/41.feature.md b/release_notes/news.d/41.feature.md new file mode 100644 index 0000000..2872326 --- /dev/null +++ b/release_notes/news.d/41.feature.md @@ -0,0 +1 @@ +yavs is a supported simulator. diff --git a/src/vunit_python_bridge/__init__.py b/src/vunit_python_bridge/__init__.py index eaf8a39..3db9227 100644 --- a/src/vunit_python_bridge/__init__.py +++ b/src/vunit_python_bridge/__init__.py @@ -13,7 +13,7 @@ language interface implementing them for the selected simulator: the VUnit Python bridge, a small C library (native/*.c) embedding CPython in the simulator -process, called through VHPIDIRECT (NVC, GHDL), the FLI (Questa/ModelSim) or VHPI +process, called through VHPIDIRECT (NVC, GHDL, yavs), the FLI (Questa/ModelSim) or VHPI (Riviera-PRO). Modules: @@ -42,17 +42,18 @@ # The foreign language interface implementing the package for a simulator. VUnit says which # simulator was selected, the package says how it is served: NVC and GHDL both go through -# VHPIDIRECT but bind the library differently, Questa/ModelSim goes through the FLI and -# Riviera-PRO through VHPI. +# VHPIDIRECT but bind the library differently, yavs binds it like NVC, Questa/ModelSim goes +# through the FLI and Riviera-PRO through VHPI. FOREIGN_LANGUAGE_INTERFACES = { "nvc": "VHPIDIRECT_NVC", "ghdl": "VHPIDIRECT_GHDL", + "yavs": "VHPIDIRECT_NVC", "modelsim": "FLI", "rivierapro": "VHPI", } # The simulators of the table, named as a user knows them -SUPPORTED_SIMULATORS = "NVC, GHDL, Questa/ModelSim or Riviera-PRO" +SUPPORTED_SIMULATORS = "NVC, GHDL, yavs, Questa/ModelSim or Riviera-PRO" def setup(context): diff --git a/src/vunit_python_bridge/bridge.py b/src/vunit_python_bridge/bridge.py index 8f6d60a..f2fbb50 100644 --- a/src/vunit_python_bridge/bridge.py +++ b/src/vunit_python_bridge/bridge.py @@ -34,7 +34,7 @@ # The simulators the bridge serves, reaching it through VHPIDIRECT, the FLI and VHPI. FLI_SIMULATORS = ("modelsim",) VHPI_SIMULATORS = ("rivierapro",) -BRIDGE_SIMULATORS = ("nvc", "ghdl") + FLI_SIMULATORS + VHPI_SIMULATORS +BRIDGE_SIMULATORS = ("nvc", "ghdl", "yavs") + FLI_SIMULATORS + VHPI_SIMULATORS # GHDL backends that link the design ahead of time. For these the library # token in the VHPIDIRECT attribute is passed to the linker instead of being @@ -77,7 +77,7 @@ def setup( """ if simulator_name is not None and simulator_name not in BRIDGE_SIMULATORS: raise PythonBridgeError( - f"The vunit-python-bridge package requires NVC, GHDL, Questa/ModelSim or Riviera-PRO, " + f"The vunit-python-bridge package requires NVC, GHDL, yavs, Questa/ModelSim or Riviera-PRO, " f"it is not supported for {simulator_name}" ) @@ -105,7 +105,7 @@ def setup( # and the library, by absolute path since Questa accepts it, for the FLI; the library by # absolute path without its suffix, which Riviera-PRO adds, and the entry point registered # by native/vhpi.c for VHPI; the VHPIDIRECT library token and the entry point itself for - # NVC and GHDL. + # NVC, GHDL and yavs. if is_fli: foreign = f"fli_{{entry_point}} {library_file!s}" elif is_vhpi: diff --git a/src/vunit_python_bridge/hdl/src/python_bridge_pkg.vhd.in b/src/vunit_python_bridge/hdl/src/python_bridge_pkg.vhd.in index b8aa051..11eebce 100644 --- a/src/vunit_python_bridge/hdl/src/python_bridge_pkg.vhd.in +++ b/src/vunit_python_bridge/hdl/src/python_bridge_pkg.vhd.in @@ -8,7 +8,7 @@ -- -- This file is a template. The setup function of the package substitutes every -- {foreign:} placeholder with the foreign attribute string of the --- selected simulator -- "VHPIDIRECT " for NVC and GHDL, +-- selected simulator -- "VHPIDIRECT " for NVC, GHDL and yavs, -- "fli_ " for the FLI of Questa/ModelSim -- and adds the -- result to the python_bridge library, together with a generated package body -- whose subprograms are never executed. The public API is diff --git a/src/vunit_python_bridge/native_library.py b/src/vunit_python_bridge/native_library.py index 2c51fef..5017ddd 100644 --- a/src/vunit_python_bridge/native_library.py +++ b/src/vunit_python_bridge/native_library.py @@ -191,9 +191,11 @@ def check_windows_64bit_simulator(simulator_name: str, simulator_prefix: Optiona _check_windows_python() if simulator_prefix is None: return - # NVC and GHDL name their executable, which VUnit lets the NVC and GHDL environment variables - # override, the Questa/ModelSim and Aldec interfaces use vsim - name = os.environ.get(simulator_name.upper(), simulator_name) if simulator_name in ("nvc", "ghdl") else "vsim" + # NVC, GHDL and yavs name their executable, which their VUnit interfaces let the NVC, GHDL and YAVS + # environment variables override, the Questa/ModelSim and Aldec interfaces use vsim + name = "vsim" + if simulator_name in ("nvc", "ghdl", "yavs"): + name = os.environ.get(simulator_name.upper(), simulator_name) executable = Path(simulator_prefix) / (name if name.lower().endswith(".exe") else f"{name}.exe") machine = pe_machine(executable) if machine is not None and machine != _PE_MACHINE_X64: diff --git a/src/vunit_python_bridge/simulator_hooks.py b/src/vunit_python_bridge/simulator_hooks.py index e68af8e..278f772 100644 --- a/src/vunit_python_bridge/simulator_hooks.py +++ b/src/vunit_python_bridge/simulator_hooks.py @@ -37,6 +37,7 @@ def register(context, bridge: PythonBridge) -> None: Register the hooks of every simulator the bridge serves with the package context. """ context.register_simulator_hooks("nvc", run_flags=_nvc_run_flags(bridge), run_env=_config_run_env(bridge)) + context.register_simulator_hooks("yavs", run_flags=_nvc_run_flags(bridge), run_env=_config_run_env(bridge)) context.register_simulator_hooks( "ghdl", elab_flags=_ghdl_elab_flags(bridge), @@ -66,7 +67,7 @@ def hook(simulator_interface, env: Dict[str, str]) -> Dict[str, str]: # pylint: def _nvc_run_flags(bridge: PythonBridge): """ - NVC run (-r) flags loading the bridge. + NVC run (-r) and yavs run flags loading the bridge. """ def hook(simulator_interface) -> List[str]: # pylint: disable=unused-argument diff --git a/tests/test_python_bridge.py b/tests/test_python_bridge.py index 8e781f9..5bf9a5a 100644 --- a/tests/test_python_bridge.py +++ b/tests/test_python_bridge.py @@ -269,6 +269,7 @@ def test_every_supported_simulator_has_its_interface(self): { "nvc": "VHPIDIRECT_NVC", "ghdl": "VHPIDIRECT_GHDL", + "yavs": "VHPIDIRECT_NVC", "modelsim": "FLI", "rivierapro": "VHPI", }, @@ -281,7 +282,7 @@ def test_the_simulators_the_bridge_serves_are_the_ones_it_is_set_up_for(self): ) def test_the_message_names_every_simulator(self): - for simulator in ("NVC", "GHDL", "Questa/ModelSim", "Riviera-PRO"): + for simulator in ("NVC", "GHDL", "yavs", "Questa/ModelSim", "Riviera-PRO"): self.assertIn(simulator, vunit_python_bridge.SUPPORTED_SIMULATORS) @@ -361,6 +362,7 @@ def _check_bridge_files_added(self, simulator, backend=None): def test_vhpidirect_adds_the_bridge_files(self): self._check_bridge_files_added(self._simulator("nvc")) self._check_bridge_files_added(self._simulator("ghdl"), backend="mcode") + self._check_bridge_files_added(self._simulator("yavs")) def test_fli_adds_the_bridge_files(self): # Questa/ModelSim is served by the bridge too, through native/fli.c. @@ -382,13 +384,14 @@ def test_vhpi_simulators_are_told_the_configuration_by_the_environment(self): def test_hooks_are_registered_for_every_simulator_the_bridge_serves(self): context = self._check_bridge_files_added(self._simulator("nvc")) - self.assertEqual(sorted(context.hooks), ["ghdl", "modelsim", "nvc"]) + self.assertEqual(sorted(context.hooks), ["ghdl", "modelsim", "nvc", "yavs"]) self.assertIsNotNone(context.hooks["nvc"]["run_flags"]) + self.assertIsNotNone(context.hooks["yavs"]["run_flags"]) self.assertIsNotNone(context.hooks["ghdl"]["elab_flags"]) self.assertIsNotNone(context.hooks["ghdl"]["run_env"]) self.assertIsNotNone(context.hooks["modelsim"]["process_flags"]) # Every simulator is told where the configuration of the project is - for simulator in ("nvc", "ghdl", "modelsim"): + for simulator in ("nvc", "ghdl", "yavs", "modelsim"): self.assertIsNotNone(context.hooks[simulator]["run_env"], simulator) def test_bridge_setup_failure_is_reported(self): @@ -602,7 +605,7 @@ def test_variable_is_not_read_at_import(self): self.assertIsNone(file_base._relative_file_base) # pylint: disable=protected-access def test_bridge_and_vhpi_add_the_package(self): - for name in ("nvc", "ghdl", "modelsim", "rivierapro"): + for name in ("nvc", "ghdl", "yavs", "modelsim", "rivierapro"): self._reset() context, package = self._setup(name) self.assertIn(("python_bridge", package), context.added_files, name) @@ -649,6 +652,7 @@ def test_token_is_the_file_name_or_the_link_flag_of_the_backend(self): # Only the GHDL backends linking ahead of time get the linker flag for simulator_name, backend, token in ( ("nvc", None, "libvunit_python_bridge.so"), + ("yavs", None, "libvunit_python_bridge.so"), ("ghdl", "mcode", "libvunit_python_bridge.so"), ("ghdl", "llvm-jit", "libvunit_python_bridge.so"), ("ghdl", "llvm", "-lvunit_python_bridge"), @@ -1408,7 +1412,7 @@ def test_rejects_32_bit_python(self): class TestSimulatorHooks(unittest.TestCase): """ - The hooks registered with the package context: what NVC, GHDL and Questa/ModelSim + The hooks registered with the package context: what NVC, GHDL, yavs and Questa/ModelSim are given for a simulation of the project. """ @@ -1416,7 +1420,7 @@ class TestSimulatorHooks(unittest.TestCase): def test_every_simulator_is_told_where_the_configuration_is(self): hooks = self._hooks(Path("/some/dir/libvunit_python_bridge.so")) - for simulator in ("nvc", "ghdl", "modelsim"): + for simulator in ("nvc", "ghdl", "yavs", "modelsim"): env = {"KEPT": "1"} result = hooks[simulator]["run_env"](self._ghdl("mcode"), env) self.assertEqual(result[simulator_hooks.CONFIG_FILE_VARIABLE], str(self.config_file), simulator) @@ -1458,6 +1462,12 @@ def test_nvc_loads_the_library_at_run_time(self): # NVC dlopen()s the library itself, nothing to do at elaboration self.assertIsNone(hooks["nvc"]["elab_flags"]) + def test_yavs_loads_the_library_at_run_time(self): + library_file = Path("/some/dir/libvunit_python_bridge.so") + hooks = self._hooks(library_file) + self.assertEqual(hooks["yavs"]["run_flags"](mock.Mock()), [f"--load={library_file!s}"]) + self.assertIsNone(hooks["yavs"]["elab_flags"]) + def test_ghdl_elab_flags_empty_for_mcode_and_jit(self): hooks = self._hooks(Path("/some/dir/libvunit_python_bridge.so")) for backend in ("mcode", "llvm-jit"): From 5e70ead37083b59f674c7e60b69e832648a7f79c Mon Sep 17 00:00:00 2001 From: Sebastian Abrahamsson Date: Thu, 29 Oct 2026 14:24:51 +0100 Subject: [PATCH 02/10] Drop the yavs entries of the simulator table yavs comes from a simulator package, so the bridge should not list it. The next commit lets a simulator package describe how it loads the bridge. This reverts commit b36e497ce251bf5599cf1bfec6679ff1eabffaa2. Co-Authored-By: Claude Opus 5.5 --- README.md | 3 +-- docs/how_it_works.rst | 14 ++---------- docs/index.rst | 1 - docs/requirements.rst | 2 +- release_notes/news.d/41.feature.md | 1 - src/vunit_python_bridge/__init__.py | 9 ++++---- src/vunit_python_bridge/bridge.py | 6 ++--- .../hdl/src/python_bridge_pkg.vhd.in | 2 +- src/vunit_python_bridge/native_library.py | 8 +++---- src/vunit_python_bridge/simulator_hooks.py | 3 +-- tests/test_python_bridge.py | 22 +++++-------------- 11 files changed, 22 insertions(+), 49 deletions(-) delete mode 100644 release_notes/news.d/41.feature.md diff --git a/README.md b/README.md index 239eb4e..16eeb07 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ can execute Python code and call Python functions — a NumPy reference model, a plot of what the design just produced — without leaving the simulation. The VHDL API, `python_pkg`/`python_context`, is compiled into the `python_bridge` library and is implemented for the selected simulator by a small C library the package builds itself, called through VHPIDIRECT -(NVC, GHDL, yavs), the FLI (Questa/ModelSim) or VHPI (Riviera-PRO). Values cross the interface with their +(NVC, GHDL), the FLI (Questa/ModelSim) or VHPI (Riviera-PRO). Values cross the interface with their VHDL types: `integer`, `real`, `string`, `boolean`, `std_ulogic`, `unsigned`/`signed`, the vector types, `integer_array_t` as a NumPy array, and VUnit's `dict_t` as a Python `dict`. A `python_object_t` is an instance of a Python class owned by VHDL, for example the backend of a @@ -74,7 +74,6 @@ check(call_integer_vector("gain", arg(integer_vector'(1, 2))) = integer_vector'( | --- | --- | --- | | NVC | VHPIDIRECT | Tested on Linux, macOS and Windows | | GHDL (mcode, llvm-jit, llvm, gcc) | VHPIDIRECT | Tested on Linux, macOS and Windows | -| yavs | VHPIDIRECT | Tested manually on Linux | | Questa/ModelSim | FLI | Tested manually on Linux; the Windows build is untested | | Riviera-PRO | VHPI | Tested manually on Linux and Windows | diff --git a/docs/how_it_works.rst b/docs/how_it_works.rst index 812da24..2572c67 100644 --- a/docs/how_it_works.rst +++ b/docs/how_it_works.rst @@ -6,7 +6,7 @@ How it works The interpreter is embedded in the simulator process by a small C library, the VUnit Python bridge (`src/vunit_python_bridge/native `__). -NVC, GHDL and yavs call it through VHPIDIRECT; Questa/ModelSim calls it through the +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. @@ -75,7 +75,7 @@ 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 and yavs are given a ``--load`` option. GHDL gets the library +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. @@ -100,15 +100,6 @@ starts, so it has to go on the command line of the process rather than on the ``vsim`` listing it in ``vsim -help all``. No simulator options need to be set by the user. -yavs is provided by the ``vunit-yavs`` package, which the run script adds before -the VUnit builtins, ``vu.add_package("vunit-yavs", allow_setup=True)``, so that -``VUNIT_SIMULATOR=yavs`` can be selected. The bridge takes the characters of a -VHDL string as UTF-8 bytes, which is what NVC and GHDL make of a UTF-8 source -file, one character per byte. yavs reads it as one character per code point -by default, so a non-ASCII character of a string literal fails in Python unless -the sources are analyzed and run with ``--encoding=latin1``, for example with -the ``yavs.global_flags`` compile and simulation options. - The VHPI attributes generated for Riviera-PRO name the library by absolute path without its suffix, which the simulator adds. The attribute separates its parts by spaces, so the setup fails, asking for @@ -129,7 +120,6 @@ 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 Riviera-PRO (VHPI) 2026.04 -yavs 0.10.0, tested manually ========================== ======================================================== On Windows, CI tests NVC 1.22 and GHDL mcode (nightly) with Python 3.10, 3.12, diff --git a/docs/index.rst b/docs/index.rst index eb10984..abc2cd2 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -58,7 +58,6 @@ Simulator Interface Status ================================= ========== ======================================================== NVC VHPIDIRECT Tested on Linux, macOS and Windows GHDL (mcode, llvm-jit, llvm, gcc) VHPIDIRECT Tested on Linux, macOS and Windows -yavs VHPIDIRECT Tested manually on Linux Questa/ModelSim FLI Tested manually on Linux; the Windows build is untested Riviera-PRO VHPI Tested manually on Linux and Windows ================================= ========== ======================================================== diff --git a/docs/requirements.rst b/docs/requirements.rst index e9fe423..cf0a35e 100644 --- a/docs/requirements.rst +++ b/docs/requirements.rst @@ -3,7 +3,7 @@ Requirements * A VUnit with support for packages and simulator hooks, see the `README `__. -* NVC, GHDL, yavs, Questa/ModelSim or Riviera-PRO, see :ref:`python_bridge:native`. +* NVC, GHDL, Questa/ModelSim or Riviera-PRO, see :ref:`python_bridge:native`. Any other simulator raises a ``RuntimeError``. * VHDL-2008 or later. * CPython 3.10 or later with the standard (GIL) build. Free-threaded builds diff --git a/release_notes/news.d/41.feature.md b/release_notes/news.d/41.feature.md deleted file mode 100644 index 2872326..0000000 --- a/release_notes/news.d/41.feature.md +++ /dev/null @@ -1 +0,0 @@ -yavs is a supported simulator. diff --git a/src/vunit_python_bridge/__init__.py b/src/vunit_python_bridge/__init__.py index 3db9227..eaf8a39 100644 --- a/src/vunit_python_bridge/__init__.py +++ b/src/vunit_python_bridge/__init__.py @@ -13,7 +13,7 @@ language interface implementing them for the selected simulator: the VUnit Python bridge, a small C library (native/*.c) embedding CPython in the simulator -process, called through VHPIDIRECT (NVC, GHDL, yavs), the FLI (Questa/ModelSim) or VHPI +process, called through VHPIDIRECT (NVC, GHDL), the FLI (Questa/ModelSim) or VHPI (Riviera-PRO). Modules: @@ -42,18 +42,17 @@ # The foreign language interface implementing the package for a simulator. VUnit says which # simulator was selected, the package says how it is served: NVC and GHDL both go through -# VHPIDIRECT but bind the library differently, yavs binds it like NVC, Questa/ModelSim goes -# through the FLI and Riviera-PRO through VHPI. +# VHPIDIRECT but bind the library differently, Questa/ModelSim goes through the FLI and +# Riviera-PRO through VHPI. FOREIGN_LANGUAGE_INTERFACES = { "nvc": "VHPIDIRECT_NVC", "ghdl": "VHPIDIRECT_GHDL", - "yavs": "VHPIDIRECT_NVC", "modelsim": "FLI", "rivierapro": "VHPI", } # The simulators of the table, named as a user knows them -SUPPORTED_SIMULATORS = "NVC, GHDL, yavs, Questa/ModelSim or Riviera-PRO" +SUPPORTED_SIMULATORS = "NVC, GHDL, Questa/ModelSim or Riviera-PRO" def setup(context): diff --git a/src/vunit_python_bridge/bridge.py b/src/vunit_python_bridge/bridge.py index f2fbb50..8f6d60a 100644 --- a/src/vunit_python_bridge/bridge.py +++ b/src/vunit_python_bridge/bridge.py @@ -34,7 +34,7 @@ # The simulators the bridge serves, reaching it through VHPIDIRECT, the FLI and VHPI. FLI_SIMULATORS = ("modelsim",) VHPI_SIMULATORS = ("rivierapro",) -BRIDGE_SIMULATORS = ("nvc", "ghdl", "yavs") + FLI_SIMULATORS + VHPI_SIMULATORS +BRIDGE_SIMULATORS = ("nvc", "ghdl") + FLI_SIMULATORS + VHPI_SIMULATORS # GHDL backends that link the design ahead of time. For these the library # token in the VHPIDIRECT attribute is passed to the linker instead of being @@ -77,7 +77,7 @@ def setup( """ if simulator_name is not None and simulator_name not in BRIDGE_SIMULATORS: raise PythonBridgeError( - f"The vunit-python-bridge package requires NVC, GHDL, yavs, Questa/ModelSim or Riviera-PRO, " + f"The vunit-python-bridge package requires NVC, GHDL, Questa/ModelSim or Riviera-PRO, " f"it is not supported for {simulator_name}" ) @@ -105,7 +105,7 @@ def setup( # and the library, by absolute path since Questa accepts it, for the FLI; the library by # absolute path without its suffix, which Riviera-PRO adds, and the entry point registered # by native/vhpi.c for VHPI; the VHPIDIRECT library token and the entry point itself for - # NVC, GHDL and yavs. + # NVC and GHDL. if is_fli: foreign = f"fli_{{entry_point}} {library_file!s}" elif is_vhpi: diff --git a/src/vunit_python_bridge/hdl/src/python_bridge_pkg.vhd.in b/src/vunit_python_bridge/hdl/src/python_bridge_pkg.vhd.in index 11eebce..b8aa051 100644 --- a/src/vunit_python_bridge/hdl/src/python_bridge_pkg.vhd.in +++ b/src/vunit_python_bridge/hdl/src/python_bridge_pkg.vhd.in @@ -8,7 +8,7 @@ -- -- This file is a template. The setup function of the package substitutes every -- {foreign:} placeholder with the foreign attribute string of the --- selected simulator -- "VHPIDIRECT " for NVC, GHDL and yavs, +-- selected simulator -- "VHPIDIRECT " for NVC and GHDL, -- "fli_ " for the FLI of Questa/ModelSim -- and adds the -- result to the python_bridge library, together with a generated package body -- whose subprograms are never executed. The public API is diff --git a/src/vunit_python_bridge/native_library.py b/src/vunit_python_bridge/native_library.py index 5017ddd..2c51fef 100644 --- a/src/vunit_python_bridge/native_library.py +++ b/src/vunit_python_bridge/native_library.py @@ -191,11 +191,9 @@ def check_windows_64bit_simulator(simulator_name: str, simulator_prefix: Optiona _check_windows_python() if simulator_prefix is None: return - # NVC, GHDL and yavs name their executable, which their VUnit interfaces let the NVC, GHDL and YAVS - # environment variables override, the Questa/ModelSim and Aldec interfaces use vsim - name = "vsim" - if simulator_name in ("nvc", "ghdl", "yavs"): - name = os.environ.get(simulator_name.upper(), simulator_name) + # NVC and GHDL name their executable, which VUnit lets the NVC and GHDL environment variables + # override, the Questa/ModelSim and Aldec interfaces use vsim + name = os.environ.get(simulator_name.upper(), simulator_name) if simulator_name in ("nvc", "ghdl") else "vsim" executable = Path(simulator_prefix) / (name if name.lower().endswith(".exe") else f"{name}.exe") machine = pe_machine(executable) if machine is not None and machine != _PE_MACHINE_X64: diff --git a/src/vunit_python_bridge/simulator_hooks.py b/src/vunit_python_bridge/simulator_hooks.py index 278f772..e68af8e 100644 --- a/src/vunit_python_bridge/simulator_hooks.py +++ b/src/vunit_python_bridge/simulator_hooks.py @@ -37,7 +37,6 @@ def register(context, bridge: PythonBridge) -> None: Register the hooks of every simulator the bridge serves with the package context. """ context.register_simulator_hooks("nvc", run_flags=_nvc_run_flags(bridge), run_env=_config_run_env(bridge)) - context.register_simulator_hooks("yavs", run_flags=_nvc_run_flags(bridge), run_env=_config_run_env(bridge)) context.register_simulator_hooks( "ghdl", elab_flags=_ghdl_elab_flags(bridge), @@ -67,7 +66,7 @@ def hook(simulator_interface, env: Dict[str, str]) -> Dict[str, str]: # pylint: def _nvc_run_flags(bridge: PythonBridge): """ - NVC run (-r) and yavs run flags loading the bridge. + NVC run (-r) flags loading the bridge. """ def hook(simulator_interface) -> List[str]: # pylint: disable=unused-argument diff --git a/tests/test_python_bridge.py b/tests/test_python_bridge.py index 5bf9a5a..8e781f9 100644 --- a/tests/test_python_bridge.py +++ b/tests/test_python_bridge.py @@ -269,7 +269,6 @@ def test_every_supported_simulator_has_its_interface(self): { "nvc": "VHPIDIRECT_NVC", "ghdl": "VHPIDIRECT_GHDL", - "yavs": "VHPIDIRECT_NVC", "modelsim": "FLI", "rivierapro": "VHPI", }, @@ -282,7 +281,7 @@ def test_the_simulators_the_bridge_serves_are_the_ones_it_is_set_up_for(self): ) def test_the_message_names_every_simulator(self): - for simulator in ("NVC", "GHDL", "yavs", "Questa/ModelSim", "Riviera-PRO"): + for simulator in ("NVC", "GHDL", "Questa/ModelSim", "Riviera-PRO"): self.assertIn(simulator, vunit_python_bridge.SUPPORTED_SIMULATORS) @@ -362,7 +361,6 @@ def _check_bridge_files_added(self, simulator, backend=None): def test_vhpidirect_adds_the_bridge_files(self): self._check_bridge_files_added(self._simulator("nvc")) self._check_bridge_files_added(self._simulator("ghdl"), backend="mcode") - self._check_bridge_files_added(self._simulator("yavs")) def test_fli_adds_the_bridge_files(self): # Questa/ModelSim is served by the bridge too, through native/fli.c. @@ -384,14 +382,13 @@ def test_vhpi_simulators_are_told_the_configuration_by_the_environment(self): def test_hooks_are_registered_for_every_simulator_the_bridge_serves(self): context = self._check_bridge_files_added(self._simulator("nvc")) - self.assertEqual(sorted(context.hooks), ["ghdl", "modelsim", "nvc", "yavs"]) + self.assertEqual(sorted(context.hooks), ["ghdl", "modelsim", "nvc"]) self.assertIsNotNone(context.hooks["nvc"]["run_flags"]) - self.assertIsNotNone(context.hooks["yavs"]["run_flags"]) self.assertIsNotNone(context.hooks["ghdl"]["elab_flags"]) self.assertIsNotNone(context.hooks["ghdl"]["run_env"]) self.assertIsNotNone(context.hooks["modelsim"]["process_flags"]) # Every simulator is told where the configuration of the project is - for simulator in ("nvc", "ghdl", "yavs", "modelsim"): + for simulator in ("nvc", "ghdl", "modelsim"): self.assertIsNotNone(context.hooks[simulator]["run_env"], simulator) def test_bridge_setup_failure_is_reported(self): @@ -605,7 +602,7 @@ def test_variable_is_not_read_at_import(self): self.assertIsNone(file_base._relative_file_base) # pylint: disable=protected-access def test_bridge_and_vhpi_add_the_package(self): - for name in ("nvc", "ghdl", "yavs", "modelsim", "rivierapro"): + for name in ("nvc", "ghdl", "modelsim", "rivierapro"): self._reset() context, package = self._setup(name) self.assertIn(("python_bridge", package), context.added_files, name) @@ -652,7 +649,6 @@ def test_token_is_the_file_name_or_the_link_flag_of_the_backend(self): # Only the GHDL backends linking ahead of time get the linker flag for simulator_name, backend, token in ( ("nvc", None, "libvunit_python_bridge.so"), - ("yavs", None, "libvunit_python_bridge.so"), ("ghdl", "mcode", "libvunit_python_bridge.so"), ("ghdl", "llvm-jit", "libvunit_python_bridge.so"), ("ghdl", "llvm", "-lvunit_python_bridge"), @@ -1412,7 +1408,7 @@ def test_rejects_32_bit_python(self): class TestSimulatorHooks(unittest.TestCase): """ - The hooks registered with the package context: what NVC, GHDL, yavs and Questa/ModelSim + The hooks registered with the package context: what NVC, GHDL and Questa/ModelSim are given for a simulation of the project. """ @@ -1420,7 +1416,7 @@ class TestSimulatorHooks(unittest.TestCase): def test_every_simulator_is_told_where_the_configuration_is(self): hooks = self._hooks(Path("/some/dir/libvunit_python_bridge.so")) - for simulator in ("nvc", "ghdl", "yavs", "modelsim"): + for simulator in ("nvc", "ghdl", "modelsim"): env = {"KEPT": "1"} result = hooks[simulator]["run_env"](self._ghdl("mcode"), env) self.assertEqual(result[simulator_hooks.CONFIG_FILE_VARIABLE], str(self.config_file), simulator) @@ -1462,12 +1458,6 @@ def test_nvc_loads_the_library_at_run_time(self): # NVC dlopen()s the library itself, nothing to do at elaboration self.assertIsNone(hooks["nvc"]["elab_flags"]) - def test_yavs_loads_the_library_at_run_time(self): - library_file = Path("/some/dir/libvunit_python_bridge.so") - hooks = self._hooks(library_file) - self.assertEqual(hooks["yavs"]["run_flags"](mock.Mock()), [f"--load={library_file!s}"]) - self.assertIsNone(hooks["yavs"]["elab_flags"]) - def test_ghdl_elab_flags_empty_for_mcode_and_jit(self): hooks = self._hooks(Path("/some/dir/libvunit_python_bridge.so")) for backend in ("mcode", "llvm-jit"): From 1c61cbcc93d35f4b37315b52ac7269098f477e5b Mon Sep 17 00:00:00 2001 From: Sebastian Abrahamsson Date: Thu, 29 Oct 2026 14:27:27 +0100 Subject: [PATCH 03/10] Let a simulator package say how it loads the bridge A simulator provided by a VUnit package is unknown to the table of the bridge. Its interface class now describes itself: foreign_language_interface names the binding ("VHPIDIRECT", "FLI" or "VHPI") and native_library_hooks(library_file) returns the simulator hooks that make it load the library. The bridge adds the configuration to the run environment those hooks give. The attributes take precedence over the table, which stays for the built-in simulators. A simulator with neither fails with an error naming the two attributes, and bad attribute values fail with what is wrong. The Windows check of the simulator executable only applies to the built-in simulators, whose executable the bridge knows. Co-Authored-By: Claude Opus 5.5 --- src/vunit_python_bridge/__init__.py | 28 +--- src/vunit_python_bridge/bridge.py | 84 +++++++++-- src/vunit_python_bridge/native_library.py | 10 +- src/vunit_python_bridge/simulator_hooks.py | 59 ++++++-- tests/test_python_bridge.py | 160 ++++++++++++++++++--- 5 files changed, 268 insertions(+), 73 deletions(-) diff --git a/src/vunit_python_bridge/__init__.py b/src/vunit_python_bridge/__init__.py index eaf8a39..1904514 100644 --- a/src/vunit_python_bridge/__init__.py +++ b/src/vunit_python_bridge/__init__.py @@ -14,7 +14,8 @@ the VUnit Python bridge, a small C library (native/*.c) embedding CPython in the simulator process, called through VHPIDIRECT (NVC, GHDL), the FLI (Questa/ModelSim) or VHPI -(Riviera-PRO). +(Riviera-PRO). A simulator provided by a VUnit package names its interface itself, see +bridge.foreign_language_interface. Modules: @@ -40,20 +41,6 @@ VHDL_PATH = Path(__file__).parent.resolve() / "hdl" / "src" -# The foreign language interface implementing the package for a simulator. VUnit says which -# simulator was selected, the package says how it is served: NVC and GHDL both go through -# VHPIDIRECT but bind the library differently, Questa/ModelSim goes through the FLI and -# Riviera-PRO through VHPI. -FOREIGN_LANGUAGE_INTERFACES = { - "nvc": "VHPIDIRECT_NVC", - "ghdl": "VHPIDIRECT_GHDL", - "modelsim": "FLI", - "rivierapro": "VHPI", -} - -# The simulators of the table, named as a user knows them -SUPPORTED_SIMULATORS = "NVC, GHDL, Questa/ModelSim or Riviera-PRO" - def setup(context): """ @@ -63,7 +50,7 @@ def setup(context): foreign language interface of the selected simulator. """ # pylint: disable=import-outside-toplevel - from .bridge import setup as setup_bridge + from .bridge import SUPPORTED_SIMULATORS, foreign_language_interface, setup as setup_bridge from . import simulator_hooks simulator_name = context.simulator_name @@ -72,12 +59,8 @@ def setup(context): f"The vunit-python-bridge package requires {SUPPORTED_SIMULATORS} but no simulator was found" ) - interface = FOREIGN_LANGUAGE_INTERFACES.get(simulator_name) - if interface is None: - raise RuntimeError( - f"The vunit-python-bridge package requires {SUPPORTED_SIMULATORS}, " - f"it has no foreign language interface for {simulator_name}" - ) + # Before anything is built, so a simulator the bridge cannot serve fails first + interface = foreign_language_interface(context.simulator_class) add_file_base_package(context) @@ -87,6 +70,7 @@ def setup(context): simulator_name, context.simulator_prefix, context.simulator_backend, + interface, ) context.add_source_files(context.library.name, list(bridge.vhdl_files)) diff --git a/src/vunit_python_bridge/bridge.py b/src/vunit_python_bridge/bridge.py index 8f6d60a..f27e9d8 100644 --- a/src/vunit_python_bridge/bridge.py +++ b/src/vunit_python_bridge/bridge.py @@ -31,10 +31,23 @@ BRIDGE_PACKAGE_TEMPLATE = VHDL_SOURCE_PATH / "python_bridge_pkg.vhd.in" CONFIG_FILE_NAME = "vunit_python_bridge.cfg" -# The simulators the bridge serves, reaching it through VHPIDIRECT, the FLI and VHPI. -FLI_SIMULATORS = ("modelsim",) -VHPI_SIMULATORS = ("rivierapro",) -BRIDGE_SIMULATORS = ("nvc", "ghdl") + FLI_SIMULATORS + VHPI_SIMULATORS +# The foreign language interface of each built-in VUnit simulator. VHPIDIRECT names the library +# by file name and the simulator hooks make the simulator load it, which GHDL does in its own way. +FOREIGN_LANGUAGE_INTERFACES = { + "nvc": "VHPIDIRECT", + "ghdl": "VHPIDIRECT_GHDL", + "modelsim": "FLI", + "rivierapro": "VHPI", +} + +# The simulators of the table, named as a user knows them +SUPPORTED_SIMULATORS = "NVC, GHDL, Questa/ModelSim or Riviera-PRO" + +# What the interface class of a simulator from a VUnit package defines instead, see +# docs/simulator_packages.rst +INTERFACE_ATTRIBUTE = "foreign_language_interface" +HOOKS_ATTRIBUTE = "native_library_hooks" +PACKAGE_INTERFACES = ("VHPIDIRECT", "FLI", "VHPI") # GHDL backends that link the design ahead of time. For these the library # token in the VHPIDIRECT attribute is passed to the linker instead of being @@ -63,6 +76,7 @@ def setup( simulator_name: Optional[str] = None, simulator_prefix: Optional[str] = None, simulator_backend: Optional[str] = None, + interface: Optional[str] = None, ) -> PythonBridge: """ Prepare the Python bridge for a project. Called by the package setup function. @@ -73,19 +87,23 @@ def setup( :param simulator_prefix: The path its executables were found in. :param simulator_backend: How the installation found there was built, which for GHDL is the code generator deciding how the library is bound. + :param interface: The foreign language interface, see foreign_language_interface. None takes it + from the table of built-in simulators by name. :returns: The bridge. Its vhdl_files are to be added to the package library. """ - if simulator_name is not None and simulator_name not in BRIDGE_SIMULATORS: - raise PythonBridgeError( - f"The vunit-python-bridge package requires NVC, GHDL, Questa/ModelSim or Riviera-PRO, " - f"it is not supported for {simulator_name}" - ) + if interface is None and simulator_name is not None: + interface = FOREIGN_LANGUAGE_INTERFACES.get(simulator_name) + if interface is None: + raise PythonBridgeError( + f"The vunit-python-bridge package requires {SUPPORTED_SIMULATORS}, " + f"it is not supported for {simulator_name}" + ) check_python_build() check_windows_64bit_simulator(simulator_name, simulator_prefix) - is_fli = simulator_name in FLI_SIMULATORS - is_vhpi = simulator_name in VHPI_SIMULATORS + is_fli = interface == "FLI" + is_vhpi = interface == "VHPI" if (is_fli or is_vhpi) and simulator_prefix is None: raise PythonBridgeError( f"The {'FLI' if is_fli else 'VHPI'} variant of the bridge is built against the {simulator_name} " @@ -105,13 +123,13 @@ def setup( # and the library, by absolute path since Questa accepts it, for the FLI; the library by # absolute path without its suffix, which Riviera-PRO adds, and the entry point registered # by native/vhpi.c for VHPI; the VHPIDIRECT library token and the entry point itself for - # NVC and GHDL. + # VHPIDIRECT. if is_fli: foreign = f"fli_{{entry_point}} {library_file!s}" elif is_vhpi: foreign = f"VHPI {_vhpi_library_token(library_file)} {{entry_point}}" else: - foreign = f"VHPIDIRECT {_vhpidirect_token(simulator_name, simulator_backend, library_file)} {{entry_point}}" + foreign = f"VHPIDIRECT {_vhpidirect_token(interface, simulator_backend, library_file)} {{entry_point}}" bridge_package = root / "vhdl" / "python_bridge_pkg.vhd" _write_if_changed(bridge_package, _render_bridge_package(foreign)) @@ -125,6 +143,42 @@ def setup( ) +def foreign_language_interface(simulator_class) -> str: + """ + The foreign language interface serving the selected simulator: the one its interface class + names in foreign_language_interface, or else the one of the built-in simulator of its name. + """ + name = simulator_class.name + interface = getattr(simulator_class, INTERFACE_ATTRIBUTE, None) + hooks = getattr(simulator_class, HOOKS_ATTRIBUTE, None) + if interface is None and hooks is None: + if name in FOREIGN_LANGUAGE_INTERFACES: + return FOREIGN_LANGUAGE_INTERFACES[name] + raise PythonBridgeError( + f"The vunit-python-bridge package requires {SUPPORTED_SIMULATORS}, or a simulator whose interface " + f"class says how it loads a native library, which {name} does not. Its simulator interface class must " + f"define {INTERFACE_ATTRIBUTE} ({', '.join(map(repr, PACKAGE_INTERFACES))}) and " + f"{HOOKS_ATTRIBUTE}(library_file), see 'Simulator packages' in the vunit-python-bridge documentation." + ) + for defined, missing in ((INTERFACE_ATTRIBUTE, HOOKS_ATTRIBUTE), (HOOKS_ATTRIBUTE, INTERFACE_ATTRIBUTE)): + if getattr(simulator_class, missing, None) is None: + raise PythonBridgeError( + f"The {name} simulator interface defines {defined} but not {missing}, " + "the vunit-python-bridge package needs both. Define it in the simulator interface class." + ) + if interface not in PACKAGE_INTERFACES: + raise PythonBridgeError( + f"{INTERFACE_ATTRIBUTE} of the {name} simulator interface is {interface!r}, " + f"the vunit-python-bridge package knows {', '.join(map(repr, PACKAGE_INTERFACES))}" + ) + if not callable(hooks): + raise PythonBridgeError( + f"{HOOKS_ATTRIBUTE} of the {name} simulator interface is not callable, it must be a static or " + "class method taking the library file" + ) + return interface + + def _prepare_library(simulator_prefix: Optional[Path], front_end: Optional[str]) -> Path: """ Prepare the library in the library directory, failing with a pointer to the variable @@ -141,12 +195,12 @@ def _prepare_library(simulator_prefix: Optional[Path], front_end: Optional[str]) ) from exc -def _vhpidirect_token(simulator_name, simulator_backend: Optional[str], library_file: Path) -> str: +def _vhpidirect_token(interface: Optional[str], simulator_backend: Optional[str], library_file: Path) -> str: """ The library token of the VHPIDIRECT attributes: the file name the simulator dlopen()s, or the linker flag of the GHDL backends that link the design ahead of time. """ - if simulator_name == "ghdl" and simulator_backend in GHDL_LINKING_BACKENDS: + if interface == "VHPIDIRECT_GHDL" and simulator_backend in GHDL_LINKING_BACKENDS: return "-lvunit_python_bridge" return library_file.name diff --git a/src/vunit_python_bridge/native_library.py b/src/vunit_python_bridge/native_library.py index 2c51fef..e00bec2 100644 --- a/src/vunit_python_bridge/native_library.py +++ b/src/vunit_python_bridge/native_library.py @@ -192,8 +192,14 @@ def check_windows_64bit_simulator(simulator_name: str, simulator_prefix: Optiona if simulator_prefix is None: return # NVC and GHDL name their executable, which VUnit lets the NVC and GHDL environment variables - # override, the Questa/ModelSim and Aldec interfaces use vsim - name = os.environ.get(simulator_name.upper(), simulator_name) if simulator_name in ("nvc", "ghdl") else "vsim" + # override, the Questa/ModelSim and Aldec interfaces use vsim. The executable of a simulator + # from a VUnit package is not known here. + if simulator_name in ("nvc", "ghdl"): + name = os.environ.get(simulator_name.upper(), simulator_name) + elif simulator_name in ("modelsim", "rivierapro"): + name = "vsim" + else: + return executable = Path(simulator_prefix) / (name if name.lower().endswith(".exe") else f"{name}.exe") machine = pe_machine(executable) if machine is not None and machine != _PE_MACHINE_X64: diff --git a/src/vunit_python_bridge/simulator_hooks.py b/src/vunit_python_bridge/simulator_hooks.py index e68af8e..385dd5b 100644 --- a/src/vunit_python_bridge/simulator_hooks.py +++ b/src/vunit_python_bridge/simulator_hooks.py @@ -21,7 +21,8 @@ from pathlib import Path from typing import Dict, List, Optional -from .bridge import GHDL_LINKING_BACKENDS, PythonBridge +from .bridge import GHDL_LINKING_BACKENDS, HOOKS_ATTRIBUTE, PythonBridge +from .native_library import PythonBridgeError # The vsim flag keeping the runtime libraries Questa bundles out of the dynamic library # search path, and the help category listing every vsim flag. @@ -31,32 +32,60 @@ # Read by native/config.c, keep in sync CONFIG_FILE_VARIABLE = "VUNIT_PYTHON_BRIDGE_CONFIG" +# The keyword arguments of register_simulator_hooks of the VUnit package context +HOOK_KINDS = ("elab_flags", "run_flags", "process_flags", "run_env") + def register(context, bridge: PythonBridge) -> None: """ - Register the hooks of every simulator the bridge serves with the package context. - """ - context.register_simulator_hooks("nvc", run_flags=_nvc_run_flags(bridge), run_env=_config_run_env(bridge)) - context.register_simulator_hooks( - "ghdl", - elab_flags=_ghdl_elab_flags(bridge), - run_env=_ghdl_run_env(bridge), - ) - context.register_simulator_hooks( - "modelsim", process_flags=_modelsim_process_flags(), run_env=_config_run_env(bridge) - ) + Register the hooks of every simulator the bridge serves with the package context. The hooks + a selected simulator interface gives by native_library_hooks replace the built-in ones. + """ + hooks = { + "nvc": dict(run_flags=_nvc_run_flags(bridge), run_env=_config_run_env(bridge)), + "ghdl": dict(elab_flags=_ghdl_elab_flags(bridge), run_env=_ghdl_run_env(bridge)), + "modelsim": dict(process_flags=_modelsim_process_flags(), run_env=_config_run_env(bridge)), + } + native_library_hooks = getattr(context.simulator_class, HOOKS_ATTRIBUTE, None) + if native_library_hooks is not None: + hooks[context.simulator_name] = _package_hooks(context.simulator_name, native_library_hooks, bridge) + for simulator_name, kinds in hooks.items(): + context.register_simulator_hooks(simulator_name, **kinds) if context.simulator_name == "rivierapro": # Its interface has no run environment hook, but starts vsim with the environment of this # process, after the setup os.environ[CONFIG_FILE_VARIABLE] = str(bridge.config_file) -def _config_run_env(bridge: PythonBridge): +def _package_hooks(simulator_name: str, native_library_hooks, bridge: PythonBridge) -> Dict: """ - Tell the bridge library where the configuration of the project is. + The hooks of a simulator interface loading the library, with the configuration added to its + run environment. """ + hooks = native_library_hooks(bridge.library_file) + if not isinstance(hooks, dict): + raise PythonBridgeError( + f"{HOOKS_ATTRIBUTE} of the {simulator_name} simulator interface returned {type(hooks).__name__}, " + f"it must return a dict with any of the keys {', '.join(HOOK_KINDS)}" + ) + unknown = sorted(str(kind) for kind in hooks if kind not in HOOK_KINDS) + if unknown: + raise PythonBridgeError( + f"{HOOKS_ATTRIBUTE} of the {simulator_name} simulator interface returned the unknown keys " + f"{', '.join(unknown)}, the keys are {', '.join(HOOK_KINDS)}" + ) + return dict(hooks, run_env=_config_run_env(bridge, hooks.get("run_env"))) + - def hook(simulator_interface, env: Dict[str, str]) -> Dict[str, str]: # pylint: disable=unused-argument +def _config_run_env(bridge: PythonBridge, simulator_run_env=None): + """ + Tell the bridge library where the configuration of the project is, on top of the environment + a simulator_run_env hook gives. + """ + + def hook(simulator_interface, env: Dict[str, str]) -> Dict[str, str]: + if simulator_run_env is not None: + env = simulator_run_env(simulator_interface, env) env = dict(env) env[CONFIG_FILE_VARIABLE] = str(bridge.config_file) return env diff --git a/tests/test_python_bridge.py b/tests/test_python_bridge.py index 8e781f9..e30ad0c 100644 --- a/tests/test_python_bridge.py +++ b/tests/test_python_bridge.py @@ -265,24 +265,18 @@ class TestForeignLanguageInterfaces(unittest.TestCase): def test_every_supported_simulator_has_its_interface(self): self.assertEqual( - vunit_python_bridge.FOREIGN_LANGUAGE_INTERFACES, + bridge_setup.FOREIGN_LANGUAGE_INTERFACES, { - "nvc": "VHPIDIRECT_NVC", + "nvc": "VHPIDIRECT", "ghdl": "VHPIDIRECT_GHDL", "modelsim": "FLI", "rivierapro": "VHPI", }, ) - def test_the_simulators_the_bridge_serves_are_the_ones_it_is_set_up_for(self): - # Every simulator is served by the bridge, which has to know the same simulators. - self.assertEqual( - set(vunit_python_bridge.FOREIGN_LANGUAGE_INTERFACES), set(bridge_setup.BRIDGE_SIMULATORS) - ) - def test_the_message_names_every_simulator(self): for simulator in ("NVC", "GHDL", "Questa/ModelSim", "Riviera-PRO"): - self.assertIn(simulator, vunit_python_bridge.SUPPORTED_SIMULATORS) + self.assertIn(simulator, bridge_setup.SUPPORTED_SIMULATORS) class TestPackageSetup(unittest.TestCase): @@ -306,9 +300,7 @@ def _simulator(name): """ A simulator interface class as the setup function sees it, that is by name. """ - simulator = mock.Mock() - simulator.name = name - return simulator + return types.SimpleNamespace(name=name) def _added_names(self, context): return {path.name for _, path in context.added_files} @@ -319,10 +311,12 @@ def test_rejects_simulator_class_none(self): def test_rejects_unsupported_simulator(self): context = self._context(self._simulator("some_simulator")) - with self.assertRaisesRegex(RuntimeError, "no foreign language interface for some_simulator") as ctx: + with self.assertRaisesRegex(RuntimeError, "which some_simulator does not") as ctx: vunit_python_bridge.setup(context) - # The message names the simulators so a user knows what is supported. - self.assertIn(vunit_python_bridge.SUPPORTED_SIMULATORS, str(ctx.exception)) + # The message names the simulators and what a simulator package has to define. + self.assertIn(bridge_setup.SUPPORTED_SIMULATORS, str(ctx.exception)) + self.assertIn("foreign_language_interface", str(ctx.exception)) + self.assertIn("native_library_hooks(library_file)", str(ctx.exception)) def _fake_bridge(self): return bridge_setup.PythonBridge( @@ -334,7 +328,7 @@ def _fake_bridge(self): ], ) - def _check_bridge_files_added(self, simulator, backend=None): + def _check_bridge_files_added(self, simulator, backend=None, interface=None): """ The generated bridge files are added for a simulator. """ @@ -352,6 +346,7 @@ def _check_bridge_files_added(self, simulator, backend=None): simulator.name, context.simulator_prefix, context.simulator_backend, + bridge_setup.FOREIGN_LANGUAGE_INTERFACES[simulator.name] if interface is None else interface, ) for expected in fake_bridge.vhdl_files: @@ -417,6 +412,129 @@ def listing(): self.assertEqual(listing(), before) +class _PackageSimulator: + """ + The interface class of a simulator provided by a VUnit package, saying how it loads a native library. + """ + + name = "pkgsim" + foreign_language_interface = "VHPIDIRECT" + + @staticmethod + def native_library_hooks(library_file): + return { + "run_flags": lambda simulator_interface: [f"--load={library_file!s}"], + "run_env": lambda simulator_interface, env: dict(env, PKGSIM="1"), + } + + +def _package_simulator(**attributes): + """ + A _PackageSimulator with some of its attributes replaced, None removing one. + """ + return type("Simulator", (_PackageSimulator,), attributes) + + +class TestSimulatorPackages(unittest.TestCase): + """ + A simulator interface class from a VUnit package says how the bridge is served by + foreign_language_interface and native_library_hooks(library_file), instead of the table. + """ + + library_file = Path("/cache/libvunit_python_bridge.so") + config_file = Path("/out/python_bridge/vunit_python_bridge.cfg") + + def _register(self, simulator_class): + context = _FakeContext(simulator_class, Path("/out"), Path("/run.py")) + bridge = bridge_setup.PythonBridge(self.library_file, self.config_file, []) + simulator_hooks.register(context, bridge) + return context.hooks + + def test_the_interface_is_taken_from_the_simulator_class(self): + for interface in ("VHPIDIRECT", "FLI", "VHPI"): + simulator = _package_simulator(foreign_language_interface=interface) + self.assertEqual(bridge_setup.foreign_language_interface(simulator), interface) + + def test_the_attributes_take_precedence_over_the_table(self): + simulator = _package_simulator(name="nvc", foreign_language_interface="FLI") + self.assertEqual(bridge_setup.foreign_language_interface(simulator), "FLI") + hooks = self._register(simulator)["nvc"] + # The hooks of the simulator replace the built-in ones, so the library is loaded once + self.assertEqual(hooks["run_flags"](mock.Mock()), [f"--load={self.library_file!s}"]) + self.assertEqual(hooks["run_env"](mock.Mock(), {})["PKGSIM"], "1") + + def test_the_built_in_simulators_are_served_by_the_table(self): + for name, interface in bridge_setup.FOREIGN_LANGUAGE_INTERFACES.items(): + simulator = types.SimpleNamespace(name=name) + self.assertEqual(bridge_setup.foreign_language_interface(simulator), interface) + + def test_the_hooks_of_the_simulator_are_registered_with_the_configuration(self): + hooks = self._register(_PackageSimulator) + self.assertEqual(sorted(hooks), ["ghdl", "modelsim", "nvc", "pkgsim"]) + self.assertEqual(hooks["pkgsim"]["run_flags"](mock.Mock()), [f"--load={self.library_file!s}"]) + self.assertIsNone(hooks["pkgsim"]["elab_flags"]) + env = {"KEPT": "1"} + result = hooks["pkgsim"]["run_env"](mock.Mock(), env) + self.assertEqual( + result, {"KEPT": "1", "PKGSIM": "1", simulator_hooks.CONFIG_FILE_VARIABLE: str(self.config_file)} + ) + self.assertEqual(env, {"KEPT": "1"}) + + def test_a_simulator_without_a_run_env_hook_is_told_the_configuration(self): + simulator = _package_simulator(native_library_hooks=staticmethod(lambda library_file: {})) + result = self._register(simulator)["pkgsim"]["run_env"](mock.Mock(), {"KEPT": "1"}) + self.assertEqual(result, {"KEPT": "1", simulator_hooks.CONFIG_FILE_VARIABLE: str(self.config_file)}) + + def test_a_simulator_without_the_attributes_is_rejected_with_what_to_define(self): + with self.assertRaisesRegex( + RuntimeError, "which xsim does not.*define foreign_language_interface.*native_library_hooks" + ): + bridge_setup.foreign_language_interface(types.SimpleNamespace(name="xsim")) + + def test_both_attributes_are_needed(self): + for missing in ("foreign_language_interface", "native_library_hooks"): + with self.assertRaisesRegex(RuntimeError, f"pkgsim simulator interface defines .* but not {missing}"): + bridge_setup.foreign_language_interface(_package_simulator(**{missing: None})) + + def test_an_unknown_interface_is_rejected(self): + with self.assertRaisesRegex(RuntimeError, "foreign_language_interface of the pkgsim .* is 'VHPI_DIRECT'"): + bridge_setup.foreign_language_interface(_package_simulator(foreign_language_interface="VHPI_DIRECT")) + + def test_hooks_that_cannot_be_called_are_rejected(self): + with self.assertRaisesRegex(RuntimeError, "native_library_hooks of the pkgsim .* is not callable"): + bridge_setup.foreign_language_interface(_package_simulator(native_library_hooks="--load")) + + def test_hooks_that_are_not_a_dict_are_rejected(self): + simulator = _package_simulator(native_library_hooks=staticmethod(lambda library_file: ["--load"])) + with self.assertRaisesRegex(RuntimeError, "returned list, it must return a dict"): + self._register(simulator) + + def test_unknown_hook_kinds_are_rejected(self): + simulator = _package_simulator(native_library_hooks=staticmethod(lambda library_file: {"sim_flags": list})) + with self.assertRaisesRegex(RuntimeError, "unknown keys sim_flags, the keys are elab_flags, run_flags"): + self._register(simulator) + + def test_vhpidirect_names_the_library_file(self): + with create_tempdir() as tempdir: + run_script = _write_run_script(tempdir / "run.py") + with mock.patch("vunit_python_bridge.bridge.prepare_library", return_value=self.library_file) as prepare: + # A backend that GHDL links ahead of time does not matter to another simulator + bridge = bridge_setup.setup(tempdir / "out", run_script, "pkgsim", "/sim/bin", "llvm", "VHPIDIRECT") + text = bridge.vhdl_files[0].read_text(encoding="utf-8") + self.assertIsNone(prepare.call_args.args[1]) + self.assertIn('"VHPIDIRECT libvunit_python_bridge.so vpy_begin"', text) + + def test_the_package_setup_serves_the_simulator(self): + with create_tempdir() as tempdir: + run_script = _write_run_script(tempdir / "run.py") + context = _FakeContext(_PackageSimulator, tempdir / "out", run_script, "/sim/bin") + fake_bridge = bridge_setup.PythonBridge(self.library_file, self.config_file, []) + with mock.patch("vunit_python_bridge.bridge.setup", return_value=fake_bridge) as setup_mock: + vunit_python_bridge.setup(context) + self.assertEqual(setup_mock.call_args.args[2:], ("pkgsim", "/sim/bin", None, "VHPIDIRECT")) + self.assertEqual(context.hooks["pkgsim"]["run_flags"](mock.Mock()), [f"--load={self.library_file!s}"]) + + class TestRelativeFileBase(unittest.TestCase): """ set_relative_file_base() and the python_file_base_pkg generated by setup(), giving @@ -446,9 +564,7 @@ def _setup(self, name="nvc"): Set the package up for a simulator, with bridge.setup() stubbed, and return the context and the generated package. """ - simulator = mock.Mock() - simulator.name = name - context = _FakeContext(simulator, self.tempdir / "out", self.run_script, "/sim/bin") + context = _FakeContext(types.SimpleNamespace(name=name), self.tempdir / "out", self.run_script, "/sim/bin") fake_bridge = bridge_setup.PythonBridge( library_file=Path("/fake/libvunit_python_bridge.so"), config_file=Path("/fake/vunit_python_bridge.cfg"), @@ -1391,6 +1507,12 @@ def test_vsim_is_checked_for_questa(self): with self.assertRaisesRegex(native_library.PythonBridgeError, "vsim.exe is 32-bit x86"): self._check("modelsim", str(tempdir)) + def test_the_executable_of_a_simulator_package_is_not_checked(self): + with create_tempdir() as tempdir: + for name in ("pkgsim.exe", "vsim.exe"): + self._write_executable(tempdir / name, 0x014C) + self._check("pkgsim", str(tempdir)) + def test_not_checked_outside_windows(self): with create_tempdir() as tempdir: self._write_executable(tempdir / "ghdl.exe", 0x014C) From 0119a063976da782899dbc2394740e1a65a6515c Mon Sep 17 00:00:00 2001 From: Sebastian Abrahamsson Date: Thu, 29 Oct 2026 14:28:12 +0100 Subject: [PATCH 04/10] Document the simulator package attributes A "Simulator packages" page for simulator authors describes foreign_language_interface and native_library_hooks, with the README, the requirements and the news fragment pointing to it. Co-Authored-By: Claude Opus 5.5 --- README.md | 4 ++ docs/index.rst | 4 ++ docs/requirements.rst | 5 ++- docs/simulator_packages.rst | 59 ++++++++++++++++++++++++++++++ release_notes/news.d/41.feature.md | 1 + 5 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 docs/simulator_packages.rst create mode 100644 release_notes/news.d/41.feature.md diff --git a/README.md b/README.md index 16eeb07..b6b4c75 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,10 @@ check(call_integer_vector("gain", arg(integer_vector'(1, 2))) = integer_vector'( | Questa/ModelSim | FLI | Tested manually on Linux; the Windows build is untested | | Riviera-PRO | VHPI | Tested manually on Linux and Windows | +A simulator provided by a VUnit package, such as yavs by `vunit-yavs`, says how it loads the bridge +itself, by two attributes of its simulator interface class: see +[Simulator packages](https://vunit-python-bridge.readthedocs.io/en/latest/simulator_packages.html). + ## Requirements - VUnit 5.0.0.dev15 or later (`vunit_hdl` on PyPI), which pip installs with the package. diff --git a/docs/index.rst b/docs/index.rst index abc2cd2..e3e35a3 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -62,6 +62,9 @@ Questa/ModelSim FLI Tested manually on Linux; the Win Riviera-PRO VHPI Tested manually on Linux and Windows ================================= ========== ======================================================== +A simulator provided by a VUnit package says how it loads the bridge itself, see +:doc:`simulator_packages`. + Getting started --------------- @@ -131,6 +134,7 @@ become available through ``python_context``: :hidden: how_it_works + simulator_packages .. toctree:: :caption: About diff --git a/docs/requirements.rst b/docs/requirements.rst index cf0a35e..3b0111e 100644 --- a/docs/requirements.rst +++ b/docs/requirements.rst @@ -3,8 +3,9 @@ Requirements * A VUnit with support for packages and simulator hooks, see the `README `__. -* NVC, GHDL, Questa/ModelSim or Riviera-PRO, see :ref:`python_bridge:native`. - Any other simulator raises a ``RuntimeError``. +* NVC, GHDL, Questa/ModelSim or Riviera-PRO, see :ref:`python_bridge:native`, + or a simulator from a VUnit package that describes how it loads the bridge, see + :ref:`python_bridge:simulator_packages`. 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. diff --git a/docs/simulator_packages.rst b/docs/simulator_packages.rst new file mode 100644 index 0000000..fee5492 --- /dev/null +++ b/docs/simulator_packages.rst @@ -0,0 +1,59 @@ +.. _python_bridge:simulator_packages: + +Simulator packages +------------------ + +A simulator can be provided by a VUnit package, whose ``vunit_pkg.toml`` names +its simulator interface class. The bridge does not list such simulators. +Instead, the interface class says how the bridge is served, with two +attributes the bridge reads from the selected simulator class: + +``foreign_language_interface`` + How the simulator calls the bridge library: ``"VHPIDIRECT"``, ``"FLI"`` or + ``"VHPI"``. + + * ``"VHPIDIRECT"``: the foreign attributes are + ``VHPIDIRECT ``, with the file name + only, so the simulator has to load the library itself, from the flags or + the environment the hooks below give it. + * ``"FLI"``: the attributes of the FLI variant of the library, built against + the ``mti.h`` of the simulator installation, as for Questa/ModelSim. + * ``"VHPI"``: the attributes of the VHPI variant, built against the + ``vhpi_user.h`` of the simulator installation, as for Riviera-PRO. + +``native_library_hooks(library_file)`` + A static or class method returning how the simulator loads the given + library: a ``dict`` of keyword arguments for + ``context.register_simulator_hooks``, that is any of ``elab_flags``, + ``run_flags``, ``process_flags`` and ``run_env``, each a function as + described under "Simulator Hooks" in the VUnit documentation of packages. + The bridge registers them for the simulator, with its own ``run_env`` on top + of the one returned, which adds ``VUNIT_PYTHON_BRIDGE_CONFIG`` to the + environment. The simulator interface must therefore apply the run + environment hooks to the simulation. + +For example, for a simulator that loads a library given with ``--load`` when +it runs a test: + +.. code-block:: python + + class MySimulatorInterface(SimulatorInterface): + name = "mysim" + foreign_language_interface = "VHPIDIRECT" + + @staticmethod + def native_library_hooks(library_file): + return {"run_flags": lambda simulator_interface: [f"--load={library_file}"]} + +The attributes take precedence over what the bridge knows about the built-in +VUnit simulators. A simulator having neither of them, and not being one of the +built-in simulators the bridge serves, fails when the package is added, with +an error naming the two attributes. So do an unknown interface name, only one +of the two attributes, and hooks that are not a ``dict`` of the keys above. + +The bridge passes a VHDL string to Python as UTF-8 bytes, one character per +byte, which is what NVC and GHDL make of a UTF-8 source file. A simulator that +reads a source as one character per code point needs an option to read it as +bytes (Latin-1) for non-ASCII string literals to reach Python unchanged. + +yavs is a simulator served this way, by its package ``vunit-yavs``. diff --git a/release_notes/news.d/41.feature.md b/release_notes/news.d/41.feature.md new file mode 100644 index 0000000..0cd1c89 --- /dev/null +++ b/release_notes/news.d/41.feature.md @@ -0,0 +1 @@ +A simulator provided by a VUnit package is supported when its simulator interface class defines `foreign_language_interface` and `native_library_hooks(library_file)`, which say how it loads the bridge. See "Simulator packages" in the documentation. From ba7c3ad430c94510c66465866dc98b47b188e609 Mon Sep 17 00:00:00 2001 From: Sebastian Abrahamsson Date: Thu, 29 Oct 2026 15:20:53 +0100 Subject: [PATCH 05/10] Remove the yavs mentions and the string encoding paragraph, shorten the news fragment Co-Authored-By: Claude Sonnet 5.5 --- README.md | 2 +- docs/simulator_packages.rst | 7 ------- release_notes/news.d/41.feature.md | 2 +- 3 files changed, 2 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index b6b4c75..887c520 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ check(call_integer_vector("gain", arg(integer_vector'(1, 2))) = integer_vector'( | Questa/ModelSim | FLI | Tested manually on Linux; the Windows build is untested | | Riviera-PRO | VHPI | Tested manually on Linux and Windows | -A simulator provided by a VUnit package, such as yavs by `vunit-yavs`, says how it loads the bridge +A simulator provided by a VUnit package says how it loads the bridge itself, by two attributes of its simulator interface class: see [Simulator packages](https://vunit-python-bridge.readthedocs.io/en/latest/simulator_packages.html). diff --git a/docs/simulator_packages.rst b/docs/simulator_packages.rst index fee5492..b2fcf59 100644 --- a/docs/simulator_packages.rst +++ b/docs/simulator_packages.rst @@ -50,10 +50,3 @@ VUnit simulators. A simulator having neither of them, and not being one of the built-in simulators the bridge serves, fails when the package is added, with an error naming the two attributes. So do an unknown interface name, only one of the two attributes, and hooks that are not a ``dict`` of the keys above. - -The bridge passes a VHDL string to Python as UTF-8 bytes, one character per -byte, which is what NVC and GHDL make of a UTF-8 source file. A simulator that -reads a source as one character per code point needs an option to read it as -bytes (Latin-1) for non-ASCII string literals to reach Python unchanged. - -yavs is a simulator served this way, by its package ``vunit-yavs``. diff --git a/release_notes/news.d/41.feature.md b/release_notes/news.d/41.feature.md index 0cd1c89..a24f13d 100644 --- a/release_notes/news.d/41.feature.md +++ b/release_notes/news.d/41.feature.md @@ -1 +1 @@ -A simulator provided by a VUnit package is supported when its simulator interface class defines `foreign_language_interface` and `native_library_hooks(library_file)`, which say how it loads the bridge. See "Simulator packages" in the documentation. +Simulators provided by VUnit packages can be used with the bridge, see "Simulator packages" in the documentation. From 1b7cc49b5c89e0f2c80d54aa0eb0e994f51d8b2e Mon Sep 17 00:00:00 2001 From: Sebastian Abrahamsson Date: Thu, 29 Oct 2026 15:21:10 +0100 Subject: [PATCH 06/10] Name the environment variables of the NVC and GHDL executables Co-Authored-By: Claude Sonnet 5.5 --- src/vunit_python_bridge/native_library.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/vunit_python_bridge/native_library.py b/src/vunit_python_bridge/native_library.py index e00bec2..9b748a1 100644 --- a/src/vunit_python_bridge/native_library.py +++ b/src/vunit_python_bridge/native_library.py @@ -162,6 +162,8 @@ def _check_windows_python() -> None: # Machine field values of the PE (Windows executable) header _PE_MACHINE_X64 = 0x8664 _PE_MACHINE_NAMES = {_PE_MACHINE_X64: "64-bit x86 (x64)", 0x014C: "32-bit x86", 0xAA64: "ARM64"} +# The environment variables VUnit reads for the executable of NVC and GHDL +EXECUTABLE_VARIABLES = {"nvc": "NVC", "ghdl": "GHDL"} def pe_machine(path: Path) -> Optional[int]: @@ -194,8 +196,8 @@ def check_windows_64bit_simulator(simulator_name: str, simulator_prefix: Optiona # NVC and GHDL name their executable, which VUnit lets the NVC and GHDL environment variables # override, the Questa/ModelSim and Aldec interfaces use vsim. The executable of a simulator # from a VUnit package is not known here. - if simulator_name in ("nvc", "ghdl"): - name = os.environ.get(simulator_name.upper(), simulator_name) + if simulator_name in EXECUTABLE_VARIABLES: + name = os.environ.get(EXECUTABLE_VARIABLES[simulator_name], simulator_name) elif simulator_name in ("modelsim", "rivierapro"): name = "vsim" else: From 3295919de346c62d56dafdfac4efb6dffc5876a4 Mon Sep 17 00:00:00 2001 From: Sebastian Abrahamsson Date: Thu, 29 Oct 2026 15:21:10 +0100 Subject: [PATCH 07/10] Call the table of built-in simulators BUILTIN_SIMULATORS Co-Authored-By: Claude Sonnet 5.5 --- src/vunit_python_bridge/bridge.py | 8 ++++---- tests/test_python_bridge.py | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/vunit_python_bridge/bridge.py b/src/vunit_python_bridge/bridge.py index f27e9d8..4a3f637 100644 --- a/src/vunit_python_bridge/bridge.py +++ b/src/vunit_python_bridge/bridge.py @@ -33,7 +33,7 @@ # The foreign language interface of each built-in VUnit simulator. VHPIDIRECT names the library # by file name and the simulator hooks make the simulator load it, which GHDL does in its own way. -FOREIGN_LANGUAGE_INTERFACES = { +BUILTIN_SIMULATORS = { "nvc": "VHPIDIRECT", "ghdl": "VHPIDIRECT_GHDL", "modelsim": "FLI", @@ -92,7 +92,7 @@ def setup( :returns: The bridge. Its vhdl_files are to be added to the package library. """ if interface is None and simulator_name is not None: - interface = FOREIGN_LANGUAGE_INTERFACES.get(simulator_name) + interface = BUILTIN_SIMULATORS.get(simulator_name) if interface is None: raise PythonBridgeError( f"The vunit-python-bridge package requires {SUPPORTED_SIMULATORS}, " @@ -152,8 +152,8 @@ def foreign_language_interface(simulator_class) -> str: interface = getattr(simulator_class, INTERFACE_ATTRIBUTE, None) hooks = getattr(simulator_class, HOOKS_ATTRIBUTE, None) if interface is None and hooks is None: - if name in FOREIGN_LANGUAGE_INTERFACES: - return FOREIGN_LANGUAGE_INTERFACES[name] + if name in BUILTIN_SIMULATORS: + return BUILTIN_SIMULATORS[name] raise PythonBridgeError( f"The vunit-python-bridge package requires {SUPPORTED_SIMULATORS}, or a simulator whose interface " f"class says how it loads a native library, which {name} does not. Its simulator interface class must " diff --git a/tests/test_python_bridge.py b/tests/test_python_bridge.py index e30ad0c..3c0f634 100644 --- a/tests/test_python_bridge.py +++ b/tests/test_python_bridge.py @@ -265,7 +265,7 @@ class TestForeignLanguageInterfaces(unittest.TestCase): def test_every_supported_simulator_has_its_interface(self): self.assertEqual( - bridge_setup.FOREIGN_LANGUAGE_INTERFACES, + bridge_setup.BUILTIN_SIMULATORS, { "nvc": "VHPIDIRECT", "ghdl": "VHPIDIRECT_GHDL", @@ -346,7 +346,7 @@ def _check_bridge_files_added(self, simulator, backend=None, interface=None): simulator.name, context.simulator_prefix, context.simulator_backend, - bridge_setup.FOREIGN_LANGUAGE_INTERFACES[simulator.name] if interface is None else interface, + bridge_setup.BUILTIN_SIMULATORS[simulator.name] if interface is None else interface, ) for expected in fake_bridge.vhdl_files: @@ -464,7 +464,7 @@ def test_the_attributes_take_precedence_over_the_table(self): self.assertEqual(hooks["run_env"](mock.Mock(), {})["PKGSIM"], "1") def test_the_built_in_simulators_are_served_by_the_table(self): - for name, interface in bridge_setup.FOREIGN_LANGUAGE_INTERFACES.items(): + for name, interface in bridge_setup.BUILTIN_SIMULATORS.items(): simulator = types.SimpleNamespace(name=name) self.assertEqual(bridge_setup.foreign_language_interface(simulator), interface) From 258eb9515446d3688951a169fa81f37b1a4c6588 Mon Sep 17 00:00:00 2001 From: Sebastian Abrahamsson Date: Thu, 29 Oct 2026 15:22:52 +0100 Subject: [PATCH 08/10] Build the list of simulators from the simulators VUnit knows The bridge starts from the built-in simulators and adds every simulator class of VUnit, for instance from a simulator package, that defines foreign_language_interface and native_library_hooks. The selected simulator is looked up in that list. Co-Authored-By: Claude Sonnet 5.5 --- docs/simulator_packages.rst | 16 ++-- src/vunit_python_bridge/__init__.py | 11 +-- src/vunit_python_bridge/bridge.py | 73 +++++++++++------ src/vunit_python_bridge/simulator_hooks.py | 12 +-- tests/test_python_bridge.py | 94 +++++++++++++++------- 5 files changed, 135 insertions(+), 71 deletions(-) diff --git a/docs/simulator_packages.rst b/docs/simulator_packages.rst index b2fcf59..354a078 100644 --- a/docs/simulator_packages.rst +++ b/docs/simulator_packages.rst @@ -5,8 +5,9 @@ Simulator packages A simulator can be provided by a VUnit package, whose ``vunit_pkg.toml`` names its simulator interface class. The bridge does not list such simulators. -Instead, the interface class says how the bridge is served, with two -attributes the bridge reads from the selected simulator class: +It collects them from VUnit, which knows the simulator interface classes of the +packages added before the bridge. A class says how the bridge is served with +two attributes: ``foreign_language_interface`` How the simulator calls the bridge library: ``"VHPIDIRECT"``, ``"FLI"`` or @@ -45,8 +46,9 @@ it runs a test: def native_library_hooks(library_file): return {"run_flags": lambda simulator_interface: [f"--load={library_file}"]} -The attributes take precedence over what the bridge knows about the built-in -VUnit simulators. A simulator having neither of them, and not being one of the -built-in simulators the bridge serves, fails when the package is added, with -an error naming the two attributes. So do an unknown interface name, only one -of the two attributes, and hooks that are not a ``dict`` of the keys above. +The simulators of these classes are added to the built-in ones the bridge +serves, and take precedence over a built-in simulator of the same ``name``. +Selecting a simulator that is not among them fails when the package is added, +with an error naming the simulators the bridge serves and the two attributes. +So do an unknown interface name, only one of the two attributes, and hooks +that are not a ``dict`` of the keys above. diff --git a/src/vunit_python_bridge/__init__.py b/src/vunit_python_bridge/__init__.py index 1904514..ca1d08f 100644 --- a/src/vunit_python_bridge/__init__.py +++ b/src/vunit_python_bridge/__init__.py @@ -15,7 +15,7 @@ the VUnit Python bridge, a small C library (native/*.c) embedding CPython in the simulator process, called through VHPIDIRECT (NVC, GHDL), the FLI (Questa/ModelSim) or VHPI (Riviera-PRO). A simulator provided by a VUnit package names its interface itself, see -bridge.foreign_language_interface. +bridge.bridge_simulators. Modules: @@ -50,17 +50,18 @@ def setup(context): foreign language interface of the selected simulator. """ # pylint: disable=import-outside-toplevel - from .bridge import SUPPORTED_SIMULATORS, foreign_language_interface, setup as setup_bridge + from .bridge import bridge_simulators, select_simulator, setup as setup_bridge from . import simulator_hooks + simulators = bridge_simulators() simulator_name = context.simulator_name if simulator_name is None: raise RuntimeError( - f"The vunit-python-bridge package requires {SUPPORTED_SIMULATORS} but no simulator was found" + f"The vunit-python-bridge package requires {', '.join(simulators)} but no simulator was found" ) # Before anything is built, so a simulator the bridge cannot serve fails first - interface = foreign_language_interface(context.simulator_class) + interface = select_simulator(simulators, simulator_name).interface add_file_base_package(context) @@ -75,4 +76,4 @@ def setup(context): context.add_source_files(context.library.name, list(bridge.vhdl_files)) - simulator_hooks.register(context, bridge) + simulator_hooks.register(context, bridge, simulators) diff --git a/src/vunit_python_bridge/bridge.py b/src/vunit_python_bridge/bridge.py index 4a3f637..9a7b287 100644 --- a/src/vunit_python_bridge/bridge.py +++ b/src/vunit_python_bridge/bridge.py @@ -13,7 +13,9 @@ import re import sys from pathlib import Path -from typing import List, NamedTuple, Optional +from typing import Callable, Dict, List, NamedTuple, Optional + +from vunit.sim_if.factory import SIMULATOR_FACTORY from .native_library import ( LIBRARY_DIR_VARIABLE, @@ -40,9 +42,6 @@ "rivierapro": "VHPI", } -# The simulators of the table, named as a user knows them -SUPPORTED_SIMULATORS = "NVC, GHDL, Questa/ModelSim or Riviera-PRO" - # What the interface class of a simulator from a VUnit package defines instead, see # docs/simulator_packages.rst INTERFACE_ATTRIBUTE = "foreign_language_interface" @@ -60,6 +59,16 @@ SUBPROGRAM_PATTERN = re.compile(r"^ (impure function|procedure) (\w+)(\([^)]*\))?( return \w+)?;", re.MULTILINE) +class BridgeSimulator(NamedTuple): + """ + How the bridge serves a simulator: its foreign language interface and, for a simulator from a + VUnit package, the native_library_hooks that make it load the library (None for a built-in one). + """ + + interface: str + native_library_hooks: Optional[Callable] + + class PythonBridge(NamedTuple): """ A prepared bridge: the native library and the generated VHDL. @@ -87,17 +96,12 @@ def setup( :param simulator_prefix: The path its executables were found in. :param simulator_backend: How the installation found there was built, which for GHDL is the code generator deciding how the library is bound. - :param interface: The foreign language interface, see foreign_language_interface. None takes it - from the table of built-in simulators by name. + :param interface: The foreign language interface, see bridge_simulators. None takes it from + there by simulator name. :returns: The bridge. Its vhdl_files are to be added to the package library. """ if interface is None and simulator_name is not None: - interface = BUILTIN_SIMULATORS.get(simulator_name) - if interface is None: - raise PythonBridgeError( - f"The vunit-python-bridge package requires {SUPPORTED_SIMULATORS}, " - f"it is not supported for {simulator_name}" - ) + interface = select_simulator(bridge_simulators(), simulator_name).interface check_python_build() check_windows_64bit_simulator(simulator_name, simulator_prefix) @@ -143,25 +147,45 @@ def setup( ) -def foreign_language_interface(simulator_class) -> str: +def bridge_simulators() -> Dict[str, BridgeSimulator]: """ - The foreign language interface serving the selected simulator: the one its interface class - names in foreign_language_interface, or else the one of the built-in simulator of its name. + The simulators the bridge serves, by name: the built-in ones and every simulator VUnit knows, + from a simulator package, whose interface class defines foreign_language_interface and + native_library_hooks. The simulator packages are added to VUnit before the bridge. """ - name = simulator_class.name - interface = getattr(simulator_class, INTERFACE_ATTRIBUTE, None) - hooks = getattr(simulator_class, HOOKS_ATTRIBUTE, None) - if interface is None and hooks is None: - if name in BUILTIN_SIMULATORS: - return BUILTIN_SIMULATORS[name] + simulators = {name: BridgeSimulator(interface, None) for name, interface in BUILTIN_SIMULATORS.items()} + for simulator_class in SIMULATOR_FACTORY.supported_simulators(): + interface = getattr(simulator_class, INTERFACE_ATTRIBUTE, None) + hooks = getattr(simulator_class, HOOKS_ATTRIBUTE, None) + if interface is not None or hooks is not None: + _check_attributes(simulator_class.name, interface, hooks) + simulators[simulator_class.name] = BridgeSimulator(interface, hooks) + return simulators + + +def select_simulator(simulators: Dict[str, BridgeSimulator], name: str) -> BridgeSimulator: + """ + The simulator of the given name from bridge_simulators. + """ + if name not in simulators: raise PythonBridgeError( - f"The vunit-python-bridge package requires {SUPPORTED_SIMULATORS}, or a simulator whose interface " + f"The vunit-python-bridge package requires {', '.join(simulators)}, or a simulator whose interface " f"class says how it loads a native library, which {name} does not. Its simulator interface class must " f"define {INTERFACE_ATTRIBUTE} ({', '.join(map(repr, PACKAGE_INTERFACES))}) and " f"{HOOKS_ATTRIBUTE}(library_file), see 'Simulator packages' in the vunit-python-bridge documentation." ) - for defined, missing in ((INTERFACE_ATTRIBUTE, HOOKS_ATTRIBUTE), (HOOKS_ATTRIBUTE, INTERFACE_ATTRIBUTE)): - if getattr(simulator_class, missing, None) is None: + return simulators[name] + + +def _check_attributes(name: str, interface, hooks) -> None: + """ + Check the attributes a simulator interface class defines to be served by the bridge. + """ + for defined, missing, value in ( + (INTERFACE_ATTRIBUTE, HOOKS_ATTRIBUTE, hooks), + (HOOKS_ATTRIBUTE, INTERFACE_ATTRIBUTE, interface), + ): + if value is None: raise PythonBridgeError( f"The {name} simulator interface defines {defined} but not {missing}, " "the vunit-python-bridge package needs both. Define it in the simulator interface class." @@ -176,7 +200,6 @@ def foreign_language_interface(simulator_class) -> str: f"{HOOKS_ATTRIBUTE} of the {name} simulator interface is not callable, it must be a static or " "class method taking the library file" ) - return interface def _prepare_library(simulator_prefix: Optional[Path], front_end: Optional[str]) -> Path: diff --git a/src/vunit_python_bridge/simulator_hooks.py b/src/vunit_python_bridge/simulator_hooks.py index 385dd5b..8e21723 100644 --- a/src/vunit_python_bridge/simulator_hooks.py +++ b/src/vunit_python_bridge/simulator_hooks.py @@ -21,7 +21,7 @@ from pathlib import Path from typing import Dict, List, Optional -from .bridge import GHDL_LINKING_BACKENDS, HOOKS_ATTRIBUTE, PythonBridge +from .bridge import GHDL_LINKING_BACKENDS, HOOKS_ATTRIBUTE, BridgeSimulator, PythonBridge from .native_library import PythonBridgeError # The vsim flag keeping the runtime libraries Questa bundles out of the dynamic library @@ -36,19 +36,19 @@ HOOK_KINDS = ("elab_flags", "run_flags", "process_flags", "run_env") -def register(context, bridge: PythonBridge) -> None: +def register(context, bridge: PythonBridge, simulators: Dict[str, BridgeSimulator]) -> None: """ Register the hooks of every simulator the bridge serves with the package context. The hooks - a selected simulator interface gives by native_library_hooks replace the built-in ones. + a simulator interface gives by native_library_hooks replace the built-in ones. """ hooks = { "nvc": dict(run_flags=_nvc_run_flags(bridge), run_env=_config_run_env(bridge)), "ghdl": dict(elab_flags=_ghdl_elab_flags(bridge), run_env=_ghdl_run_env(bridge)), "modelsim": dict(process_flags=_modelsim_process_flags(), run_env=_config_run_env(bridge)), } - native_library_hooks = getattr(context.simulator_class, HOOKS_ATTRIBUTE, None) - if native_library_hooks is not None: - hooks[context.simulator_name] = _package_hooks(context.simulator_name, native_library_hooks, bridge) + for name, simulator in simulators.items(): + if simulator.native_library_hooks is not None: + hooks[name] = _package_hooks(name, simulator.native_library_hooks, bridge) for simulator_name, kinds in hooks.items(): context.register_simulator_hooks(simulator_name, **kinds) if context.simulator_name == "rivierapro": diff --git a/tests/test_python_bridge.py b/tests/test_python_bridge.py index 3c0f634..91bd004 100644 --- a/tests/test_python_bridge.py +++ b/tests/test_python_bridge.py @@ -30,6 +30,8 @@ else: import tomli as tomllib # A dependency of VUnit before Python 3.11 +from vunit.sim_if.factory import SimulatorFactory + import vunit_python_bridge from vunit_python_bridge import bridge as bridge_setup, file_base, native_library, simulator_hooks @@ -257,10 +259,10 @@ def test_generated_packages_are_up_to_date(self): self.assertEqual((generator.SRC_PATH / name).read_text(encoding="utf-8"), text, f"regenerate {name}") -class TestForeignLanguageInterfaces(unittest.TestCase): +class TestBuiltinSimulators(unittest.TestCase): """ - The table deciding how a simulator is served. VUnit only says which simulator was - selected, so the package has to know the foreign language interface of each of them. + The table of the built-in simulators. VUnit only says which simulator was selected, so the + package has to know the foreign language interface of each of them. """ def test_every_supported_simulator_has_its_interface(self): @@ -275,8 +277,8 @@ def test_every_supported_simulator_has_its_interface(self): ) def test_the_message_names_every_simulator(self): - for simulator in ("NVC", "GHDL", "Questa/ModelSim", "Riviera-PRO"): - self.assertIn(simulator, bridge_setup.SUPPORTED_SIMULATORS) + with self.assertRaisesRegex(RuntimeError, "nvc, ghdl, modelsim, rivierapro, or a simulator"): + bridge_setup.select_simulator(bridge_setup.bridge_simulators(), "xsim") class TestPackageSetup(unittest.TestCase): @@ -314,7 +316,7 @@ def test_rejects_unsupported_simulator(self): with self.assertRaisesRegex(RuntimeError, "which some_simulator does not") as ctx: vunit_python_bridge.setup(context) # The message names the simulators and what a simulator package has to define. - self.assertIn(bridge_setup.SUPPORTED_SIMULATORS, str(ctx.exception)) + self.assertIn(", ".join(bridge_setup.BUILTIN_SIMULATORS), str(ctx.exception)) self.assertIn("foreign_language_interface", str(ctx.exception)) self.assertIn("native_library_hooks(library_file)", str(ctx.exception)) @@ -418,6 +420,8 @@ class _PackageSimulator: """ name = "pkgsim" + compile_options = [] + sim_options = [] foreign_language_interface = "VHPIDIRECT" @staticmethod @@ -435,39 +439,67 @@ def _package_simulator(**attributes): return type("Simulator", (_PackageSimulator,), attributes) +def _factory(*simulator_classes): + """ + A stand-in for the VUnit simulator factory knowing the given simulator classes. + """ + return types.SimpleNamespace(supported_simulators=lambda: list(simulator_classes)) + + +def _simulators(*simulator_classes): + """ + The simulators the bridge serves when VUnit knows the given simulator classes, the built-in + VUnit simulators left out. + """ + with mock.patch.object(bridge_setup, "SIMULATOR_FACTORY", _factory(*simulator_classes)): + return bridge_setup.bridge_simulators() + + class TestSimulatorPackages(unittest.TestCase): """ - A simulator interface class from a VUnit package says how the bridge is served by - foreign_language_interface and native_library_hooks(library_file), instead of the table. + The bridge serves the simulators VUnit knows whose interface class says how by + foreign_language_interface and native_library_hooks(library_file), as well as the built-in ones. """ library_file = Path("/cache/libvunit_python_bridge.so") config_file = Path("/out/python_bridge/vunit_python_bridge.cfg") - def _register(self, simulator_class): - context = _FakeContext(simulator_class, Path("/out"), Path("/run.py")) + def _register(self, *simulator_classes): + context = _FakeContext(None, Path("/out"), Path("/run.py")) bridge = bridge_setup.PythonBridge(self.library_file, self.config_file, []) - simulator_hooks.register(context, bridge) + simulator_hooks.register(context, bridge, _simulators(*simulator_classes)) return context.hooks + def test_the_simulators_are_collected_from_vunit(self): + # A simulator registered in a VUnit simulator factory, as a package does + factory = SimulatorFactory() + factory.register_simulator(_PackageSimulator) + with mock.patch.object(bridge_setup, "SIMULATOR_FACTORY", factory): + simulators = bridge_setup.bridge_simulators() + self.assertEqual(list(simulators), [*bridge_setup.BUILTIN_SIMULATORS, "pkgsim"]) + self.assertEqual(simulators["pkgsim"].interface, "VHPIDIRECT") + self.assertIs(simulators["pkgsim"].native_library_hooks, _PackageSimulator.native_library_hooks) + + def test_the_built_in_simulators_are_served_without_hooks(self): + # Also when VUnit knows simulators that do not define the attributes + simulators = _simulators(types.SimpleNamespace(name="xsim")) + interfaces = {name: simulator.interface for name, simulator in simulators.items()} + self.assertEqual(interfaces, bridge_setup.BUILTIN_SIMULATORS) + self.assertEqual({simulator.native_library_hooks for simulator in simulators.values()}, {None}) + def test_the_interface_is_taken_from_the_simulator_class(self): for interface in ("VHPIDIRECT", "FLI", "VHPI"): - simulator = _package_simulator(foreign_language_interface=interface) - self.assertEqual(bridge_setup.foreign_language_interface(simulator), interface) + simulators = _simulators(_package_simulator(foreign_language_interface=interface)) + self.assertEqual(bridge_setup.select_simulator(simulators, "pkgsim").interface, interface) - def test_the_attributes_take_precedence_over_the_table(self): + def test_the_simulator_class_takes_precedence_over_the_built_in_simulator(self): simulator = _package_simulator(name="nvc", foreign_language_interface="FLI") - self.assertEqual(bridge_setup.foreign_language_interface(simulator), "FLI") + self.assertEqual(_simulators(simulator)["nvc"].interface, "FLI") hooks = self._register(simulator)["nvc"] # The hooks of the simulator replace the built-in ones, so the library is loaded once self.assertEqual(hooks["run_flags"](mock.Mock()), [f"--load={self.library_file!s}"]) self.assertEqual(hooks["run_env"](mock.Mock(), {})["PKGSIM"], "1") - def test_the_built_in_simulators_are_served_by_the_table(self): - for name, interface in bridge_setup.BUILTIN_SIMULATORS.items(): - simulator = types.SimpleNamespace(name=name) - self.assertEqual(bridge_setup.foreign_language_interface(simulator), interface) - def test_the_hooks_of_the_simulator_are_registered_with_the_configuration(self): hooks = self._register(_PackageSimulator) self.assertEqual(sorted(hooks), ["ghdl", "modelsim", "nvc", "pkgsim"]) @@ -486,23 +518,26 @@ def test_a_simulator_without_a_run_env_hook_is_told_the_configuration(self): self.assertEqual(result, {"KEPT": "1", simulator_hooks.CONFIG_FILE_VARIABLE: str(self.config_file)}) def test_a_simulator_without_the_attributes_is_rejected_with_what_to_define(self): + simulators = _simulators(types.SimpleNamespace(name="xsim"), _PackageSimulator) with self.assertRaisesRegex( - RuntimeError, "which xsim does not.*define foreign_language_interface.*native_library_hooks" + RuntimeError, + "requires nvc, ghdl, modelsim, rivierapro, pkgsim, .*which xsim does not.*define " + "foreign_language_interface.*native_library_hooks", ): - bridge_setup.foreign_language_interface(types.SimpleNamespace(name="xsim")) + bridge_setup.select_simulator(simulators, "xsim") def test_both_attributes_are_needed(self): for missing in ("foreign_language_interface", "native_library_hooks"): with self.assertRaisesRegex(RuntimeError, f"pkgsim simulator interface defines .* but not {missing}"): - bridge_setup.foreign_language_interface(_package_simulator(**{missing: None})) + _simulators(_package_simulator(**{missing: None})) def test_an_unknown_interface_is_rejected(self): with self.assertRaisesRegex(RuntimeError, "foreign_language_interface of the pkgsim .* is 'VHPI_DIRECT'"): - bridge_setup.foreign_language_interface(_package_simulator(foreign_language_interface="VHPI_DIRECT")) + _simulators(_package_simulator(foreign_language_interface="VHPI_DIRECT")) def test_hooks_that_cannot_be_called_are_rejected(self): with self.assertRaisesRegex(RuntimeError, "native_library_hooks of the pkgsim .* is not callable"): - bridge_setup.foreign_language_interface(_package_simulator(native_library_hooks="--load")) + _simulators(_package_simulator(native_library_hooks="--load")) def test_hooks_that_are_not_a_dict_are_rejected(self): simulator = _package_simulator(native_library_hooks=staticmethod(lambda library_file: ["--load"])) @@ -529,7 +564,10 @@ def test_the_package_setup_serves_the_simulator(self): run_script = _write_run_script(tempdir / "run.py") context = _FakeContext(_PackageSimulator, tempdir / "out", run_script, "/sim/bin") fake_bridge = bridge_setup.PythonBridge(self.library_file, self.config_file, []) - with mock.patch("vunit_python_bridge.bridge.setup", return_value=fake_bridge) as setup_mock: + with ( + mock.patch("vunit_python_bridge.bridge.setup", return_value=fake_bridge) as setup_mock, + mock.patch.object(bridge_setup, "SIMULATOR_FACTORY", _factory(_PackageSimulator)), + ): vunit_python_bridge.setup(context) self.assertEqual(setup_mock.call_args.args[2:], ("pkgsim", "/sim/bin", None, "VHPIDIRECT")) self.assertEqual(context.hooks["pkgsim"]["run_flags"](mock.Mock()), [f"--load={self.library_file!s}"]) @@ -813,7 +851,7 @@ def test_no_simulator_prefix_for_vhpidirect(self): self.assertIsNone(prepare_mock.call_args.args[1]) def test_unsupported_simulator_raises(self): - with self.assertRaisesRegex(RuntimeError, "Questa/ModelSim or Riviera-PRO, it is not supported for xsim"): + with self.assertRaisesRegex(RuntimeError, "which xsim does not"): self._setup("xsim") def test_vhpi_attributes_name_the_library_without_its_suffix_and_the_entry_point(self): @@ -1548,7 +1586,7 @@ def test_every_simulator_is_told_where_the_configuration_is(self): def _hooks(self, library_file): bridge = bridge_setup.PythonBridge(library_file=library_file, config_file=self.config_file, vhdl_files=[]) context = _FakeContext(None, Path("/out"), Path("/run.py")) - simulator_hooks.register(context, bridge) + simulator_hooks.register(context, bridge, bridge_setup.bridge_simulators()) return context.hooks @staticmethod From 1991d522ead8dcb7b72c08ecc02b2f735169f4c9 Mon Sep 17 00:00:00 2001 From: Sebastian Abrahamsson Date: Thu, 29 Oct 2026 15:40:41 +0100 Subject: [PATCH 09/10] Check and call the hooks of the selected simulator only A simulator class of another package that is half defined no longer breaks a project. Co-Authored-By: Claude Sonnet 5.5 --- docs/simulator_packages.rst | 10 +++--- src/vunit_python_bridge/__init__.py | 2 +- src/vunit_python_bridge/bridge.py | 16 ++++++--- src/vunit_python_bridge/simulator_hooks.py | 10 +++--- tests/test_python_bridge.py | 41 ++++++++++++++++------ 5 files changed, 54 insertions(+), 25 deletions(-) diff --git a/docs/simulator_packages.rst b/docs/simulator_packages.rst index 354a078..e21f256 100644 --- a/docs/simulator_packages.rst +++ b/docs/simulator_packages.rst @@ -48,7 +48,9 @@ it runs a test: The simulators of these classes are added to the built-in ones the bridge serves, and take precedence over a built-in simulator of the same ``name``. -Selecting a simulator that is not among them fails when the package is added, -with an error naming the simulators the bridge serves and the two attributes. -So do an unknown interface name, only one of the two attributes, and hooks -that are not a ``dict`` of the keys above. +Only the selected simulator is checked and has its hooks called. Selecting a +simulator that is not among them fails when the package is added, with an +error naming the simulators the bridge serves and the two attributes. So do +an unknown interface name, only one of the two attributes, and hooks that are +not a ``dict`` of the keys above. A class defining only one of the attributes +is ignored when another simulator is selected. diff --git a/src/vunit_python_bridge/__init__.py b/src/vunit_python_bridge/__init__.py index ca1d08f..771b329 100644 --- a/src/vunit_python_bridge/__init__.py +++ b/src/vunit_python_bridge/__init__.py @@ -53,8 +53,8 @@ def setup(context): from .bridge import bridge_simulators, select_simulator, setup as setup_bridge from . import simulator_hooks - simulators = bridge_simulators() simulator_name = context.simulator_name + simulators = bridge_simulators(simulator_name) if simulator_name is None: raise RuntimeError( f"The vunit-python-bridge package requires {', '.join(simulators)} but no simulator was found" diff --git a/src/vunit_python_bridge/bridge.py b/src/vunit_python_bridge/bridge.py index 9a7b287..1a6661b 100644 --- a/src/vunit_python_bridge/bridge.py +++ b/src/vunit_python_bridge/bridge.py @@ -101,7 +101,7 @@ def setup( :returns: The bridge. Its vhdl_files are to be added to the package library. """ if interface is None and simulator_name is not None: - interface = select_simulator(bridge_simulators(), simulator_name).interface + interface = select_simulator(bridge_simulators(simulator_name), simulator_name).interface check_python_build() check_windows_64bit_simulator(simulator_name, simulator_prefix) @@ -147,19 +147,25 @@ def setup( ) -def bridge_simulators() -> Dict[str, BridgeSimulator]: +def bridge_simulators(selected: Optional[str] = None) -> Dict[str, BridgeSimulator]: """ The simulators the bridge serves, by name: the built-in ones and every simulator VUnit knows, from a simulator package, whose interface class defines foreign_language_interface and - native_library_hooks. The simulator packages are added to VUnit before the bridge. + native_library_hooks. The simulator packages are added to VUnit before the bridge. Only the + attributes of the selected simulator are checked, a class defining just one of them is + otherwise not a simulator the bridge serves. """ simulators = {name: BridgeSimulator(interface, None) for name, interface in BUILTIN_SIMULATORS.items()} for simulator_class in SIMULATOR_FACTORY.supported_simulators(): interface = getattr(simulator_class, INTERFACE_ATTRIBUTE, None) hooks = getattr(simulator_class, HOOKS_ATTRIBUTE, None) - if interface is not None or hooks is not None: + if interface is None and hooks is None: + continue + if simulator_class.name == selected: _check_attributes(simulator_class.name, interface, hooks) - simulators[simulator_class.name] = BridgeSimulator(interface, hooks) + elif interface is None or hooks is None: + continue + simulators[simulator_class.name] = BridgeSimulator(interface, hooks) return simulators diff --git a/src/vunit_python_bridge/simulator_hooks.py b/src/vunit_python_bridge/simulator_hooks.py index 8e21723..0943fb1 100644 --- a/src/vunit_python_bridge/simulator_hooks.py +++ b/src/vunit_python_bridge/simulator_hooks.py @@ -38,17 +38,17 @@ def register(context, bridge: PythonBridge, simulators: Dict[str, BridgeSimulator]) -> None: """ - Register the hooks of every simulator the bridge serves with the package context. The hooks - a simulator interface gives by native_library_hooks replace the built-in ones. + Register the hooks of every built-in simulator with the package context. The hooks the selected + simulator interface gives by native_library_hooks replace the built-in ones. """ hooks = { "nvc": dict(run_flags=_nvc_run_flags(bridge), run_env=_config_run_env(bridge)), "ghdl": dict(elab_flags=_ghdl_elab_flags(bridge), run_env=_ghdl_run_env(bridge)), "modelsim": dict(process_flags=_modelsim_process_flags(), run_env=_config_run_env(bridge)), } - for name, simulator in simulators.items(): - if simulator.native_library_hooks is not None: - hooks[name] = _package_hooks(name, simulator.native_library_hooks, bridge) + selected = simulators.get(context.simulator_name) + if selected is not None and selected.native_library_hooks is not None: + hooks[context.simulator_name] = _package_hooks(context.simulator_name, selected.native_library_hooks, bridge) for simulator_name, kinds in hooks.items(): context.register_simulator_hooks(simulator_name, **kinds) if context.simulator_name == "rivierapro": diff --git a/tests/test_python_bridge.py b/tests/test_python_bridge.py index 91bd004..d924ae8 100644 --- a/tests/test_python_bridge.py +++ b/tests/test_python_bridge.py @@ -446,13 +446,13 @@ def _factory(*simulator_classes): return types.SimpleNamespace(supported_simulators=lambda: list(simulator_classes)) -def _simulators(*simulator_classes): +def _simulators(*simulator_classes, selected=None): """ The simulators the bridge serves when VUnit knows the given simulator classes, the built-in VUnit simulators left out. """ with mock.patch.object(bridge_setup, "SIMULATOR_FACTORY", _factory(*simulator_classes)): - return bridge_setup.bridge_simulators() + return bridge_setup.bridge_simulators(selected) class TestSimulatorPackages(unittest.TestCase): @@ -464,10 +464,14 @@ class TestSimulatorPackages(unittest.TestCase): library_file = Path("/cache/libvunit_python_bridge.so") config_file = Path("/out/python_bridge/vunit_python_bridge.cfg") - def _register(self, *simulator_classes): - context = _FakeContext(None, Path("/out"), Path("/run.py")) + def _register(self, simulator_class, *other_classes): + """ + The hooks registered with simulator_class selected. + """ + context = _FakeContext(simulator_class, Path("/out"), Path("/run.py")) bridge = bridge_setup.PythonBridge(self.library_file, self.config_file, []) - simulator_hooks.register(context, bridge, _simulators(*simulator_classes)) + simulators = _simulators(simulator_class, *other_classes, selected=simulator_class.name) + simulator_hooks.register(context, bridge, simulators) return context.hooks def test_the_simulators_are_collected_from_vunit(self): @@ -489,12 +493,12 @@ def test_the_built_in_simulators_are_served_without_hooks(self): def test_the_interface_is_taken_from_the_simulator_class(self): for interface in ("VHPIDIRECT", "FLI", "VHPI"): - simulators = _simulators(_package_simulator(foreign_language_interface=interface)) + simulators = _simulators(_package_simulator(foreign_language_interface=interface), selected="pkgsim") self.assertEqual(bridge_setup.select_simulator(simulators, "pkgsim").interface, interface) def test_the_simulator_class_takes_precedence_over_the_built_in_simulator(self): simulator = _package_simulator(name="nvc", foreign_language_interface="FLI") - self.assertEqual(_simulators(simulator)["nvc"].interface, "FLI") + self.assertEqual(_simulators(simulator, selected="nvc")["nvc"].interface, "FLI") hooks = self._register(simulator)["nvc"] # The hooks of the simulator replace the built-in ones, so the library is loaded once self.assertEqual(hooks["run_flags"](mock.Mock()), [f"--load={self.library_file!s}"]) @@ -529,15 +533,32 @@ def test_a_simulator_without_the_attributes_is_rejected_with_what_to_define(self def test_both_attributes_are_needed(self): for missing in ("foreign_language_interface", "native_library_hooks"): with self.assertRaisesRegex(RuntimeError, f"pkgsim simulator interface defines .* but not {missing}"): - _simulators(_package_simulator(**{missing: None})) + _simulators(_package_simulator(**{missing: None}), selected="pkgsim") + + def test_a_simulator_that_is_not_selected_is_not_checked(self): + broken = ( + _package_simulator(name="half", native_library_hooks=None), + _package_simulator(name="unknown", foreign_language_interface="VHPI_DIRECT"), + _package_simulator(name="uncallable", native_library_hooks="--load"), + ) + simulators = _simulators(*broken, selected="nvc") + self.assertNotIn("half", simulators) + # Only the hooks of the selected simulator are called + called = [] + counting = _package_simulator( + name="counting", native_library_hooks=staticmethod(lambda library_file: called.append(1) or {}) + ) + hooks = self._register(_PackageSimulator, counting, *broken) + self.assertEqual(sorted(hooks), ["ghdl", "modelsim", "nvc", "pkgsim"]) + self.assertEqual(called, []) def test_an_unknown_interface_is_rejected(self): with self.assertRaisesRegex(RuntimeError, "foreign_language_interface of the pkgsim .* is 'VHPI_DIRECT'"): - _simulators(_package_simulator(foreign_language_interface="VHPI_DIRECT")) + _simulators(_package_simulator(foreign_language_interface="VHPI_DIRECT"), selected="pkgsim") def test_hooks_that_cannot_be_called_are_rejected(self): with self.assertRaisesRegex(RuntimeError, "native_library_hooks of the pkgsim .* is not callable"): - _simulators(_package_simulator(native_library_hooks="--load")) + _simulators(_package_simulator(native_library_hooks="--load"), selected="pkgsim") def test_hooks_that_are_not_a_dict_are_rejected(self): simulator = _package_simulator(native_library_hooks=staticmethod(lambda library_file: ["--load"])) From 924030e9fef7fde2fee341ff84031b898a1599cf Mon Sep 17 00:00:00 2001 From: Sebastian Abrahamsson Date: Thu, 29 Oct 2026 15:42:28 +0100 Subject: [PATCH 10/10] Skip the test of a registered simulator class when VUnit cannot register one Co-Authored-By: Claude Sonnet 5.5 --- tests/test_python_bridge.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_python_bridge.py b/tests/test_python_bridge.py index d924ae8..7947b81 100644 --- a/tests/test_python_bridge.py +++ b/tests/test_python_bridge.py @@ -474,6 +474,7 @@ def _register(self, simulator_class, *other_classes): simulator_hooks.register(context, bridge, simulators) return context.hooks + @unittest.skipUnless(hasattr(SimulatorFactory, "register_simulator"), "VUnit has no simulator packages") def test_the_simulators_are_collected_from_vunit(self): # A simulator registered in a VUnit simulator factory, as a package does factory = SimulatorFactory()