diff --git a/README.md b/README.md index e7d6582..cfc2202 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,9 @@ the selected simulator by a foreign language interface the package builds itself called through VHPIDIRECT (NVC, GHDL) or the FLI (Questa/ModelSim), or a VHPI application built with the simulator's own compiler driver (Riviera-PRO/Active-HDL). Values cross the interface with their VHDL types: `integer`, `real`, `string`, `boolean`, `std_ulogic`, `unsigned`/`signed`, the vector -types, and `integer_array_t` as a NumPy array. +types, and `integer_array_t` as a NumPy array. A `python_object_t` is an instance of a Python class +owned by VHDL, for example the backend of a verification component, with a namespace of its own for +every instance. ## Installation diff --git a/docs/file_paths.rst b/docs/file_paths.rst index 3330d9b..bc9157e 100644 --- a/docs/file_paths.rst +++ b/docs/file_paths.rst @@ -36,12 +36,9 @@ How a file name is resolved ``pathlib.Path`` and so on - The current working directory of Python, which is the working directory of the simulator. See the warning below. - * - Python module imported by name, ``import model`` - - ``sys.path``, which holds the directory of the run script first, like - ``python run.py`` does, the directories of ``PYTHONPATH`` (the - simulator inherits the environment of VUnit, so a ``PYTHONPATH`` set - in the run script before ``vu.main()`` is seen), and, while - ``exec_file`` executes a file, the directory of that file. + * - Python module imported by name, ``import model``, and the class of a + :ref:`Python object ` + - ``sys.path``: see :ref:`python_bridge:finding_code` below. .. warning:: @@ -53,6 +50,34 @@ How a file name is resolved ``Path(__file__).parent`` in the model, or pass an absolute name from VHDL, built with ``base_path`` or ``tb_path``. +.. _python_bridge:finding_code: + +Python modules +~~~~~~~~~~~~~~ + +The interpreter in the simulator imports modules the way Python started on the +run script does. Without any setup it finds: + +* modules in the directory of the run script, the first entry of ``sys.path``. + This is also how ``import_run_script`` imports the run script; +* the packages of the Python environment VUnit runs in, for example an active + virtual environment, including packages installed with ``pip install -e``; +* while ``exec_file`` executes a file, the modules next to that file. + +Python code anywhere else is found once its directory is on ``PYTHONPATH``, +set in the run script before ``vu.main()``: the simulations inherit the +environment of VUnit (see the recipes below). A verification component used by +many testbenches can so keep its model in a directory of its own and name its +class, for example ``"my_vc_model.Model"`` for a :ref:`Python object +`. Installing the model as a package, for example with +``pip install -e``, has the same effect without changing the run script. + +There is one ``sys.path`` and one ``sys.modules`` per simulation, shared by all +:ref:`sessions `. Two modules with the same name are +therefore the same module, so a package is best named after the component it +belongs to, and state kept at module level is shared by everything that +imports the module. + Recipes ~~~~~~~ diff --git a/docs/index.rst b/docs/index.rst index d58a554..7d3de8e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -38,6 +38,8 @@ Key features ``boolean``, ``std_ulogic``, ``unsigned``/``signed``, the vector types, and ``integer_array_t`` as a NumPy array. * :doc:`Sessions ` give models and component instances Python namespaces of their own. +* :doc:`Python objects ` are instances of Python classes owned by VHDL, for example + the backends of verification components. * :doc:`Python files and modules `, including the run script itself, are executed or imported from VHDL, with :doc:`file paths ` that work for testbenches and verification components alike. @@ -111,6 +113,7 @@ become available through ``python_context``: types errors sessions + python_objects python_files file_paths @@ -121,6 +124,7 @@ become available through ``python_context``: api/python_context api/python_ffi_pkg api/python_pkg + api/python_object_pkg .. toctree:: :caption: Simulators diff --git a/docs/other_simulators.rst b/docs/other_simulators.rst index e8ac486..652a8e8 100644 --- a/docs/other_simulators.rst +++ b/docs/other_simulators.rst @@ -35,8 +35,9 @@ arguments, a 20 register status dump, wide ``call``, Python files executed with ``exec_file`` or imported with ``import_module_from_file``, two models loaded into a session each, and a Python model failing with the logger of the default session mocked. Its last -test case drives ``python_model``, a verification component whose behaviour is -the Python function in ``python_model.py`` rather than VHDL. The component finds -that file by joining its name to ``base_path``, the directory of the run script, which +test cases drive ``python_model``, a verification component whose behaviour is +the Python function in ``python_model.py`` rather than VHDL, and ``filter_vc``, +whose instances have Python objects as backends. ``python_model`` finds its file +by joining its name to ``base_path``, the directory of the run script, which ``run.py`` sets as the base directory, while the testbench gives the files it loads itself relative to its own directory. See :ref:`python_bridge:file_paths`. diff --git a/docs/python_objects.rst b/docs/python_objects.rst new file mode 100644 index 0000000..e6b1997 --- /dev/null +++ b/docs/python_objects.rst @@ -0,0 +1,268 @@ +.. _python_bridge:objects: + +Python objects +-------------- + +A ``python_object_t`` is an instance of a Python class owned by VHDL. Every +object lives in a :ref:`session ` of its own, where it +is bound to ``self``, so several objects, of the same class or not, never +share names or state. This makes an object the natural backend of a +verification component: every instance of the component gets an object of its +own, and a testbench can create one and give it to a component. + +.. code-block:: vhdl + + constant model : python_object_t := new_python_object( + "my_models.uart.UartModel", kwarg("baud", 115200), id => get_id("uart") + ); + + ... + + count := call(model, "push", arg(byte)); -- calls self.push(byte) + call(model, "reset"); -- no return value + check_true(eval_boolean(model, "self.is_idle()")); + exec(model, "self.trace = True"); + +``new_python_object(class_name, args, id)`` names the class and the arguments +of its constructor, given like the arguments of ``call``, including groups made +with ``&``. The class is + +* a dotted name like ``"package.module.Class"``, imported from the module it + names, which is found like any other import: see :ref:`python_bridge:finding_code`. + No file name is involved, so the class is found the same way wherever the + object is created. +* the name of a class defined in the default session, like ``"Class"``, for + example by ``exec`` or ``exec_file`` in the testbench, or reached through a + name defined there, like ``"np.ndarray"`` after ``exec("import numpy as np")``. + +A name that is neither, or that is not a class, is reported as an error of +the object. + +``call(object, method, ...)`` calls a method, and ``eval(object, expr)`` and +``exec(object, code)`` evaluate an expression and execute code in the session +of the object, where the object is ``self``. They have the same arguments, +result types and aliases as ``call``, ``eval`` and ``exec``, including the +procedure forms taking a ``std_ulogic_vector``, ``signed`` or ``unsigned`` +result as an ``out`` parameter. + +new_python_object or create_python_object +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Both make an object and take the same arguments. They differ in when Python +constructs it: + +.. list-table:: + :header-rows: 1 + :widths: 30 35 35 + + * - + - ``new_python_object`` + - ``create_python_object`` + * - Constructed + - On first use: the first ``call``, ``eval`` or ``exec`` + - By the call itself + * - Use it for + - Constants, generics and the handles of verification components, made + during elaboration + - Variables in a process, for example in a test case + * - A wrong class or constructor argument + - Is reported at the first use + - Is reported by ``create_python_object`` + * - During elaboration + - Works: nothing runs in Python + - Does not work: the simulator cannot run Python during elaboration + +Use ``new_python_object`` wherever the object is declared outside a process: a +constant in an architecture or a package, a generic, or the record a +verification component's constructor returns. Nothing happens in Python until +the object is used, so it does not depend on the order of the processes or on +``test_runner_setup``. + +Use ``create_python_object`` in a process when the object should exist, and +its errors be reported, at a known point, for example when a test case starts: + +.. code-block:: vhdl + + if run("Test a transfer") then + model := create_python_object("my_models.uart.UartModel", kwarg("baud", 115200)); + +Identity +~~~~~~~~ + +The ``id`` of an object names its session and its logger, +``get_logger(object)``, which reports the errors of its operations. It is +optional and mainly makes the log readable. Without an +``id``, objects are enumerated the way VUnit enumerates verification +components: ``python_bridge:python:object:1``, +``python_bridge:python:object:2`` and so on. The session of an object must +be its own, so ``new_python_object`` with an identity already used by another +object, or by a session made with ``new_session``, is reported as a failure on +the logger of the identity and returns ``null_python_object``. Using +``null_python_object`` is reported as a failure on ``python_logger``. + +From a model with module state to an object +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A model loaded with ``exec_file`` and keeping its state in module globals is +shared by everything that calls it. Move the state into a class and give every +user an object: + +.. code-block:: python + + # Before: packet_model.py, loaded with exec_file + _packets = [] + + def make_packets(seed, n): + ... + + # After: the state belongs to the instance + class PacketSource: + def __init__(self, width): + self.width = width + self.packets = [] + + def make_packets(self, seed, n): + ... + +.. code-block:: vhdl + + -- Before + exec_file("packet_model.py"); + call("make_packets", arg(seed), arg(n)); + + -- After: one object per source, found by module name + constant source : python_object_t := new_python_object( + "packet_model.PacketSource", kwarg("width", 32) + ); + ... + call(source, "make_packets", arg(seed), arg(n)); + +The class is found by its module name, so its directory must be on +``sys.path``, which for a model next to its testbench means adding that +directory to ``PYTHONPATH`` in the run script: see +:ref:`python_bridge:finding_code`. + +Backends of verification components +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A verification component usually makes its object in the constructor of its +handle, the way VUnit's own components make everything they need there. +``get_python_object(object, class_name, args, id)`` returns ``object`` when one +is given and makes a new one otherwise, like ``get_logger`` and ``get_id`` do +for loggers and identities. Every instance then gets a backend of its own, +unless the testbench gives it one: + +.. code-block:: vhdl + + type uart_master_t is record + p_std_cfg : std_cfg_t; + p_model : python_object_t; + end record; + + impure function new_uart_master( + baud : positive; model : python_object_t := null_python_object; id : id_t := null_id + ) return uart_master_t is + constant std_cfg : std_cfg_t := create_std_cfg(id, "my_vcs", "uart_master"); + begin + return ( + p_std_cfg => std_cfg, + p_model => get_python_object(model, "my_vcs.uart.UartModel", kwarg("baud", baud), get_id(std_cfg)) + ); + end; + +The component itself only calls the object of its handle: + +.. code-block:: vhdl + + entity uart_master is + generic(master : uart_master_t); + ... + + process + begin + wait until rising_edge(clk) and start = '1'; + call(master.p_model, "transmit", arg(to_integer(data))); + end process; + +The handle is a constant made during elaboration, and so is its object, but +nothing happens in Python until the first call. ``create_std_cfg`` numbers the +instances made without an ``id``, ``my_vcs:uart_master:1``, +``my_vcs:uart_master:2`` and so on, and their objects get the same identities. + +``filter_vc`` of the `embedded_python example +`__ +is a complete component of this kind: the handle is in ``filter_vc_pkg.vhd``, +the component in ``filter_vc.vhd`` and the Python model in ``filter_model.py``. +Its ``Test Python objects as backends of verification components`` test case +drives three instances, two with objects of their own and one sharing an object +with the testbench. + +A component without a handle can take the path of its instance, ``'path_name``, +as the identity, in the statement part of a process: GHDL leaves the instance +labels out of ``'path_name`` and ``'instance_name`` when they are evaluated +during elaboration, which would give all instances the same identity. +``tests/path_counter_vc.vhd`` shows this: + +.. code-block:: vhdl + + process + variable vc_id : id_t := id; + variable backend : python_object_t; + begin + if vc_id = null_id then + vc_id := get_id(path_counter_vc'path_name); + end if; + backend := get_python_object(model, "models.counter_model.Counter", kwarg("step", step), vc_id); + ... + +Several components can share an object, and so can a component and the +testbench: the testbench makes the object and gives it to each of them. A +scoreboard filled by a monitor and checked by the test is a typical case: + +.. code-block:: vhdl + + constant scoreboard : python_object_t := new_python_object("my_models.Scoreboard"); + + ... + + monitor_inst : entity work.my_monitor + generic map ( + monitor => new_my_monitor(model => scoreboard) + ) + port map ( + ... + ); + + ... + + check_equal(integer'(call(scoreboard, "num_mismatches")), 0); + +In the same way, a component can let its user choose the class, by taking its +name as a ``string`` generic. + +Errors and limitations +~~~~~~~~~~~~~~~~~~~~~~ + +* Errors are reported on the logger of the object, named after what failed: + ``call("push") failed:`` for every form of ``call``, ``eval("", ...) + failed:`` for ``eval`` and ``exec(...) failed:`` for ``exec``, followed by + the Python error. +* An object that cannot be created, for example since its class is not + found, reports the error once, as ``new_python_object("") failed:`` + or ``create_python_object("") failed:``. + Every later use of the object, calls and assignments alike, fails with + ``The object could not be created: ``. +* The caveats of sessions apply: the module of a class, and any state kept in + it, is shared by all objects. +* String arguments are passed to Python verbatim, see + :ref:`python_bridge:semantics`, which applies to constructor arguments too. +* An ``integer_array_t`` argument is transferred to Python when it is made, so + it cannot be a constructor argument of an object made during elaboration, + before the simulation starts. Give the values to the object in a call + instead: ``call(model, "load", arg(values))``. See `#17 + `__. +* An object that holds resources, such as files or threads, is closed by a + call like any other method: ``call(model, "close")``. +* Objects need sessions, and so the Python bridge: NVC, GHDL or Questa. On + Riviera-PRO/Active-HDL, ``new_python_object`` reports that as a failure on + the logger of the object and returns ``null_python_object``. diff --git a/docs/sessions.rst b/docs/sessions.rst index 8050975..21d0bf5 100644 --- a/docs/sessions.rst +++ b/docs/sessions.rst @@ -128,7 +128,7 @@ session, keeping the mutable model state on ``self`` rather than in globals: .. code-block:: vhdl - import_module_from_file(join(tb_path(runner_cfg), "accumulator.py"), "accumulator", session); + import_module_from_file(model_file, "accumulator", session); exec("model = accumulator.Accumulator()", session); y <= call("model.accumulate", arg(x), session => session); @@ -136,6 +136,9 @@ session, keeping the mutable model state on ``self`` rather than in globals: The module, and so the class, is shared by all sessions (see the caveats below), while every session has a ``model`` object of its own. Like all non-default sessions, this needs the Python bridge: NVC, GHDL or Questa. +:ref:`Python objects ` package this pattern: an object +is an instance of a class in a session of its own, with no module loading or +session handling left to the component. Caveats ~~~~~~~ diff --git a/docs/vhdl_api.py b/docs/vhdl_api.py index 1a175ee..73cdc93 100644 --- a/docs/vhdl_api.py +++ b/docs/vhdl_api.py @@ -33,6 +33,12 @@ "Arguments, ``call``, the result types implemented by the Python bridge, Python files " "and modules, and ``base_path``.", ), + ( + "python_object_pkg", + "python_object_pkg.vhd", + "Python objects: instances of Python classes owned by VHDL, each in a session of its own, " + "see :doc:`../python_objects`.", + ), ] BANNER = re.compile(r"^\s*-{10,}\s*$") diff --git a/examples/embedded_python/filter_model.py b/examples/embedded_python/filter_model.py new file mode 100644 index 0000000..0ce275f --- /dev/null +++ b/examples/embedded_python/filter_model.py @@ -0,0 +1,24 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. +# +# Copyright (c) 2014-2026, Lars Asplund lars.anders.asplund@gmail.com + +""" +The model of filter_vc: the average of the last inputs. Every filter_vc gets +an object of its own, or the one its testbench gives it. +""" + + +class MovingAverage: + def __init__(self, window=2): + self.window = window + self.samples = [] + + def push(self, x): + self.samples.append(x) + recent = self.samples[-self.window :] + return sum(recent) // len(recent) + + def num_samples(self): + return len(self.samples) diff --git a/examples/embedded_python/filter_vc.vhd b/examples/embedded_python/filter_vc.vhd new file mode 100644 index 0000000..2a7bb7b --- /dev/null +++ b/examples/embedded_python/filter_vc.vhd @@ -0,0 +1,40 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this file, +-- You can obtain one at http://mozilla.org/MPL/2.0/. +-- +-- Copyright (c) 2014-2026, Lars Asplund lars.anders.asplund@gmail.com +-- +-- A verification component whose behaviour is the Python object of its handle. +-- Every clock cycle with valid set, x is given to the object and its result is +-- the next y. The object is not created in Python until the first valid input. + +library ieee; +use ieee.std_logic_1164.all; + +library vunit_lib; +context vunit_lib.vunit_context; + +library python_bridge; +context python_bridge.python_context; + +use work.filter_vc_pkg.all; + +entity filter_vc is + generic(filter : filter_vc_t); + port( + clk : in std_logic; + valid : in std_logic; + x : in integer; + y : out integer + ); +end entity; + +architecture python of filter_vc is +begin + process(clk) + begin + if rising_edge(clk) and valid = '1' then + y <= call(get_model(filter), "push", arg(x)); + end if; + end process; +end architecture; diff --git a/examples/embedded_python/filter_vc_pkg.vhd b/examples/embedded_python/filter_vc_pkg.vhd new file mode 100644 index 0000000..8b2cc53 --- /dev/null +++ b/examples/embedded_python/filter_vc_pkg.vhd @@ -0,0 +1,42 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this file, +-- You can obtain one at http://mozilla.org/MPL/2.0/. +-- +-- Copyright (c) 2014-2026, Lars Asplund lars.anders.asplund@gmail.com +-- +-- The handle of filter_vc. Like the handles of VUnit's verification +-- components, it is made by its constructor during elaboration, here together +-- with the Python object that is the behaviour of the component. + +library vunit_lib; +context vunit_lib.vunit_context; + +library python_bridge; +context python_bridge.python_context; + +package filter_vc_pkg is + type filter_vc_t is record + p_model : python_object_t; + end record; + + -- A MovingAverage of its own, named by id, unless the testbench gives it a model + impure function new_filter_vc( + window : positive := 2; model : python_object_t := null_python_object; id : id_t := null_id + ) return filter_vc_t; + + impure function get_model(filter : filter_vc_t) return python_object_t; +end package; + +package body filter_vc_pkg is + impure function new_filter_vc( + window : positive := 2; model : python_object_t := null_python_object; id : id_t := null_id + ) return filter_vc_t is + begin + return (p_model => get_python_object(model, "filter_model.MovingAverage", kwarg("window", window), id)); + end; + + impure function get_model(filter : filter_vc_t) return python_object_t is + begin + return filter.p_model; + end; +end package body; diff --git a/examples/embedded_python/tb_example.vhd b/examples/embedded_python/tb_example.vhd index 9a200f7..5dfc689 100644 --- a/examples/embedded_python/tb_example.vhd +++ b/examples/embedded_python/tb_example.vhd @@ -11,6 +11,8 @@ use vunit_lib.random_pkg.all; library python_bridge; context python_bridge.python_context; +use work.filter_vc_pkg.all; + library ieee; use ieee.std_logic_1164.all; use ieee.math_real.all; @@ -40,6 +42,15 @@ architecture tb of tb_example is -- Ports of the two accumulator_model instances signal acc_a_x, acc_b_x : integer := 0; signal acc_a_y, acc_b_y : integer; + + -- A Python object made by the testbench and given to a filter_vc, which the + -- testbench keeps using + constant shared_average : python_object_t := new_python_object("filter_model.MovingAverage", kwarg("window", 3)); + + -- Ports of the three filter_vc instances + signal filter_valid : std_logic := '0'; + signal filter_x : integer := 0; + signal filter_a_y, filter_b_y, filter_c_y : integer; begin test_runner : process constant pi : real := 3.141592653589793; @@ -710,6 +721,22 @@ begin end loop; + elsif run("Test Python objects as backends of verification components") then + -- Every filter_vc has a MovingAverage object of its own, but filter_c_inst + -- uses the one of the testbench, which the testbench can query too + for idx in 1 to 4 loop + wait until rising_edge(clk); + filter_valid <= '1'; + filter_x <= 10 * idx; + end loop; + wait until rising_edge(clk); + filter_valid <= '0'; + wait until rising_edge(clk); + check_equal(filter_a_y, 35, result("for the average of the last 2 inputs")); + check_equal(filter_b_y, 25, result("for the average of the last 4 inputs")); + check_equal(filter_c_y, 30, result("for the average of the last 3 inputs")); + check_equal(integer'(call(shared_average, "num_samples")), 4, result("for the shared object")); + --------------------------------------------------------------------- -- Examples of string manipulation -- @@ -789,15 +816,65 @@ begin -- A component whose behaviour is a Python function python_model_inst : entity work.python_model - generic map(model_file => "python_model.py") - port map(x => model_x, y => model_y); + generic map ( + model_file => "python_model.py" + ) + port map ( + x => model_x, + y => model_y + ); -- Two instances of a component with a stateful Python model acc_a_inst : entity work.accumulator_model - generic map(model_file => "accumulator_model.py") - port map(x => acc_a_x, y => acc_a_y); + generic map ( + model_file => "accumulator_model.py" + ) + port map ( + x => acc_a_x, + y => acc_a_y + ); acc_b_inst : entity work.accumulator_model - generic map(model_file => "accumulator_model.py") - port map(x => acc_b_x, y => acc_b_y); + generic map ( + model_file => "accumulator_model.py" + ) + port map ( + x => acc_b_x, + y => acc_b_y + ); + + -- Verification components whose behaviour is a Python object: two of their + -- own, one named by an id and one enumerated, and the one of the testbench + filter_a_inst : entity work.filter_vc + generic map ( + filter => new_filter_vc(window => 2, id => get_id("filter_a")) + ) + port map ( + clk => clk, + valid => filter_valid, + x => filter_x, + y => filter_a_y + ); + + filter_b_inst : entity work.filter_vc + generic map ( + filter => new_filter_vc(window => 4) + ) + port map ( + clk => clk, + valid => filter_valid, + x => filter_x, + y => filter_b_y + ); + + filter_c_inst : entity work.filter_vc + generic map ( + filter => new_filter_vc(model => shared_average) + ) + port map ( + clk => clk, + valid => filter_valid, + x => filter_x, + y => filter_c_y + ); end; diff --git a/release_notes/unreleased.md b/release_notes/unreleased.md index ab7fb01..f64c0c3 100644 --- a/release_notes/unreleased.md +++ b/release_notes/unreleased.md @@ -1,3 +1,7 @@ +## New features + +- `python_object_t`: an instance of a Python class owned by VHDL, made with `new_python_object` and created on first use in a session of its own and called with `call(object, "method", ...)` and `eval(object, "expr")`. `get_python_object(object, class_name, args, id)` gives a verification component a backend of its own unless the testbench gives it one. See "Python objects" in the user guide. + ## Changes - `import_module_from_file` resolves a relative path against the directory of the testbench file, like `exec_file`, rather than against the working directory of the simulator. It must be called after `test_runner_setup`. Callers that relied on a path relative to the working directory must pass an absolute path instead. See #7. diff --git a/src/vunit_python_bridge/hdl/src/python_context.vhd b/src/vunit_python_bridge/hdl/src/python_context.vhd index c70bb14..f6f7114 100644 --- a/src/vunit_python_bridge/hdl/src/python_context.vhd +++ b/src/vunit_python_bridge/hdl/src/python_context.vhd @@ -10,4 +10,5 @@ context python_context is library python_bridge; use python_bridge.python_pkg.all; use python_bridge.python_ffi_pkg.all; + use python_bridge.python_object_pkg.all; end context; diff --git a/src/vunit_python_bridge/hdl/src/python_ffi_pkg_bridge.vhd b/src/vunit_python_bridge/hdl/src/python_ffi_pkg_bridge.vhd index 3b2e491..a3adea8 100644 --- a/src/vunit_python_bridge/hdl/src/python_ffi_pkg_bridge.vhd +++ b/src/vunit_python_bridge/hdl/src/python_ffi_pkg_bridge.vhd @@ -22,6 +22,7 @@ -- reporting a failure. library vunit_lib; +use vunit_lib.dict_pkg.all; use vunit_lib.id_pkg.all; use vunit_lib.integer_array_pkg.all; use vunit_lib.integer_vector_ptr_pkg.all; @@ -109,6 +110,12 @@ package python_ffi_pkg is ----------------------------------------------------------------------------- -- Private, the primitives the bridge operations of python_pkg are built on ----------------------------------------------------------------------------- + -- Whether this simulator has sessions, and with them Python objects + constant p_sessions_supported : boolean := true; + + -- Whether new_session has made a session with the identity + impure function p_has_session(id : id_t) return boolean; + -- Result kinds, must match src/vunit_python_bridge/runtime.py constant p_kind_integer : integer := 0; constant p_kind_real : integer := 1; @@ -169,6 +176,7 @@ package python_ffi_pkg is -- The result of the last evaluation impure function p_result_integer return integer; + impure function p_result_real return real; impure function p_result_string return string; impure function p_result_integer_vector return integer_vector; impure function p_result_real_vector return real_vector; @@ -189,11 +197,20 @@ package body python_ffi_pkg is return new_session(get_id(name, parent => p_python_id)); end; + -- The full names of the identities new_session has made sessions with + constant p_session_ids : dict_t := new_dict; + impure function new_session(id : id_t) return python_session_t is begin + set_string(p_session_ids, full_name(id), ""); return (p_data => new_integer_vector_ptr(1, value => to_integer(id))); end; + impure function p_has_session(id : id_t) return boolean is + begin + return has_key(p_session_ids, full_name(id)); + end; + impure function get_id(session : python_session_t) return id_t is begin return to_id(get(session.p_data, 0)); @@ -335,6 +352,11 @@ package body python_ffi_pkg is return vpy_result_integer; end; + impure function p_result_real return real is + begin + return vpy_result_real; + end; + impure function p_result_string return string is constant len : natural := vpy_result_meta(0); variable result : string(1 to len); diff --git a/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd b/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd new file mode 100644 index 0000000..4d6df19 --- /dev/null +++ b/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd @@ -0,0 +1,682 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this file, +-- You can obtain one at http://mozilla.org/MPL/2.0/. +-- +-- Copyright (c) 2014-2026, Lars Asplund lars.anders.asplund@gmail.com +-- +-- Python objects owned by VHDL. An object is an instance of a Python class +-- living in a session of its own, where it is self, created on first use. It +-- is typically the backend of a verification component, see "Python objects" +-- in the user guide. +-- +-- The call and eval overloads of an object are those of python_pkg with the +-- object first, generated from the same result tables. +-- +-- This file is generated from tools/python_object_pkg.vhd.in by +-- src/vunit_python_bridge/hdl/tools/generate_python_pkg.py. Do not edit. + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; + +library vunit_lib; +use vunit_lib.dict_pkg.all; +use vunit_lib.integer_array_pkg.all; +use vunit_lib.id_pkg.all; +use vunit_lib.logger_pkg.all; +use vunit_lib.integer_vector_ptr_pkg.all; +use vunit_lib.string_ptr_pkg.all; + +use work.python_ffi_pkg.all; +use work.python_pkg.all; + +package python_object_pkg is + type python_object_t is record + p_data : integer_vector_ptr_t; + end record; + constant null_python_object : python_object_t := (p_data => null_ptr); + + -- An instance of class_name, a dotted name like "package.module.Class" or the + -- name of a class defined in the default session, constructed with args on first use. + -- The identity names the object and its session. Without one, objects are + -- enumerated like VUnit's verification components: python_bridge:python:object:. + -- An identity already used by an object or a session, and a simulator + -- without sessions (Riviera-PRO/Active-HDL), are reported as a failure on the + -- logger of the identity, and null_python_object is returned. + impure function new_python_object(class_name : string; args : arg_t := null_arg; id : id_t := null_id) + return python_object_t; + + -- Like new_python_object, but the object is constructed by this call rather + -- than on first use, so a wrong class or constructor argument is reported here. + -- Only in a process: Python cannot run during elaboration, so a constant or a + -- generic is made with new_python_object. + impure function create_python_object(class_name : string; args : arg_t := null_arg; id : id_t := null_id) + return python_object_t; + + -- object when it is given, else a new object, like get_logger and get_id: + -- the default backend of a verification component that can also be given one + impure function get_python_object( + object : python_object_t; class_name : string; args : arg_t := null_arg; id : id_t := null_id + ) return python_object_t; + + impure function get_id(object : python_object_t) return id_t; + -- The logger of the object, which reports the errors of its operations + impure function get_logger(object : python_object_t) return logger_t; + -- The session of the object, in which it is bound to self + impure function get_session(object : python_object_t) return python_session_t; + + -- Execute code in the session of the object, where it is self, like exec + procedure exec(object : python_object_t; code : string); + + -- Call a method of the object, like call + procedure call( + object : python_object_t; method : string; + arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg + ); + + impure function call_integer_w_arg( + object : python_object_t; method : string; + arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg + ) return integer; + alias call is call_integer_w_arg[ + python_object_t, string, arg_t, arg_t, arg_t, arg_t, arg_t, + arg_t, arg_t, arg_t, arg_t, arg_t return integer]; + + impure function call_real( + object : python_object_t; method : string; + arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg + ) return real; + alias call is call_real[ + python_object_t, string, arg_t, arg_t, arg_t, arg_t, arg_t, + arg_t, arg_t, arg_t, arg_t, arg_t return real]; + + impure function call_integer_vector( + object : python_object_t; method : string; + arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg + ) return integer_vector; + alias call is call_integer_vector[ + python_object_t, string, arg_t, arg_t, arg_t, arg_t, arg_t, + arg_t, arg_t, arg_t, arg_t, arg_t return integer_vector]; + + impure function call_real_vector( + object : python_object_t; method : string; + arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg + ) return real_vector; + alias call is call_real_vector[ + python_object_t, string, arg_t, arg_t, arg_t, arg_t, arg_t, + arg_t, arg_t, arg_t, arg_t, arg_t return real_vector]; + + impure function call_string( + object : python_object_t; method : string; + arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg + ) return string; + alias call is call_string[ + python_object_t, string, arg_t, arg_t, arg_t, arg_t, arg_t, + arg_t, arg_t, arg_t, arg_t, arg_t return string]; + + impure function call_integer_vector_ptr( + object : python_object_t; method : string; + arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg + ) return integer_vector_ptr_t; + alias call is call_integer_vector_ptr[ + python_object_t, string, arg_t, arg_t, arg_t, arg_t, arg_t, + arg_t, arg_t, arg_t, arg_t, arg_t return integer_vector_ptr_t]; + + impure function call_boolean( + object : python_object_t; method : string; + arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg + ) return boolean; + alias call is call_boolean[ + python_object_t, string, arg_t, arg_t, arg_t, arg_t, arg_t, + arg_t, arg_t, arg_t, arg_t, arg_t return boolean]; + + impure function call_std_ulogic( + object : python_object_t; method : string; + arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg + ) return std_ulogic; + alias call is call_std_ulogic[ + python_object_t, string, arg_t, arg_t, arg_t, arg_t, arg_t, + arg_t, arg_t, arg_t, arg_t, arg_t return std_ulogic]; + + impure function call_std_ulogic_vector( + object : python_object_t; method : string; + arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg + ) return std_ulogic_vector; + + impure function call_integer_array( + object : python_object_t; method : string; + arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg + ) return integer_array_t; + alias call is call_integer_array[ + python_object_t, string, arg_t, arg_t, arg_t, arg_t, arg_t, + arg_t, arg_t, arg_t, arg_t, arg_t return integer_array_t]; + + procedure call_std_ulogic_vector( + object : python_object_t; method : string; result : out std_ulogic_vector; + arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg + ); + procedure call_signed( + object : python_object_t; method : string; result : out signed; + arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg + ); + procedure call_unsigned( + object : python_object_t; method : string; result : out unsigned; + arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg + ); + + -- Evaluate an expression in the session of the object, where it is self, like eval + impure function eval_integer(object : python_object_t; expr : string) return integer; + alias eval is eval_integer[python_object_t, string return integer]; + impure function eval_real(object : python_object_t; expr : string) return real; + alias eval is eval_real[python_object_t, string return real]; + impure function eval_integer_vector(object : python_object_t; expr : string) return integer_vector; + alias eval is eval_integer_vector[python_object_t, string return integer_vector]; + impure function eval_real_vector(object : python_object_t; expr : string) return real_vector; + alias eval is eval_real_vector[python_object_t, string return real_vector]; + impure function eval_string(object : python_object_t; expr : string) return string; + alias eval is eval_string[python_object_t, string return string]; + impure function eval_integer_vector_ptr(object : python_object_t; expr : string) return integer_vector_ptr_t; + alias eval is eval_integer_vector_ptr[python_object_t, string return integer_vector_ptr_t]; + impure function eval_boolean(object : python_object_t; expr : string) return boolean; + alias eval is eval_boolean[python_object_t, string return boolean]; + impure function eval_std_ulogic(object : python_object_t; expr : string) return std_ulogic; + alias eval is eval_std_ulogic[python_object_t, string return std_ulogic]; + impure function eval_std_ulogic_vector(object : python_object_t; expr : string) return std_ulogic_vector; + impure function eval_integer_array(object : python_object_t; expr : string) return integer_array_t; + procedure eval_std_ulogic_vector(object : python_object_t; expr : string; result : out std_ulogic_vector); + procedure eval_signed(object : python_object_t; expr : string; result : out signed); + procedure eval_unsigned(object : python_object_t; expr : string; result : out unsigned); +end package; + +package body python_object_pkg is + constant session_idx : natural := 0; + constant code_idx : natural := 1; + constant created_idx : natural := 2; + constant operation_idx : natural := 3; + -- The parent of the identities of the objects created without one + constant anonymous_id : id_t := get_id("object", parent => p_python_id); + + -- The defaults of the call results of a failed call + constant p_no_integers : integer_vector(0 downto 1) := (others => 0); + constant p_no_reals : real_vector(0 downto 1) := (others => 0.0); + + impure function p_to_integer_vector_ptr(values : integer_vector) return integer_vector_ptr_t is + constant normalized : integer_vector(0 to values'length - 1) := values; + constant result : integer_vector_ptr_t := new_integer_vector_ptr(values'length); + begin + for idx in normalized'range loop + set(result, idx, normalized(idx)); + end loop; + return result; + end; + + -- new_python_object and create_python_object, operation naming the one called + impure function p_new_python_object(class_name : string; args : arg_t; id : id_t; operation : string) + return python_object_t is + variable object_id : id_t := id; + variable data : integer_vector_ptr_t; + begin + if object_id = null_id then + -- Like enumerate of vc_pkg, which is only there with the verification components + object_id := get_id(to_string(num_children(anonymous_id) + 1), parent => anonymous_id); + end if; + if not p_sessions_supported then + failure(get_logger(object_id), operation & " failed:" & LF & "Python objects require NVC, GHDL or Questa."); + return null_python_object; + end if; + -- The session of the object is that of its identity, which must be its own + if p_has_session(object_id) then + failure( + get_logger(object_id), + operation & " failed:" & LF & "The identity " & full_name(object_id) & + " is already used by another Python object or session, give each object an id of its own." + ); + return null_python_object; + end if; + data := new_integer_vector_ptr(4); + set(data, session_idx, to_integer(new_session(object_id).p_data)); + set(data, code_idx, to_integer(new_string_ptr( + to_call_str("__vunit__.instantiate_as_self", arg(class_name), args) + ))); + set(data, operation_idx, to_integer(new_string_ptr(operation))); + return (p_data => data); + end; + + impure function new_python_object(class_name : string; args : arg_t := null_arg; id : id_t := null_id) + return python_object_t is + begin + return p_new_python_object(class_name, args, id, "new_python_object(""" & class_name & """)"); + end; + + impure function get_python_object( + object : python_object_t; class_name : string; args : arg_t := null_arg; id : id_t := null_id + ) return python_object_t is + begin + if object /= null_python_object then + return object; + end if; + return new_python_object(class_name, args, id); + end; + + -- Report the use of null_python_object, e.g. the result of a failed new_python_object + impure function p_is_null(object : python_object_t) return boolean is + begin + if object = null_python_object then + failure(python_logger, "null_python_object was used, it is not an object"); + return true; + end if; + return false; + end; + + -- The session of null_python_object, where nothing is bound to self + constant null_session : python_session_t := new_session(get_id("null_object", parent => p_python_id)); + + impure function get_session(object : python_object_t) return python_session_t is + begin + if p_is_null(object) then + return null_session; + end if; + return (p_data => to_integer_vector_ptr(get(object.p_data, session_idx))); + end; + + impure function get_id(object : python_object_t) return id_t is + begin + return get_id(get_session(object)); + end; + + -- Construct the object unless it is constructed. A failure is reported once, as + -- the failure of new_python_object or create_python_object, and the object + -- then raises it on every use. + procedure p_create(object : python_object_t) is + variable ok : boolean; + begin + if p_is_null(object) then + return; + end if; + if get(object.p_data, created_idx) = 0 then + set(object.p_data, created_idx, 1); + ok := p_exec( + to_string(to_string_ptr(get(object.p_data, code_idx))), 0, + to_string(to_string_ptr(get(object.p_data, operation_idx))), + get_session(object) + ); + end if; + end; + + impure function create_python_object(class_name : string; args : arg_t := null_arg; id : id_t := null_id) + return python_object_t is + constant object : python_object_t := + p_new_python_object(class_name, args, id, "create_python_object(""" & class_name & """)"); + begin + if object /= null_python_object then + p_create(object); + end if; + return object; + end; + + -- Create the object if needed. False for null_python_object, which is reported. + impure function p_created(object : python_object_t) return boolean is + begin + if object = null_python_object then + return not p_is_null(object); + end if; + p_create(object); + return true; + end; + + procedure exec(object : python_object_t; code : string) is + begin + if p_created(object) then + exec(code, get_session(object)); + end if; + end; + + impure function get_logger(object : python_object_t) return logger_t is + begin + return get_logger(get_id(object)); + end; + + -- The name of a method call in error messages + function p_call_operation(method : string) return string is + begin + return "call(""" & method & """)"; + end; + + procedure call( + object : python_object_t; method : string; + arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg + ) is + variable ok : boolean; + begin + if not p_created(object) then + return; + end if; + -- An error is reported as that of the method rather than of the code executed + ok := p_exec( + to_call_str("self." & method, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10), + 0, p_call_operation(method), get_session(object) + ); + end; + + impure function call_integer_w_arg( + object : python_object_t; method : string; + arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg + ) return integer is + begin + if not p_created(object) then + return integer'low; + end if; + if p_eval( + to_call_str("self." & method, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10), + p_kind_integer, -1, p_call_operation(method), get_session(object) + ) then + return p_result_integer; + end if; + return integer'low; + end; + + impure function call_real( + object : python_object_t; method : string; + arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg + ) return real is + begin + if not p_created(object) then + return 0.0; + end if; + if p_eval( + to_call_str("self." & method, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10), + p_kind_real, -1, p_call_operation(method), get_session(object) + ) then + return p_result_real; + end if; + return 0.0; + end; + + impure function call_integer_vector( + object : python_object_t; method : string; + arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg + ) return integer_vector is + begin + if not p_created(object) then + return p_no_integers; + end if; + if p_eval( + to_call_str("self." & method, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10), + p_kind_integer_vector, -1, p_call_operation(method), get_session(object) + ) then + return p_result_integer_vector; + end if; + return p_no_integers; + end; + + impure function call_real_vector( + object : python_object_t; method : string; + arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg + ) return real_vector is + begin + if not p_created(object) then + return p_no_reals; + end if; + if p_eval( + to_call_str("self." & method, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10), + p_kind_real_vector, -1, p_call_operation(method), get_session(object) + ) then + return p_result_real_vector; + end if; + return p_no_reals; + end; + + impure function call_string( + object : python_object_t; method : string; + arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg + ) return string is + begin + if not p_created(object) then + return ""; + end if; + if p_eval( + to_call_str("self." & method, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10), + p_kind_string, -1, p_call_operation(method), get_session(object) + ) then + return p_result_string; + end if; + return ""; + end; + + impure function call_integer_vector_ptr( + object : python_object_t; method : string; + arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg + ) return integer_vector_ptr_t is + begin + if not p_created(object) then + return null_ptr; + end if; + if p_eval( + to_call_str("self." & method, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10), + p_kind_integer_vector, -1, p_call_operation(method), get_session(object) + ) then + return p_to_integer_vector_ptr(p_result_integer_vector); + end if; + return null_ptr; + end; + + impure function call_boolean( + object : python_object_t; method : string; + arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg + ) return boolean is + begin + if not p_created(object) then + return false; + end if; + if p_eval( + to_call_str("self." & method, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10), + p_kind_boolean, -1, p_call_operation(method), get_session(object) + ) then + return p_result_integer /= 0; + end if; + return false; + end; + + impure function call_std_ulogic( + object : python_object_t; method : string; + arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg + ) return std_ulogic is + begin + if not p_created(object) then + return 'U'; + end if; + if p_eval( + to_call_str("self." & method, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10), + p_kind_std_ulogic, -1, p_call_operation(method), get_session(object) + ) then + return p_to_std_ulogic(p_result_string(1)); + end if; + return 'U'; + end; + + impure function call_std_ulogic_vector( + object : python_object_t; method : string; + arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg + ) return std_ulogic_vector is + begin + if not p_created(object) then + return ""; + end if; + if p_eval( + to_call_str("self." & method, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10), + p_kind_std_ulogic_vector, -1, p_call_operation(method), get_session(object) + ) then + return p_to_std_ulogic_vector(p_result_string); + end if; + return ""; + end; + + impure function call_integer_array( + object : python_object_t; method : string; + arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg + ) return integer_array_t is + begin + if not p_created(object) then + return null_integer_array; + end if; + if p_eval( + to_call_str("self." & method, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10), + p_kind_integer_array, -1, p_call_operation(method), get_session(object) + ) then + return p_result_integer_array; + end if; + return null_integer_array; + end; + + procedure call_std_ulogic_vector( + object : python_object_t; method : string; result : out std_ulogic_vector; + arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg + ) is + begin + if not p_created(object) then + return; + end if; + if p_eval( + to_call_str("self." & method, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10), + p_kind_std_ulogic_vector, result'length, p_call_operation(method), get_session(object) + ) then + result := p_to_std_ulogic_vector(p_result_string); + end if; + end; + + procedure call_signed( + object : python_object_t; method : string; result : out signed; + arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg + ) is + begin + if not p_created(object) then + return; + end if; + if p_eval( + to_call_str("self." & method, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10), + p_kind_signed, result'length, p_call_operation(method), get_session(object) + ) then + result := signed(p_to_std_ulogic_vector(p_result_string)); + end if; + end; + + procedure call_unsigned( + object : python_object_t; method : string; result : out unsigned; + arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg + ) is + begin + if not p_created(object) then + return; + end if; + if p_eval( + to_call_str("self." & method, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10), + p_kind_unsigned, result'length, p_call_operation(method), get_session(object) + ) then + result := unsigned(p_to_std_ulogic_vector(p_result_string)); + end if; + end; + + impure function eval_integer(object : python_object_t; expr : string) return integer is + begin + if not p_created(object) then + return integer'low; + end if; + return eval_integer(expr, session => get_session(object)); + end; + + impure function eval_real(object : python_object_t; expr : string) return real is + begin + if not p_created(object) then + return 0.0; + end if; + return eval_real(expr, session => get_session(object)); + end; + + impure function eval_integer_vector(object : python_object_t; expr : string) return integer_vector is + begin + if not p_created(object) then + return p_no_integers; + end if; + return eval_integer_vector(expr, session => get_session(object)); + end; + + impure function eval_real_vector(object : python_object_t; expr : string) return real_vector is + begin + if not p_created(object) then + return p_no_reals; + end if; + return eval_real_vector(expr, session => get_session(object)); + end; + + impure function eval_string(object : python_object_t; expr : string) return string is + begin + if not p_created(object) then + return ""; + end if; + return eval_string(expr, session => get_session(object)); + end; + + impure function eval_integer_vector_ptr(object : python_object_t; expr : string) return integer_vector_ptr_t is + begin + if not p_created(object) then + return null_ptr; + end if; + return eval_integer_vector_ptr(expr, session => get_session(object)); + end; + + impure function eval_boolean(object : python_object_t; expr : string) return boolean is + begin + if not p_created(object) then + return false; + end if; + return eval_boolean(expr, session => get_session(object)); + end; + + impure function eval_std_ulogic(object : python_object_t; expr : string) return std_ulogic is + begin + if not p_created(object) then + return 'U'; + end if; + return eval_std_ulogic(expr, session => get_session(object)); + end; + + impure function eval_std_ulogic_vector(object : python_object_t; expr : string) return std_ulogic_vector is + begin + if not p_created(object) then + return ""; + end if; + return eval_std_ulogic_vector(expr, session => get_session(object)); + end; + + impure function eval_integer_array(object : python_object_t; expr : string) return integer_array_t is + begin + if not p_created(object) then + return null_integer_array; + end if; + return eval_integer_array(expr, session => get_session(object)); + end; + + procedure eval_std_ulogic_vector(object : python_object_t; expr : string; result : out std_ulogic_vector) is + begin + if not p_created(object) then + return; + end if; + eval_std_ulogic_vector(expr, result, session => get_session(object)); + end; + + procedure eval_signed(object : python_object_t; expr : string; result : out signed) is + begin + if not p_created(object) then + return; + end if; + eval_signed(expr, result, session => get_session(object)); + end; + + procedure eval_unsigned(object : python_object_t; expr : string; result : out unsigned) is + begin + if not p_created(object) then + return; + end if; + eval_unsigned(expr, result, session => get_session(object)); + end; +end package body; diff --git a/src/vunit_python_bridge/hdl/src/python_pkg.vhd b/src/vunit_python_bridge/hdl/src/python_pkg.vhd index 04c659d..7fc5272 100644 --- a/src/vunit_python_bridge/hdl/src/python_pkg.vhd +++ b/src/vunit_python_bridge/hdl/src/python_pkg.vhd @@ -280,6 +280,10 @@ package python_pkg is -- or VUNIT_PYTHON_BRIDGE_FILE_BASE. Without one, a failure is reported on -- python_logger and "" is returned. impure function base_path return string; + + -- Private, the conversions of the results of std_ulogic types, shared with python_object_pkg + function p_to_std_ulogic(value : character) return std_ulogic; + function p_to_std_ulogic_vector(value : string) return std_ulogic_vector; end package; package body python_pkg is diff --git a/src/vunit_python_bridge/hdl/src/python_pkg_vhpi.vhd b/src/vunit_python_bridge/hdl/src/python_pkg_vhpi.vhd index 0512c4a..1c12cdd 100644 --- a/src/vunit_python_bridge/hdl/src/python_pkg_vhpi.vhd +++ b/src/vunit_python_bridge/hdl/src/python_pkg_vhpi.vhd @@ -9,6 +9,7 @@ use std.textio.all; library vunit_lib; +use vunit_lib.dict_pkg.all; use vunit_lib.id_pkg.all; use vunit_lib.integer_array_pkg.all; use vunit_lib.integer_vector_ptr_pkg.all; @@ -80,11 +81,22 @@ package python_ffi_pkg is procedure p_exec(code : string); attribute foreign of p_exec : procedure is "VHPI libraries/python exec"; + -- Only for the declarations python_object_pkg shares with the Python bridge: + -- objects need sessions, which are only supported there + impure function p_exec( + text : string; is_file : integer; operation : string; session : python_session_t := default_session + ) return boolean; procedure exec(code : string; session : python_session_t := default_session); ----------------------------------------------------------------------------- -- Private, the primitives the bridge operations of python_pkg are built on ----------------------------------------------------------------------------- + -- Whether this simulator has sessions, and with them Python objects + constant p_sessions_supported : boolean := false; + + -- Whether new_session has made a session with the identity + impure function p_has_session(id : id_t) return boolean; + -- Some operations of python_pkg are implemented by the VUnit Python -- bridge, which is only available for NVC, GHDL and Questa. The primitives -- below are declared so that python_pkg has one body for every simulator, @@ -130,7 +142,10 @@ package python_ffi_pkg is -- The result of the last evaluation impure function p_result_integer return integer; + impure function p_result_real return real; impure function p_result_string return string; + impure function p_result_integer_vector return integer_vector; + impure function p_result_real_vector return real_vector; impure function p_result_integer_array return integer_array_t; -- Transfer an integer_array_t to Python and return the id it is staged @@ -144,11 +159,20 @@ package body python_ffi_pkg is return new_session(get_id(name, parent => p_python_id)); end; + -- The full names of the identities new_session has made sessions with + constant p_session_ids : dict_t := new_dict; + impure function new_session(id : id_t) return python_session_t is begin + set_string(p_session_ids, full_name(id), ""); return (p_data => new_integer_vector_ptr(1, value => to_integer(id))); end; + impure function p_has_session(id : id_t) return boolean is + begin + return has_key(p_session_ids, full_name(id)); + end; + impure function get_id(session : python_session_t) return id_t is begin return to_id(get(session.p_data, 0)); @@ -171,6 +195,15 @@ package body python_ffi_pkg is end if; end; + impure function p_exec( + text : string; is_file : integer; operation : string; session : python_session_t := default_session + ) return boolean is + begin + -- python_object_pkg, the only user, fails before an object is used here + failure(python_logger, operation & " requires NVC, GHDL or Questa"); + return false; + end; + impure function eval_integer(expr : string; session : python_session_t := default_session) return integer is begin p_check_session(session); @@ -263,12 +296,32 @@ package body python_ffi_pkg is return integer'low; end; + impure function p_result_real return real is + begin + p_unsupported("p_result_real"); + return 0.0; + end; + impure function p_result_string return string is begin p_unsupported("p_result_string"); return ""; end; + impure function p_result_integer_vector return integer_vector is + constant no_integers : integer_vector(0 downto 1) := (others => 0); + begin + p_unsupported("p_result_integer_vector"); + return no_integers; + end; + + impure function p_result_real_vector return real_vector is + constant no_reals : real_vector(0 downto 1) := (others => 0.0); + begin + p_unsupported("p_result_real_vector"); + return no_reals; + end; + impure function p_result_integer_array return integer_array_t is begin p_unsupported("p_result_integer_array"); diff --git a/src/vunit_python_bridge/hdl/tools/generate_python_pkg.py b/src/vunit_python_bridge/hdl/tools/generate_python_pkg.py index 8dc3252..9def0b9 100644 --- a/src/vunit_python_bridge/hdl/tools/generate_python_pkg.py +++ b/src/vunit_python_bridge/hdl/tools/generate_python_pkg.py @@ -5,15 +5,16 @@ # Copyright (c) 2014-2026, Lars Asplund lars.anders.asplund@gmail.com """ -Generate python_pkg.vhd from python_pkg.vhd.in in this directory. - -The template holds the parts of python_pkg that are written by hand. The -generated parts are the argument value types of call, the result types of eval -and call and the execution of Python files. They are mostly one overload per -type, which is why they are generated. The operations that are implemented by -the Python bridge (NVC, GHDL and Questa) are built on the p_ prefixed -primitives of python_ffi_pkg, which the VHPI variant of that package -implements by reporting a failure. +Generate python_pkg.vhd and python_object_pkg.vhd from the templates in this directory. + +Each template holds the parts of its package that are written by hand. The +generated parts of python_pkg are the argument value types of call, the result +types of eval and call and the execution of Python files. Those of +python_object_pkg are the call and eval overloads of a python_object_t, from the +same result tables. They are mostly one overload per type, which is why they are +generated. The operations that are implemented by the Python bridge (NVC, GHDL +and Questa) are built on the p_ prefixed primitives of python_ffi_pkg, which the +VHPI variant of that package implements by reporting a failure. """ from pathlib import Path @@ -75,6 +76,8 @@ name="integer", vhdl="integer", kind="p_kind_integer", + value="p_result_integer", + default="integer'low", hand_written_eval=True, hand_written_call=True, ), @@ -82,6 +85,8 @@ name="real", vhdl="real", kind="p_kind_real", + value="p_result_real", + default="0.0", hand_written_eval=True, hand_written_call=True, ), @@ -89,6 +94,8 @@ name="integer_vector", vhdl="integer_vector", kind="p_kind_integer_vector", + value="p_result_integer_vector", + default="p_no_integers", hand_written_eval=True, hand_written_call=True, ), @@ -96,6 +103,8 @@ name="real_vector", vhdl="real_vector", kind="p_kind_real_vector", + value="p_result_real_vector", + default="p_no_reals", hand_written_eval=True, hand_written_call=False, ), @@ -103,6 +112,8 @@ name="string", vhdl="string", kind="p_kind_string", + value="p_result_string", + default='""', hand_written_eval=True, hand_written_call=False, ), @@ -110,6 +121,8 @@ name="integer_vector_ptr", vhdl="integer_vector_ptr_t", kind="p_kind_integer_vector", + value="p_to_integer_vector_ptr(p_result_integer_vector)", + default="null_ptr", hand_written_eval=True, hand_written_call=False, ), @@ -363,6 +376,161 @@ def call_subprograms(): return "\n".join(parts).rstrip("\n") +# ------------------------------------------------------------------------- +# call and eval of an object +# ------------------------------------------------------------------------- +OBJECT = "object : python_object_t" +OBJECT_ARG_SIGNATURE = ", ".join(["python_object_t", "string"] + ["arg_t"] * len(ARGS)) + + +def object_declarations(): + """ + Declarations of the call and eval overloads of an object: those of + python_pkg with the object first, the method or expression second. + """ + lines = [ + " -- Call a method of the object, like call", + " procedure call(", + f" {OBJECT}; method : string;", + f" {ARG_PARAMETERS}", + " );", + "", + ] + for result in RESULTS: + name = call_name(result) + lines.append(f" impure function {name}(") + lines.append(f" {OBJECT}; method : string;") + lines.append(f" {ARG_PARAMETERS}") + lines.append(f" ) return {result['vhdl']};") + if result.get("alias_call", True): + # The signature over two lines, to keep them short + signature = OBJECT_ARG_SIGNATURE.split(", ") + lines.append(f" alias call is {name}[") + lines.append(f" {', '.join(signature[:7])},") + lines.append(f" {', '.join(signature[7:])} return {result['vhdl']}];") + lines.append("") + for result in PROCEDURE_RESULTS: + lines.append(f" procedure call_{result['name']}(") + lines.append(f" {OBJECT}; method : string; result : out {result['vhdl']};") + lines.append(f" {ARG_PARAMETERS}") + lines.append(" );") + lines += ["", " -- Evaluate an expression in the session of the object, where it is self, like eval"] + for result in RESULTS: + lines.append(f" impure function eval_{result['name']}({OBJECT}; expr : string) return {result['vhdl']};") + if result.get("alias_eval", True): + lines.append(f" alias eval is eval_{result['name']}[python_object_t, string return {result['vhdl']}];") + for result in PROCEDURE_RESULTS: + lines.append(f" procedure eval_{result['name']}({OBJECT}; expr : string; result : out {result['vhdl']});") + return "\n".join(lines) + + +def object_subprograms(): + """ + Bodies of the call and eval overloads of an object. Each creates the object + if needed and performs the operation in the session of the object. The call + overloads report their errors as call("") rather than as the code + they evaluate, like the call procedure. + """ + session = "session => get_session(object)" + parts = [ + f"""\ + procedure call( + {OBJECT}; method : string; + {ARG_PARAMETERS} + ) is + variable ok : boolean; + begin + if not p_created(object) then + return; + end if; + -- An error is reported as that of the method rather than of the code executed + ok := p_exec( + to_call_str("self." & method, {ARG_ACTUALS}), + 0, p_call_operation(method), get_session(object) + ); + end; +""" + ] + for result in RESULTS: + name = call_name(result) + parts.append( + f"""\ + impure function {name}( + {OBJECT}; method : string; + {ARG_PARAMETERS} + ) return {result['vhdl']} is + begin + if not p_created(object) then + return {result['default']}; + end if; + if p_eval( + to_call_str("self." & method, {ARG_ACTUALS}), + {result['kind']}, -1, p_call_operation(method), get_session(object) + ) then + return {result['value']}; + end if; + return {result['default']}; + end; +""" + ) + for result in PROCEDURE_RESULTS: + parts.append( + f"""\ + procedure call_{result['name']}( + {OBJECT}; method : string; result : out {result['vhdl']}; + {ARG_PARAMETERS} + ) is + begin + if not p_created(object) then + return; + end if; + if p_eval( + to_call_str("self." & method, {ARG_ACTUALS}), + {result['kind']}, result'length, p_call_operation(method), get_session(object) + ) then + result := {result['value']}; + end if; + end; +""" + ) + for result in RESULTS: + parts.append( + f"""\ + impure function eval_{result['name']}({OBJECT}; expr : string) return {result['vhdl']} is + begin + if not p_created(object) then + return {result['default']}; + end if; + return eval_{result['name']}(expr, {session}); + end; +""" + ) + for result in PROCEDURE_RESULTS: + parts.append( + f"""\ + procedure eval_{result['name']}({OBJECT}; expr : string; result : out {result['vhdl']}) is + begin + if not p_created(object) then + return; + end if; + eval_{result['name']}(expr, result, {session}); + end; +""" + ) + return "\n".join(parts).rstrip("\n") + + +def generate_object_package(): + """ + Generate python_object_pkg from its template and the generated overloads. + """ + template = (TEMPLATE_PATH / "python_object_pkg.vhd.in").read_text(encoding="utf-8") + return Template(template).substitute( + object_declarations=object_declarations(), + object_subprograms=object_subprograms(), + ) + + def generate_package(): """ Generate python_pkg from the template and the generated declarations. @@ -380,6 +548,7 @@ def generate_package(): def main(): (SRC_PATH / "python_pkg.vhd").write_text(generate_package(), encoding="utf-8", newline="\n") + (SRC_PATH / "python_object_pkg.vhd").write_text(generate_object_package(), encoding="utf-8", newline="\n") if __name__ == "__main__": diff --git a/src/vunit_python_bridge/hdl/tools/python_object_pkg.vhd.in b/src/vunit_python_bridge/hdl/tools/python_object_pkg.vhd.in new file mode 100644 index 0000000..d95e8e8 --- /dev/null +++ b/src/vunit_python_bridge/hdl/tools/python_object_pkg.vhd.in @@ -0,0 +1,229 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this file, +-- You can obtain one at http://mozilla.org/MPL/2.0/. +-- +-- Copyright (c) 2014-2026, Lars Asplund lars.anders.asplund@gmail.com +-- +-- Python objects owned by VHDL. An object is an instance of a Python class +-- living in a session of its own, where it is self, created on first use. It +-- is typically the backend of a verification component, see "Python objects" +-- in the user guide. +-- +-- The call and eval overloads of an object are those of python_pkg with the +-- object first, generated from the same result tables. +-- +-- This file is generated from tools/python_object_pkg.vhd.in by +-- src/vunit_python_bridge/hdl/tools/generate_python_pkg.py. Do not edit. + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; + +library vunit_lib; +use vunit_lib.dict_pkg.all; +use vunit_lib.integer_array_pkg.all; +use vunit_lib.id_pkg.all; +use vunit_lib.logger_pkg.all; +use vunit_lib.integer_vector_ptr_pkg.all; +use vunit_lib.string_ptr_pkg.all; + +use work.python_ffi_pkg.all; +use work.python_pkg.all; + +package python_object_pkg is + type python_object_t is record + p_data : integer_vector_ptr_t; + end record; + constant null_python_object : python_object_t := (p_data => null_ptr); + + -- An instance of class_name, a dotted name like "package.module.Class" or the + -- name of a class defined in the default session, constructed with args on first use. + -- The identity names the object and its session. Without one, objects are + -- enumerated like VUnit's verification components: python_bridge:python:object:. + -- An identity already used by an object or a session, and a simulator + -- without sessions (Riviera-PRO/Active-HDL), are reported as a failure on the + -- logger of the identity, and null_python_object is returned. + impure function new_python_object(class_name : string; args : arg_t := null_arg; id : id_t := null_id) + return python_object_t; + + -- Like new_python_object, but the object is constructed by this call rather + -- than on first use, so a wrong class or constructor argument is reported here. + -- Only in a process: Python cannot run during elaboration, so a constant or a + -- generic is made with new_python_object. + impure function create_python_object(class_name : string; args : arg_t := null_arg; id : id_t := null_id) + return python_object_t; + + -- object when it is given, else a new object, like get_logger and get_id: + -- the default backend of a verification component that can also be given one + impure function get_python_object( + object : python_object_t; class_name : string; args : arg_t := null_arg; id : id_t := null_id + ) return python_object_t; + + impure function get_id(object : python_object_t) return id_t; + -- The logger of the object, which reports the errors of its operations + impure function get_logger(object : python_object_t) return logger_t; + -- The session of the object, in which it is bound to self + impure function get_session(object : python_object_t) return python_session_t; + + -- Execute code in the session of the object, where it is self, like exec + procedure exec(object : python_object_t; code : string); + +$object_declarations +end package; + +package body python_object_pkg is + constant session_idx : natural := 0; + constant code_idx : natural := 1; + constant created_idx : natural := 2; + constant operation_idx : natural := 3; + -- The parent of the identities of the objects created without one + constant anonymous_id : id_t := get_id("object", parent => p_python_id); + + -- The defaults of the call results of a failed call + constant p_no_integers : integer_vector(0 downto 1) := (others => 0); + constant p_no_reals : real_vector(0 downto 1) := (others => 0.0); + + impure function p_to_integer_vector_ptr(values : integer_vector) return integer_vector_ptr_t is + constant normalized : integer_vector(0 to values'length - 1) := values; + constant result : integer_vector_ptr_t := new_integer_vector_ptr(values'length); + begin + for idx in normalized'range loop + set(result, idx, normalized(idx)); + end loop; + return result; + end; + + -- new_python_object and create_python_object, operation naming the one called + impure function p_new_python_object(class_name : string; args : arg_t; id : id_t; operation : string) + return python_object_t is + variable object_id : id_t := id; + variable data : integer_vector_ptr_t; + begin + if object_id = null_id then + -- Like enumerate of vc_pkg, which is only there with the verification components + object_id := get_id(to_string(num_children(anonymous_id) + 1), parent => anonymous_id); + end if; + if not p_sessions_supported then + failure(get_logger(object_id), operation & " failed:" & LF & "Python objects require NVC, GHDL or Questa."); + return null_python_object; + end if; + -- The session of the object is that of its identity, which must be its own + if p_has_session(object_id) then + failure( + get_logger(object_id), + operation & " failed:" & LF & "The identity " & full_name(object_id) & + " is already used by another Python object or session, give each object an id of its own." + ); + return null_python_object; + end if; + data := new_integer_vector_ptr(4); + set(data, session_idx, to_integer(new_session(object_id).p_data)); + set(data, code_idx, to_integer(new_string_ptr( + to_call_str("__vunit__.instantiate_as_self", arg(class_name), args) + ))); + set(data, operation_idx, to_integer(new_string_ptr(operation))); + return (p_data => data); + end; + + impure function new_python_object(class_name : string; args : arg_t := null_arg; id : id_t := null_id) + return python_object_t is + begin + return p_new_python_object(class_name, args, id, "new_python_object(""" & class_name & """)"); + end; + + impure function get_python_object( + object : python_object_t; class_name : string; args : arg_t := null_arg; id : id_t := null_id + ) return python_object_t is + begin + if object /= null_python_object then + return object; + end if; + return new_python_object(class_name, args, id); + end; + + -- Report the use of null_python_object, e.g. the result of a failed new_python_object + impure function p_is_null(object : python_object_t) return boolean is + begin + if object = null_python_object then + failure(python_logger, "null_python_object was used, it is not an object"); + return true; + end if; + return false; + end; + + -- The session of null_python_object, where nothing is bound to self + constant null_session : python_session_t := new_session(get_id("null_object", parent => p_python_id)); + + impure function get_session(object : python_object_t) return python_session_t is + begin + if p_is_null(object) then + return null_session; + end if; + return (p_data => to_integer_vector_ptr(get(object.p_data, session_idx))); + end; + + impure function get_id(object : python_object_t) return id_t is + begin + return get_id(get_session(object)); + end; + + -- Construct the object unless it is constructed. A failure is reported once, as + -- the failure of new_python_object or create_python_object, and the object + -- then raises it on every use. + procedure p_create(object : python_object_t) is + variable ok : boolean; + begin + if p_is_null(object) then + return; + end if; + if get(object.p_data, created_idx) = 0 then + set(object.p_data, created_idx, 1); + ok := p_exec( + to_string(to_string_ptr(get(object.p_data, code_idx))), 0, + to_string(to_string_ptr(get(object.p_data, operation_idx))), + get_session(object) + ); + end if; + end; + + impure function create_python_object(class_name : string; args : arg_t := null_arg; id : id_t := null_id) + return python_object_t is + constant object : python_object_t := + p_new_python_object(class_name, args, id, "create_python_object(""" & class_name & """)"); + begin + if object /= null_python_object then + p_create(object); + end if; + return object; + end; + + -- Create the object if needed. False for null_python_object, which is reported. + impure function p_created(object : python_object_t) return boolean is + begin + if object = null_python_object then + return not p_is_null(object); + end if; + p_create(object); + return true; + end; + + procedure exec(object : python_object_t; code : string) is + begin + if p_created(object) then + exec(code, get_session(object)); + end if; + end; + + impure function get_logger(object : python_object_t) return logger_t is + begin + return get_logger(get_id(object)); + end; + + -- The name of a method call in error messages + function p_call_operation(method : string) return string is + begin + return "call(""" & method & """)"; + end; + +$object_subprograms +end package body; diff --git a/src/vunit_python_bridge/hdl/tools/python_pkg.vhd.in b/src/vunit_python_bridge/hdl/tools/python_pkg.vhd.in index a97979e..18cbd2c 100644 --- a/src/vunit_python_bridge/hdl/tools/python_pkg.vhd.in +++ b/src/vunit_python_bridge/hdl/tools/python_pkg.vhd.in @@ -182,6 +182,10 @@ $call_declarations -- or VUNIT_PYTHON_BRIDGE_FILE_BASE. Without one, a failure is reported on -- python_logger and "" is returned. impure function base_path return string; + + -- Private, the conversions of the results of std_ulogic types, shared with python_object_pkg + function p_to_std_ulogic(value : character) return std_ulogic; + function p_to_std_ulogic_vector(value : string) return std_ulogic_vector; end package; package body python_pkg is diff --git a/src/vunit_python_bridge/runtime.py b/src/vunit_python_bridge/runtime.py index cf2e044..516c797 100644 --- a/src/vunit_python_bridge/runtime.py +++ b/src/vunit_python_bridge/runtime.py @@ -15,6 +15,7 @@ """ import builtins +import importlib import linecache import math import numbers @@ -109,6 +110,69 @@ def _type_error(kind, value, expected): ) +def _resolve_class(class_name): + """ + The class a name like "package.module.Class" or "Class" names. The first + part is looked up in the default session (__main__) first, so that a class + defined there, or reached through a name imported there like "np.ndarray", + is found, and imported as a module otherwise. + """ + parts = class_name.split(".") + if parts[0] in vars(__main__): + target, attributes = vars(__main__)[parts[0]], parts[1:] + else: + for index in range(len(parts) - 1, 0, -1): + module_name = ".".join(parts[:index]) + try: + target = importlib.import_module(module_name) + except ModuleNotFoundError as exc: + # Only a missing module of the name itself, not one the module imports + if exc.name is None or not (module_name + ".").startswith(exc.name + "."): + raise + continue + attributes = parts[index:] + break + else: + raise ImportError(f"No class named {class_name!r}: neither defined in the default session nor importable") + for attribute in attributes: + if not hasattr(target, attribute): + owner = getattr(target, "__name__", type(target).__name__) + raise ImportError(f"No class named {class_name!r}: {owner} has no attribute {attribute!r}") + target = getattr(target, attribute) + if not isinstance(target, type): + raise TypeError(f"{class_name!r} is not a class but a {type(target).__name__}") + return target + + +def _plain(exc): + """ + Mark an exception to be reported without its traceback, which only shows the + bridge, e.g. that a class was not found. + """ + exc.vunit_plain = True + return exc + + +class _UncreatedObject: # pylint: disable=too-few-public-methods + """ + Bound to self when the object of a python_object_t could not be created, so + that its later uses fail with the reason rather than with NameError. + """ + + def __init__(self, class_name, exc): + message = f"The {class_name} object could not be created: {type(exc).__name__}: {exc}" + object.__setattr__(self, "_message", message) + + def __getattr__(self, name): + raise _plain(RuntimeError(self._message)) + + def __setattr__(self, name, value): + raise _plain(RuntimeError(self._message)) + + def __delattr__(self, name): + raise _plain(RuntimeError(self._message)) + + class BridgeHandle: """ The ``__vunit__`` object that VHDL-generated Python expressions use. @@ -136,6 +200,24 @@ def error(message): """ raise RuntimeError(message) + def instantiate_as_self(self, class_name, *args, **kwargs): + """ + Construct the object of a VHDL python_object_t, an instance of the class + named class_name, and bind it to self in the current session. If that + fails, self raises the reason on every use instead of being undefined. + """ + namespace = self._runtime.namespace + try: + class_ = _resolve_class(class_name) + except (ImportError, TypeError) as exc: + namespace["self"] = _UncreatedObject(class_name, exc) + raise _plain(exc) + try: + namespace["self"] = class_(*args, **kwargs) + except Exception as exc: + namespace["self"] = _UncreatedObject(class_name, exc) + raise + def __repr__(self): return "" @@ -205,8 +287,12 @@ def _flush(): def format_exception(self, exc): """ - Format an exception with its traceback, hiding frames of this module. + Format an exception with its traceback, hiding frames of this module, or + without it when it is marked plain. """ + if getattr(exc, "vunit_plain", False): + self._flush() + return "".join(traceback.format_exception_only(type(exc), exc)).rstrip("\n") this_file = os.path.normcase(os.path.abspath(__file__)) traceback_ = exc.__traceback__ while traceback_ is not None and ( @@ -251,7 +337,7 @@ def select_session(self, name): self._array_meta = {} @property - def _namespace(self): + def namespace(self): """ Namespace of the current session. """ @@ -290,7 +376,7 @@ def _execute_source(self, source): # Make tracebacks show the source lines of inline code linecache.cache[file_name] = (len(source), None, source.splitlines(True), file_name) code = compile(source, file_name, "exec", dont_inherit=True) - exec(code, self._namespace, self._namespace) # pylint: disable=exec-used + exec(code, self.namespace, self.namespace) # pylint: disable=exec-used @staticmethod def _resolve_file(file_name): @@ -311,7 +397,7 @@ def _execute_file(self, file_name): source = fptr.read() code = compile(source, str(path), "exec", dont_inherit=True) - namespace = self._namespace + namespace = self.namespace previous_file = namespace.get("__file__", _NO_VALUE) directory = str(path.parent) namespace["__file__"] = str(path) @@ -350,7 +436,7 @@ def evaluate(self, text, kind, width): linecache.cache[file_name] = (len(text), None, text.splitlines(True), file_name) code = compile(text, file_name, "eval", dont_inherit=True) # Running the user's own Python code is the purpose of this module - self._result = eval(code, self._namespace, self._namespace) # pylint: disable=eval-used + self._result = eval(code, self.namespace, self.namespace) # pylint: disable=eval-used except BaseException: self._array_meta = {} self._flush() diff --git a/src/vunit_python_bridge/vunit_pkg.toml b/src/vunit_python_bridge/vunit_pkg.toml index 1183a69..c96a3f3 100644 --- a/src/vunit_python_bridge/vunit_pkg.toml +++ b/src/vunit_python_bridge/vunit_pkg.toml @@ -14,4 +14,4 @@ setup = "vunit_python_bridge:setup" # The foreign language interface implementing python_ffi_pkg is selected by the # setup function since the choice depends on the simulator. [[package.sources]] -include = ["hdl/src/python_context.vhd", "hdl/src/python_pkg.vhd"] +include = ["hdl/src/python_context.vhd", "hdl/src/python_pkg.vhd", "hdl/src/python_object_pkg.vhd"] diff --git a/tests/counter_vc.vhd b/tests/counter_vc.vhd new file mode 100644 index 0000000..0fdda31 --- /dev/null +++ b/tests/counter_vc.vhd @@ -0,0 +1,32 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this file, +-- You can obtain one at http://mozilla.org/MPL/2.0/. +-- +-- Copyright (c) 2014-2026, Lars Asplund lars.anders.asplund@gmail.com +-- +-- A verification component whose behaviour is the Python object of its handle. + +library vunit_lib; +context vunit_lib.vunit_context; + +library python_bridge; +context python_bridge.python_context; + +use work.counter_vc_pkg.all; + +entity counter_vc is + generic(vc : counter_vc_t); + port( + tick : in natural; + count : out integer + ); +end entity; + +architecture python of counter_vc is +begin + process + begin + wait on tick; + count <= call(get_model(vc), "add", arg(1)); + end process; +end architecture; diff --git a/tests/counter_vc_pkg.vhd b/tests/counter_vc_pkg.vhd new file mode 100644 index 0000000..a6e1b30 --- /dev/null +++ b/tests/counter_vc_pkg.vhd @@ -0,0 +1,41 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this file, +-- You can obtain one at http://mozilla.org/MPL/2.0/. +-- +-- Copyright (c) 2014-2026, Lars Asplund lars.anders.asplund@gmail.com +-- +-- The handle of counter_vc, made the way VUnit's verification components are: +-- the constructor creates everything, including the Python object. + +library vunit_lib; +context vunit_lib.vunit_context; + +library python_bridge; +context python_bridge.python_context; + +package counter_vc_pkg is + type counter_vc_t is record + p_model : python_object_t; + end record; + + -- A Counter of its own, with the identity of the VC, unless given a model + impure function new_counter_vc( + step : natural := 1; model : python_object_t := null_python_object; id : id_t := null_id + ) return counter_vc_t; + + impure function get_model(vc : counter_vc_t) return python_object_t; +end package; + +package body counter_vc_pkg is + impure function new_counter_vc( + step : natural := 1; model : python_object_t := null_python_object; id : id_t := null_id + ) return counter_vc_t is + begin + return (p_model => get_python_object(model, "models.counter_model.Counter", kwarg("step", step), id)); + end; + + impure function get_model(vc : counter_vc_t) return python_object_t is + begin + return vc.p_model; + end; +end package body; diff --git a/tests/models/counter_model.py b/tests/models/counter_model.py new file mode 100644 index 0000000..56d0123 --- /dev/null +++ b/tests/models/counter_model.py @@ -0,0 +1,32 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. +# +# Copyright (c) 2014-2026, Lars Asplund lars.anders.asplund@gmail.com + + +""" +A stateful model for the Python object tests: each VHDL instance must get its own. +""" + + +class Counter: + def __init__(self, start=0, step=1): + self.count = start + self.step = step + + def add(self, times): + self.count += times * self.step + return self.count + + def value(self): + return self.count + + def history(self): + return list(range(self.count + 1)) + + def ratio(self): + return self.count / 4 + + def name(self): + return "counter" diff --git a/tests/path_counter_vc.vhd b/tests/path_counter_vc.vhd new file mode 100644 index 0000000..1bb91a3 --- /dev/null +++ b/tests/path_counter_vc.vhd @@ -0,0 +1,48 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this file, +-- You can obtain one at http://mozilla.org/MPL/2.0/. +-- +-- Copyright (c) 2014-2026, Lars Asplund lars.anders.asplund@gmail.com +-- +-- A verification component without a handle whose behaviour is a Python +-- object: its identity is its instance path or the id it is given. + +library vunit_lib; +context vunit_lib.vunit_context; + +library python_bridge; +context python_bridge.python_context; + +entity path_counter_vc is + generic( + -- The identity of the VC, the instance path when null_id + id : id_t := null_id; + -- The object to use instead of a Counter of its own + model : python_object_t := null_python_object; + step : natural := 1 + ); + port( + tick : in natural; + count : out integer + ); +end entity; + +architecture python of path_counter_vc is +begin + process + variable vc_id : id_t := id; + variable backend : python_object_t; + begin + -- 'path_name is taken in the process body: during elaboration GHDL leaves + -- the instance out of it, which would give every instance the same identity + if vc_id = null_id then + vc_id := get_id(path_counter_vc'path_name); + end if; + backend := get_python_object(model, "models.counter_model.Counter", kwarg("step", step), vc_id); + + loop + wait on tick; + count <= call(backend, "add", arg(1)); + end loop; + end process; +end architecture; diff --git a/tests/run.py b/tests/run.py index a6ffc9d..5a3e1dc 100644 --- a/tests/run.py +++ b/tests/run.py @@ -103,6 +103,8 @@ def main(): os.environ.pop(FILE_BASE_VARIABLE, None) vu = VUnit.from_args(args) vu.add_vhdl_builtins() + # For create_std_cfg in the tests of Python objects as backends of verification components + vu.add_verification_components() vu.add_package("vunit-python-bridge", allow_setup=True) simulator_name = vu.get_simulator_name() @@ -113,6 +115,8 @@ def main(): if simulator_name in BRIDGE_SIMULATORS: # The operations implemented by the Python bridge lib.add_source_file(ROOT / "tb_python_pkg_bridge.vhd") + objects = ("tb_python_objects.vhd", "counter_vc_pkg.vhd", "counter_vc.vhd", "path_counter_vc.vhd") + lib.add_source_files([ROOT / name for name in objects]) vu.set_compile_option("rivierapro.vcom_flags", ["-dbg"]) vu.set_sim_option("rivierapro.vsim_flags", ["-interceptcoutput"]) diff --git a/tests/tb_python_objects.vhd b/tests/tb_python_objects.vhd new file mode 100644 index 0000000..58d55f1 --- /dev/null +++ b/tests/tb_python_objects.vhd @@ -0,0 +1,255 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this file, +-- You can obtain one at http://mozilla.org/MPL/2.0/. +-- +-- Copyright (c) 2014-2026, Lars Asplund lars.anders.asplund@gmail.com +-- + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; + +library vunit_lib; +context vunit_lib.vunit_context; +context vunit_lib.vc_context; + +library python_bridge; +context python_bridge.python_context; + +use work.counter_vc_pkg.all; + +entity tb_python_objects is + generic(runner_cfg : string); +end entity; + +architecture tb of tb_python_objects is + -- Created by the bench at elaboration, passed to a VC and inspected by the bench + constant shared_model : python_object_t := new_python_object("models.counter_model.Counter", kwarg("start", 100)); + + signal tick : natural := 0; + signal count_a, count_b, count_c, count_d : integer; +begin + main : process + constant duplicate_id : id_t := get_id("duplicate"); + constant session_id : id_t := get_id("session_first"); + constant broken_id : id_t := get_id("broken"); + constant deferred_broken_id : id_t := get_id("deferred_broken"); + constant call_errors_id : id_t := get_id("call_errors"); + variable object, other : python_object_t; + variable session : python_session_t; + variable std_cfg, other_std_cfg : std_cfg_t; + variable byte : unsigned(7 downto 0); + variable bits : std_ulogic_vector(7 downto 0); + begin + test_runner_setup(runner, runner_cfg); + + while test_suite loop + if run("Test that every instance has an object of its own and a given one is shared") then + for idx in 1 to 3 loop + tick <= idx; + wait for 1 ns; + end loop; + check_equal(count_a, 3, "vc_a, a Counter of its own"); + check_equal(count_b, 6, "vc_b, a Counter of its own with step 2"); + check_equal(count_c, 103, "vc_c, the shared Counter"); + check_equal(count_d, 9, "vc_d, a Counter of its own with step 3, by instance path"); + check_equal(integer'(call(shared_model, "value")), 103); + check_equal(eval_string(shared_model, "type(self).__name__"), "Counter"); + + elsif run("Test the result types of the calls of an object") then + object := new_python_object("models.counter_model.Counter", kwarg("start", 5)); + check_equal(integer'(eval(object, "self.count")), 5); + check_equal(real'(call(object, "ratio")), 1.25); + call(object, "add", arg(1)); + call_unsigned(object, "value", byte); + check(byte = 6, "unsigned result"); + check(integer_vector'(call(object, "history")) = (0, 1, 2, 3, 4, 5, 6)); + check_equal(length(call_integer_array(object, "history")), 7); + check_true(eval_boolean(object, "self.count == 6")); + -- The eval procedures of an object + eval_unsigned(object, "self.count + 1", byte); + check(byte = 7, "eval_unsigned"); + eval_std_ulogic_vector(object, "'0000' + '1000'", bits); + check(bits = "00001000", "eval_std_ulogic_vector"); + + elsif run("Test exec and get_logger of an object") then + object := create_python_object("models.counter_model.Counter", id => get_id("exec_test")); + check(get_id(get_session(object)) = get_id("exec_test"), "session of the object"); + exec(object, "self.count = 41"); + check_equal(integer'(call(object, "add", arg(1))), 42); + check(get_logger(object) = get_logger(get_id("exec_test")), "logger of the object"); + + elsif run("Test an object of a class defined in the default session") then + exec("class Doubler:" + " def apply(self, x):" + " return 2 * x"); + object := new_python_object("Doubler"); + check_equal(integer'(call(object, "apply", arg(21))), 42); + -- Without an id, objects are enumerated like VUnit's verification components + check_equal(find(full_name(get_id(object)), "python_bridge:python:object:"), 1); + + elsif run("Test that an identity used by another object or a session fails") then + mock(get_logger(duplicate_id), failure); + object := new_python_object("models.counter_model.Counter", id => duplicate_id); + check_no_log; + object := new_python_object("models.counter_model.Counter", id => duplicate_id); + check_only_log( + get_logger(duplicate_id), + "new_python_object(""models.counter_model.Counter"") failed:" & LF & + "The identity duplicate is already used by another Python object or session, " & + "give each object an id of its own.", + failure + ); + unmock(get_logger(duplicate_id)); + check(object = null_python_object, "no object for a used identity"); + + session := new_session(session_id); + mock(get_logger(session_id), failure); + object := new_python_object("models.counter_model.Counter", id => session_id); + check_only_log( + get_logger(session_id), + "new_python_object(""models.counter_model.Counter"") failed:" & LF & + "The identity session_first is already used by another Python object or session, " & + "give each object an id of its own.", + failure + ); + unmock(get_logger(session_id)); + check(object = null_python_object, "no object for the identity of a session"); + + elsif run("Test an object that cannot be created") then + -- create_python_object reports it right away + mock(get_logger(broken_id), failure); + object := create_python_object("models.no_such_module.Model", id => broken_id); + check_only_log( + get_logger(broken_id), + "create_python_object(""models.no_such_module.Model"") failed:" & LF & + "ImportError: No class named 'models.no_such_module.Model': models has no attribute 'no_such_module'", + failure + ); + -- Reported once, then every use fails with the reason + check_equal(integer'(call(object, "value")), integer'low); + check_only_log( + get_logger(broken_id), + "call(""value"") failed:" & LF & + "RuntimeError: The models.no_such_module.Model object could not be created: " & + "ImportError: No class named 'models.no_such_module.Model': models has no attribute 'no_such_module'", + failure + ); + unmock(get_logger(broken_id)); + + -- new_python_object reports it at the first use, as new_python_object + mock(get_logger(deferred_broken_id), failure); + object := new_python_object("models.no_such_module.Model", id => deferred_broken_id); + check_no_log; + check_equal(integer'(call(object, "value")), integer'low); + check_log( + get_logger(deferred_broken_id), + "new_python_object(""models.no_such_module.Model"") failed:" & LF & + "ImportError: No class named 'models.no_such_module.Model': models has no attribute 'no_such_module'", + failure + ); + check_only_log( + get_logger(deferred_broken_id), + "call(""value"") failed:" & LF & + "RuntimeError: The models.no_such_module.Model object could not be created: " & + "ImportError: No class named 'models.no_such_module.Model': models has no attribute 'no_such_module'", + failure + ); + unmock(get_logger(deferred_broken_id)); + + elsif run("Test that the call of a method is named in its errors") then + object := new_python_object("models.counter_model.Counter", id => call_errors_id); + mock(get_logger(call_errors_id), failure); + -- The function and the procedure forms name the call alike + check_equal(integer'(call(object, "name")), integer'low); + check_only_log( + get_logger(call_errors_id), + "call(""name"") failed:" & LF & + "TypeError: Cannot convert Python str ('counter') to VHDL integer; expected int", + failure + ); + call_unsigned(object, "name", byte); + check_only_log( + get_logger(call_errors_id), + "call(""name"") failed:" & LF & + "TypeError: Cannot convert Python str ('counter') to VHDL unsigned; expected int", + failure + ); + unmock(get_logger(call_errors_id)); + + elsif run("Test that objects have sessions and numbers of their own") then + object := new_python_object("models.counter_model.Counter", kwarg("start", 1)); + other := new_python_object("models.counter_model.Counter", kwarg("start", 2)); + check_equal(integer'(eval(object, "self.count")), 1); + check_equal(integer'(eval(other, "self.count")), 2); + exec(object, "shared = 'object'"); + check_false(eval_boolean(other, "'shared' in globals()"), "the sessions are separate"); + -- Without an id, objects are enumerated like VUnit's verification components + check_equal(full_name(get_id(object)), "python_bridge:python:object:" & name(get_id(object))); + check_equal(integer'value(name(get_id(other))), integer'value(name(get_id(object))) + 1); + + elsif run("Test that exec and get_logger work before the object is created") then + object := new_python_object("models.counter_model.Counter", id => get_id("lazy")); + check(get_logger(object) = get_logger(get_id("lazy")), "logger before creation"); + exec(object, "self.count = 7"); + check_equal(integer'(call(object, "value")), 7); + + elsif run("Test that null_python_object is reported") then + mock(python_logger, failure); + call(null_python_object, "value"); + check_only_log(python_logger, "null_python_object was used, it is not an object", failure); + check_equal(integer'(call(null_python_object, "value")), integer'low); + check_only_log(python_logger, "null_python_object was used, it is not an object", failure); + unmock(python_logger); + + elsif run("Test objects with the identities of create_std_cfg") then + -- As in "Backends of verification components" of the user guide + std_cfg := create_std_cfg(provider => "test_vcs", vc_name => "counter"); + other_std_cfg := create_std_cfg(provider => "test_vcs", vc_name => "counter"); + object := get_python_object(null_python_object, "models.counter_model.Counter", id => get_id(std_cfg)); + other := get_python_object(null_python_object, "models.counter_model.Counter", id => get_id(other_std_cfg)); + check_equal(full_name(get_id(object)), "test_vcs:counter:1"); + check_equal(full_name(get_id(other)), "test_vcs:counter:2"); + check_equal(integer'(call(object, "add", arg(1))), 1); + end if; + end loop; + + test_runner_cleanup(runner); + end process; + + vc_a : entity work.counter_vc + generic map ( + vc => new_counter_vc(id => get_id("vc_a")) + ) + port map ( + tick => tick, + count => count_a + ); + + -- No id: an enumerated anonymous identity + vc_b : entity work.counter_vc + generic map ( + vc => new_counter_vc(step => 2) + ) + port map ( + tick => tick, + count => count_b + ); + + vc_c : entity work.counter_vc + generic map ( + vc => new_counter_vc(model => shared_model) + ) + port map ( + tick => tick, + count => count_c + ); + + -- Without a handle: the instance path is the identity + vc_d : entity work.path_counter_vc + generic map ( + step => 3 + ) + port map ( + tick => tick, + count => count_d + ); +end architecture; diff --git a/tests/test_python_bridge.py b/tests/test_python_bridge.py index 31527d4..d293430 100644 --- a/tests/test_python_bridge.py +++ b/tests/test_python_bridge.py @@ -11,12 +11,14 @@ library with the system C compiler, and reading/writing files, is fine. """ +import importlib.util import os import re import shutil import subprocess import sys import tempfile +import types import unittest from contextlib import contextmanager from glob import glob @@ -157,7 +159,7 @@ def test_sources_are_the_simulator_independent_vhdl(self): includes = [item for source in self.package["sources"] for item in source["include"]] self.assertEqual( sorted(includes), - ["hdl/src/python_context.vhd", "hdl/src/python_pkg.vhd"], + ["hdl/src/python_context.vhd", "hdl/src/python_object_pkg.vhd", "hdl/src/python_pkg.vhd"], ) for include in includes: self.assertTrue((PACKAGE_ROOT / include).is_file(), include) @@ -168,6 +170,92 @@ def test_the_simulator_dependent_vhdl_is_not_listed(self): self.assertNotIn(f"hdl/src/{name}", includes) +class TestObjectCreation(unittest.TestCase): + """ + __vunit__.instantiate_as_self, which creates the object of a python_object_t in its session. + """ + + def setUp(self): + from vunit_python_bridge import runtime # pylint: disable=import-outside-toplevel + + self.namespace = {} + self.handle = runtime.BridgeHandle(mock.Mock(namespace=self.namespace)) + + def test_binds_an_instance_of_a_class_of_a_module_to_self(self): + self.handle.instantiate_as_self("collections.OrderedDict", a=1) + self.assertEqual(self.namespace["self"], {"a": 1}) + + def test_a_dotted_name_can_name_a_class_in_a_class(self): + module = types.ModuleType("models_under_test") + module.Outer = type("Outer", (), {"Inner": type("Inner", (), {})}) + with mock.patch.dict(sys.modules, {"models_under_test": module}): + self.handle.instantiate_as_self("models_under_test.Outer.Inner") + self.assertIsInstance(self.namespace["self"], module.Outer.Inner) + + def test_a_bare_name_is_a_class_of_the_default_session(self): + class Model: # pylint: disable=too-few-public-methods + pass + + with mock.patch.dict("__main__.__dict__", {"Model": Model}): + self.handle.instantiate_as_self("Model") + self.assertIsInstance(self.namespace["self"], Model) + + def test_a_name_imported_in_the_default_session_is_followed(self): + module = types.ModuleType("real_module_name") + module.Model = type("Model", (), {}) + # Like "import real_module_name as alias" in the default session + with mock.patch.dict("__main__.__dict__", {"alias": module}): + self.handle.instantiate_as_self("alias.Model") + self.assertIsInstance(self.namespace["self"], module.Model) + + def test_a_missing_bare_name_is_reported_as_not_found(self): + with self.assertRaisesRegex(ImportError, "No class named 'NoSuchModel': neither defined in the default"): + self.handle.instantiate_as_self("NoSuchModel") + + def test_a_missing_class_of_a_module_names_the_module(self): + with self.assertRaisesRegex(ImportError, "'json.JSONDecoderr': json has no attribute 'JSONDecoderr'"): + self.handle.instantiate_as_self("json.JSONDecoderr") + + def test_a_name_that_is_not_a_class_is_reported(self): + with self.assertRaisesRegex(TypeError, "'os.getcwd' is not a class but a builtin_function_or_method"): + self.handle.instantiate_as_self("os.getcwd") + + def test_a_failing_import_inside_the_module_is_not_hidden(self): + with create_tempdir() as tempdir: + (tempdir / "broken_model.py").write_text("import no_such_dependency\n", encoding="utf-8") + with mock.patch.object(sys, "path", [str(tempdir)] + sys.path): + with self.assertRaisesRegex(ModuleNotFoundError, "No module named 'no_such_dependency'"): + self.handle.instantiate_as_self("broken_model.Model") + + def test_an_object_that_cannot_be_created_tells_why_when_used(self): + with self.assertRaises(ImportError): + self.handle.instantiate_as_self("no_such_module.Model", 1) + with self.assertRaisesRegex( + RuntimeError, + "The no_such_module.Model object could not be created: ImportError: No class named 'no_such_module.Model'", + ): + self.namespace["self"].step() + with self.assertRaisesRegex(RuntimeError, "could not be created"): + self.namespace["self"].trace = True + + +class TestGeneratedPackages(unittest.TestCase): + """ + The generated VHDL packages are those the generator gives for the templates. + """ + + def test_generated_packages_are_up_to_date(self): + generator_path = PACKAGE_ROOT / "hdl" / "tools" / "generate_python_pkg.py" + spec = importlib.util.spec_from_file_location("generate_python_pkg", generator_path) + generator = importlib.util.module_from_spec(spec) + spec.loader.exec_module(generator) + for name, text in ( + ("python_pkg.vhd", generator.generate_package()), + ("python_object_pkg.vhd", generator.generate_object_package()), + ): + self.assertEqual((generator.SRC_PATH / name).read_text(encoding="utf-8"), text, f"regenerate {name}") + + class TestForeignLanguageInterfaces(unittest.TestCase): """ The table deciding how a simulator is served. VUnit only says which simulator was