Skip to content

Add Python objects: instances of Python classes owned by VHDL - #15

Open
ru551n wants to merge 36 commits into
VUnit:mainfrom
ru551n:python-objects
Open

ru551n wants to merge 36 commits into
VUnit:mainfrom
ru551n:python-objects

Conversation

@ru551n

@ru551n ru551n commented Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

Adds Python objects to the bridge: instances of Python classes owned by VHDL, each living in a session of its own with state of its own. One can be a reference model, a memory model shared between a bus slave and the test, a helper class in a testbench, or the backend of a verification component. That last use is the one that motivated the design. It replaces #14 and covers most of #8.

The problem

When a VC's behaviour lives in Python, every VC author has to solve the same problems:

  • Namespaces. Functions and state in the default session collide when two instances of the same VC, or two models, use the same names. Sessions fix this, but each VC must create one with a unique identity, load its code into it, and pass it to every call.
  • Locating the code. exec_file resolves relative to the testbench, which a VC deep in the hierarchy doesn't know. That leads to sys.path manipulation, passed-around paths, and ordering workarounds around test_runner_setup.
  • String-built calls. Object and module names get concatenated into Python source, so mistakes only show up at run time.
  • Sharing. There's no simple way for a testbench to create a model and give it to a VC, so that both use the same state.

The API

constant model : python_object_t := new_python_object("my_vcs.uart.UartModel", kwarg("baud", 115200), id => get_id("uart"));

count := call(model, "push", arg(byte));     -- method call, any result type of call()
call(model, "reset");                        -- procedure form
check_true(eval_boolean(model, "obj.is_idle()"));
exec(model, "obj.trace = True");
byte := call_unsigned(model, "pop", byte'length);  -- or size_res: call_unsigned(model, "pop", byte)
  • The class is named, not loaded from a file. A dotted name like "package.module.Class" is imported from the module it names, found like any import: the run script directory, the Python environment, or PYTHONPATH. A bare "Class" is a class defined in the default session, and a name like "np.ndarray" follows a name defined there. A name that isn't found, or isn't a class, is reported in one line.

  • One object, one session. The instance is bound to obj in a session named by the object's identity, so objects never share names or state.

  • Constructed on first use, or right away. new_python_object only records the class and the constructor arguments, so an object can be a constant or a generic, independent of test_runner_setup. create_python_object takes the same arguments and constructs the object in the call, so a wrong class or argument is reported there; it works only in a process, since NVC and GHDL can't run the bridge during elaboration. The user guide has a table of when to use each.

  • Identity.

    • An object without an id is numbered per class like VUnit's VCs: python_object:my_vcs.uart.UartModel:1, :2, and so on.
    • An identity already used by another object, or by a session made with new_session, is reported on that identity's logger, and new_python_object returns null_python_object. Using null_python_object is reported on python_logger.
    • get_logger(object) reports the object's errors, named after what failed: call("push") failed: for every form of call, eval(...) and exec(...) for those, and new_python_object("<class>") failed:.
  • Lookup by identity. get_python_object(id) and get_python_object(name) return the object made with that identity, so a testbench can reach a VC's object without being given it. Unlike get_logger, it doesn't make one: an identity without an object returns null_python_object. new_python_object and create_python_object also take the identity by its full name (name => "tb:uart_model").

  • VC backends. A VUnit-style VC has one constructor making its own object, with the id from create_std_cfg, and one taking an object, e.g. shared with the testbench or other VCs. A given object keeps its own identity:

    impure function new_uart_master(baud : positive; 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 => new_python_object("my_vcs.uart.UartModel", kwarg("baud", baud), get_id(std_cfg))
      );
    end;
    
    impure function new_uart_master(model : python_object_t; id : id_t := null_id) return uart_master_t is
    begin
      return (p_std_cfg => create_std_cfg(id, "my_vcs", "uart_master"), p_model => model);
    end;
  • Generated overloads. call and eval on an object come from the same result tables as python_pkg: every result type, the out-parameter procedures, the vector functions sized by a length or size_res, and the same aliases. python_object_pkg.vhd is generated from hdl/tools/python_object_pkg.vhd.in by generate_python_pkg.py, and a pytest checks that both generated packages are up to date.

The identity comes from the handle rather than 'path_name, because GHDL drops the instance labels from 'path_name/'instance_name when they're evaluated during elaboration. A VC whose handle has no id can make its object in a process with its instance path as the identity; the user guide shows both.

Changes

  • hdl/tools/python_object_pkg.vhd.in and the generated hdl/src/python_object_pkg.vhd, added to python_context.
  • runtime.py: __vunit__.instantiate_as_obj. If the class can't be found or constructed, the error is reported once and obj becomes a stand-in that raises the reason on every later use, assignments included. Classes are found with importlib; pydoc is not imported.
  • python_pkg gets the vector functions sized by a length or size_res, and the alias call_integer of call_integer_w_arg.
  • The FFI packages tell the object package which identities have sessions. The VHPI FFI gets a p_exec with an operation name, which only reports, and the result getters the generated object overloads use.
  • Example: examples/embedded_python gets filter_vc, a clocked, handle-based VC whose behaviour is a MovingAverage object, with a test case driving two instances that have their own objects and one given the testbench's object.
  • Docs:

Tests

  • tests/tb_python_objects.vhd covers:
    • handle-based VCs (tests/counter_vc_pkg.vhd, tests/counter_vc.vhd) with their own id, an enumerated id and a shared object,
    • a VC with a handle without id, on its instance path (tests/path_counter_vc.vhd),
    • get_python_object by identity, full name and instance path, and null_python_object for an identity without an object,
    • a class defined in the default session,
    • an identity already used by an object or a session,
    • an object that can't be created, reported by create_python_object right away and by the first use of a new_python_object, then on every use,
    • the naming of call errors, for the function and the procedure forms,
    • separate sessions and the numbering of anonymous objects,
    • every generated result type, the eval procedures and the sized vector functions,
    • exec, get_logger, create_python_object and get_session, also before creation,
    • null_python_object,
    • objects with the identities of create_std_cfg, as in the user guide.
  • pytest covers __vunit__.instantiate_as_obj: dotted names including nested classes, names followed from the default session, default-session classes, a missing name, a name that isn't a class, import errors inside a model not being mistaken for a missing class, and the failed-creation stand-in.
  • Locally:
    • the full suite (123 tests) and the example pass on NVC and GHDL,
    • pytest passes (121),
    • Sphinx builds without warnings,
    • the PYTHONPATH setup from the docs works with the run script started from another directory,
    • the VHPI package analyses cleanly with GHDL together with python_pkg and python_object_pkg, apart from its foreign bodies.

Coordination with #18

The "Errors and limitations" bullet that integer_array_t constructor arguments can't be made during elaboration goes if #18 merges. #29 keeps it true, reporting such an argument as a failure.

🤖 Generated with Claude Code

@ru551n ru551n changed the title Prototype: Python objects owned by VHDL, for VC backends Prototype: Python objects owned by VHDL Oct 8, 2026
@ru551n ru551n changed the title Prototype: Python objects owned by VHDL Python objects owned by VHDL, for verification component backends Oct 8, 2026
@ru551n ru551n changed the title Python objects owned by VHDL, for verification component backends Add Python objects: instances of Python classes owned by VHDL Oct 8, 2026
@ru551n
ru551n marked this pull request as ready for review October 8, 2026 18:09
constant python_logger : logger_t := get_logger(p_python_id);

-- Whether sessions other than the default one, and so Python objects, are supported
constant p_supports_sessions : boolean := true;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To claude: Why is this required?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It wasn't required, so it's removed in 7306ba5. I'd added it only so that the Riviera-PRO/Active-HDL failure would say "Python objects require …". Their FFI already fails on the first use of a non-default session ("Python sessions are only supported with NVC, GHDL and Questa"), and every object uses a session of its own, so that message already covers it.

Comment thread docs/user_guide.rst Outdated
.. code-block:: vhdl

constant model : python_object_t := new_python_object(
"my_models.uart:UartModel", kwarg("baud", 115200), id => get_id("uart")

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the colon required, cant we just have regular python namespace conventions?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also mentions that this calls the constructor

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, the colon is gone in 7306ba5. A class is now a plain dotted name, "package.module.Class". It's resolved by importing the longest prefix that is a module and taking the rest as attributes, so a nested class ("package.module.Outer.Inner") works without a separator. An import error inside the user's module is still reported as such, not mistaken for a missing module; pytest covers both cases.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. The paragraph now says that new_python_object names the class and the arguments of its constructor, and that the constructor is called on first use (or right away by create).

Comment thread docs/user_guide.rst Outdated
Comment on lines +317 to +327
The model of a verification component is best a Python package next to it,
named after it since module names are global to the simulation:

.. code-block:: text

my_vc/
my_vc.vhd
python/
my_vc_model/
__init__.py

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this required or implicit behaviour?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partly implicit. The run script's directory is the first entry of sys.path, and the packages of VUnit's Python environment (including pip install -e) are importable without anything extra. PYTHONPATH is only needed for code somewhere else, such as a VC's own Python folder. The new "Where Python code is found" chapter now spells this out.

Comment thread docs/user_guide.rst Outdated
Comment on lines +328 to +331
The run script prepends the Python folder to ``PYTHONPATH`` before VUnit
starts. The simulator runs Python in the environment of VUnit, so the setting
reaches the interpreter in the simulator and the class is
``"my_vc_model.Model"`` wherever the component is used:

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should document the namespacing conventions (directories added to paths etc) in it's own chapter.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done: a new "Where Python code is found" chapter, before Sessions. It covers what the interpreter finds without setup (the run script directory, VUnit's Python environment), adding folders with PYTHONPATH from the run script, how exec_file/import_module_from_file resolve file names (and that exec_file temporarily adds the file's directory), and that sys.path/sys.modules are global to the simulation. The VC package layout moved there from the objects section, which now links to it.

Comment thread docs/user_guide.rst Outdated
Comment on lines +364 to +369
``get_python_object(object, class_name, args, id)`` returns ``object`` when it
is given, and a new object otherwise, like ``get_logger`` and ``get_id``. A
verification component made the way VUnit's own are creates its object in the
constructor of its handle, with the identity of the component. Every instance
then gets a backend of its own by default and the object of the testbench when
given one:

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rewrite this for better readability.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rewritten: it now leads with where a VC makes its object (the handle constructor, like VUnit's own components) and then what get_python_object does, before the example.

Comment thread docs/user_guide.rst Outdated
Comment on lines +403 to +412
The handle is a constant made during elaboration, and so is the object, which
is not created in Python until its first call. ``create_std_cfg`` gives
instances without an ``id`` the identities ``my_vcs:uart_master:1``,
``my_vcs:uart_master:2`` and so on, which become the identities of their
objects. ``filter_vc_pkg.vhd`` and ``filter_vc.vhd`` of the `embedded_python
example <https://github.com/VUnit/vunit-python-bridge/tree/main/examples/embedded_python>`__
are a complete component of this kind, with a 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 given an object of the testbench, which the testbench queries too.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could also do with some readability improvements.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Split into two shorter paragraphs. The first covers elaboration versus first call and the create_std_cfg numbering; the second points to filter_vc in the example and lists which file holds what.

Comment thread docs/user_guide.rst Outdated
Comment on lines +432 to +434
A testbench giving several components, or a component and itself, the same
object shares its state between them, for example a scoreboard filled by a
monitor and checked by the test:

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Readability.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reworded: "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." The scoreboard example follows. The closing sentence about a class-name generic is clearer too.

Comment thread examples/embedded_python/filter_vc.vhd
Comment thread src/vunit_python_bridge/runtime.py Outdated
"""
raise RuntimeError(message)

def create(self, class_name, *args, **kwargs):

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method should be named more of what it does.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed to __vunit__.instantiate_as_self, since it instantiates the named class and binds the instance to self in the current session.

Comment thread docs/user_guide.rst Outdated
Comment on lines +120 to +143
.. code-block:: text

my_vc/
my_vc.vhd
python/
my_vc_model/
__init__.py

and the run script puts that folder on the path, after which the model is
``my_vc_model`` wherever the component is used:

.. code-block:: python

import os
from pathlib import Path
from vunit import VUnit

my_vc_python = Path(__file__).parent / "my_vc" / "python"
os.environ["PYTHONPATH"] = os.pathsep.join(
filter(None, [str(my_vc_python), os.environ.get("PYTHONPATH")])
)

vu = VUnit.from_argv()
...

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not very clear if the model is found automatically or not, since I think these snippets are in the wrong order.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, the order hid it. Reworked in f10700d. The chapter now has a subsection per topic: "Found without setup", "Adding a folder", "Files given by name" and "Module names". "Adding a folder" opens with "Python code anywhere else is not found until its folder is on PYTHONPATH". It then shows the VC layout, says outright that my_vc/python can't be imported as it is, shows the run script snippet, and only then says that my_vc_model is importable everywhere. pip install -e is mentioned there as the alternative.

Comment thread docs/user_guide.rst Outdated
Comment on lines +405 to +425
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);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use a different example than scoreboard

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude (AI assistant) here, on behalf of @ru551n:

Fixed in a158cf7: the shared object example in docs/python_objects.rst is now a memory model written by a bus slave and read back by the test.

Comment thread docs/python_objects.rst Outdated
Comment on lines +266 to +268
* 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``.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove, this is part of the bigger picture of whether riviera pro is supported.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude (AI assistant) here, on behalf of @ru551n:

Fixed in a158cf7: removed the Riviera-PRO/Active-HDL item.

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude (AI assistant) here, on behalf of @ru551n:

It was only there to fail at new_python_object on Riviera-PRO/Active-HDL. That is not needed: the first use of an object already fails in the VHPI primitives (<operation> requires NVC, GHDL or Questa). Fixed in a158cf7: removed p_sessions_supported and its check.

python_pkg with the object first, the method or expression second.
"""
lines = [
" -- Call a method of the object, like call",

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
" -- Call a method of the object, like call",
" -- Call a method of the object",

Comment thread docs/python_objects.rst Outdated

count := call(model, "push", arg(byte)); -- calls self.push(byte)
call(model, "reset"); -- no return value
check_true(eval_boolean(model, "self.is_idle()"));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should consider other names than self to avoid potential name collisions. What if user code creates another class in the model's name space? self would have another meaning for methods within that class. Maybe we can borrow from other languages? this is common but it collides with the Python package this. Very unlikely to be used though. me is another option. Or we can simply use obj (but not object since that is already defined by Python). I'm leaning towards obj

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude (AI assistant) here, on behalf of @ru551n:

Fixed in a117778: the object is bound to obj in its session, so eval(model, "obj.is_idle()"), exec(model, "obj.trace = True"), and call(model, "push", ...) calls obj.push(...). The runtime hook is now __vunit__.instantiate_as_obj.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, renamed to obj

Comment thread docs/python_objects.rst Outdated
``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``,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about using the class name? The hierarchy of the class is reflected in the parent/child hierarchy of the id concept.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed, it now resolves as python_bridge:python:object:package:module:Class:<n>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need all of the python_bridge:python:object: prefix. Generally speaking we want the shortest naming scheme that gives uniqueness.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, renamed as python_object:my_models.uart.UartModel:1. Should we use the dots or colon in the python dotted name? I think dots since it is exactly what python sees, colons would be confusing.

Comment thread docs/python_objects.rst Outdated

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get_logger and get_id takes a string as input and returns the logger/id. Creating something similar would be to call get_python_object with the class name. However, that doesn't work since we can have several instances of that class. Instead, get_python_object should take the id as input or the id name. If that name doesn't exist, a new object can't be created as that requires arguments to the constructor of that class. In that case it should return null_python_object and the user has to create the object "manually"

@ru551n ru551n Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partially fixed in 96758a3. Unknown objects now also return as null_python_object without failure.

Comment thread docs/python_objects.rst
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");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should elaborate on design patterns for ID assignment. If id is provided, we should use it. The question is what we use when it isn't provided. Are there cases where we should use the model ID? It seems like the only use case for providing a model is when it is shared among several VCs and the VCs should have different names. If the VC isn't shared, we might as well let the function create it. I guess that means we should never take the ID from the incoming model and the example can remain as is.

@ru551n ru551n Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed together with the other comment on the same topic, now split into two functions. Also added doc section on id assignment.

Comment thread tests/counter_vc_pkg.vhd Outdated
end record;

-- A Counter of its own, with the identity of the VC, unless given a model
impure function new_counter_vc(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After seeing a few of these, I think the design pattern should be that we have different new functions rather than trying to solve two use cases with the same function:

  1. impure function new_counter_vc(model : python_object_t; id : id_t := null_id) return counter_vc_t;
  2. impure function new_counter_vc(step : natural := 1; id : id_t := null_id) return counter_vc_t;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comment #15 (comment).

Comment thread tests/path_counter_vc.vhd
context python_bridge.python_context;

entity path_counter_vc is
generic(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would say that this is a bit of an anti-pattern. Some of the updates we've done recently is to change VCs that have more than one generic into having just the handle. Multiple generics will still work of course but I would avoid promoting it.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed, merged into one handle.

Comment thread tests/tb_python_objects.vhd Outdated

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use the alias eval_integer

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you considered call functions for the vector types? Similar to vec := to_integer(17, vec) and vec := to_integer(17, vec'length). With VHDL-2019, vec := call_unsigned(object, "value") is another option.

@ru551n ru551n Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea. Added both 'length and res size type overloads.

I left out the VHDL-2019 type for now.

Comment thread release_notes/news.d/15.feature.md Outdated
@@ -0,0 +1 @@
`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")`. The identity of an object can be given as an `id_t` or by its full name, and `get_python_object(id)` or `get_python_object(name)` returns the object made with it. See "Python objects" in the user guide.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shorten this, we don't need to supply the technical details in a news fragment. Point the user to the documentation.

Comment thread release_notes/news.d/15.feature.1.md Outdated
@@ -0,0 +1 @@
`call_integer` is an alias of `call_integer_w_arg`, the `call` returning `integer`, named like `eval_integer` and `call_real`.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove, nothing a user needs to know.

Comment thread docs/python_objects.rst Outdated
``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``,

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, renamed as python_object:my_models.uart.UartModel:1. Should we use the dots or colon in the python dotted name? I think dots since it is exactly what python sees, colons would be confusing.

ru551n and others added 15 commits October 16, 2026 14:48
python_object_t is an instance of a Python class living in a session of its
own, created on first use and called with call(object, "method", args).
Every VC instance can get an object of its own without namespace or path
handling, and a bench can create one and pass it to a VC through a generic.

Hand-written prototype for review of the API; the full set of result types
would be generated once it is agreed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
python_object_pkg.vhd is generated from python_object_pkg.vhd.in by
generate_python_pkg.py, from the same result tables as python_pkg, so an
object has every result type of call and eval, the procedure results and
the same aliases. A test checks that both generated packages are up to date.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Like get_logger and get_id, it gives the object it is given or a new one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An object whose class cannot be imported or constructed reports that error
once and is then bound to a stand-in raising the reason on every use,
rather than leaving self undefined. The user guide gets a Python objects
section, and the README and the release notes mention them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
new_object is now new_python_object and get is get_python_object, as
VUnit names the constructors and getters of its types. An object without
an id is python_bridge:python:object:<n>, enumerated like VUnit's
verification components. The user guide makes the handle constructor of a
VUnit style verification component the place to create its object, which
tests/counter_vc_pkg.vhd now shows; the handle-less component taking its
instance path is tests/path_counter_vc.vhd.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
exec(object, code) executes code with the object as self, and
get_logger(object) is the logger reporting its errors. On
Riviera-PRO/Active-HDL, which have no sessions, the first use of an object
says that objects require NVC, GHDL or Questa. The user guide notes that an
integer_array_t cannot be a constructor argument of an object made during
elaboration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
filter_vc is a verification component whose handle, made by
new_filter_vc, holds the Python object that is its behaviour. The new
test case drives three instances: two with objects of their own and one
given an object of the testbench, which the testbench queries too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the verification component pattern of VUnit#14: the model of a verification
component as a package next to it, put on PYTHONPATH by the run script or
installed with pip install -e. The class example of the independent models
section gets its file from the model_file generic of the component instead
of runner_cfg, which a component does not have.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Name the failures of objects after what failed: new_python_object("<class>")
for a failed creation and call("<method>") for the procedure form of a
method call, rather than an exec of their session. p_create and p_self are
private to the package body, and the PROTOTYPE marker is gone. The user
guide shows a component calling its object in a process and how errors are
named.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One association per line in the generic and port maps of the tests, the
example and the user guide, the layout speja gives them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A class is named by a plain dotted name, resolved from the longest prefix
  that is a module, so nested classes work without the module:Class form.
- __vunit__.create is __vunit__.instantiate_as_self, after what it does.
- p_supports_sessions is gone: the FFI of Riviera-PRO/Active-HDL already
  reports the first use of a session other than the default one.
- filter_vc of the example takes a clock and a valid input.
- The user guide gets a "Where Python code is found" chapter on the run
  script directory, the Python environment, PYTHONPATH, file names and
  global module names, and clearer paragraphs on backends of verification
  components and shared objects.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Where Python code is found chapter gets a subsection per topic, and
the verification component example states that its folder is not
importable until the run script adds it to PYTHONPATH, before showing how.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- pydoc.locate resolves dotted class names instead of a hand-written loop.
- The identity check is in VHDL only, which also catches a duplicate at
  elaboration; the Python check of the session was a second copy of it.
- Runtime.namespace is the property itself rather than a wrapper of it.
- create is the procedure the generated code calls, without p_create
  behind it, p_self is inlined and the operation of a failed creation is
  stored rather than rebuilt from the class name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ru551n and others added 21 commits October 16, 2026 14:49
…ery use

A class name now starts in the default session, so a class reached
through a name defined there, like "np.ndarray", is found, and is
otherwise imported with importlib rather than pydoc, which is no longer
imported at every simulation start. A name that is not found, or not a
class, is reported in one line rather than with the traceback of the
bridge. The stand-in of an object that could not be created also fails
on assignments.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- new_python_object reports, on the logger of the identity, an identity
  already used by an object or by a session made with new_session, and
  a simulator without sessions (Riviera-PRO/Active-HDL), and then
  returns null_python_object rather than carrying on. Using
  null_python_object is reported on python_logger.
- The VHPI p_exec stub reports and executes nothing, rather than
  running the code in the default session.
- Every form of call on an object reports its errors as
  call("<method>"), like the procedure did: the call overloads evaluate
  with p_eval and the result getters, which the FFI packages now all
  have.
- create is named create_now, which is what it does.
- Tests of the error paths: a used identity, an object that cannot be
  created, the naming of call errors, separate sessions and numbering,
  exec and get_logger before creation, null_python_object, the eval
  procedures and the identities of create_std_cfg.
- The generated lines are at most 120 characters, and the docstring of
  the generator describes both packages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The section on how modules are found is now the "Python modules" part
of "File paths", whose table already covered file names, with one
recipe for PYTHONPATH. The objects chapter describes the new identity
rule, the failure on Riviera-PRO/Active-HDL, the class lookup and the
error names of call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Converting a stream-width model kept in module globals to an object per
source, in a project that used exec_file models before, showed:
- A misspelled class in a module that does import said only "No class
  named", which reads like a missing module. It now names the module
  and the missing attribute.
- The user guide gets a recipe for moving from a model with module
  state to an object, says that the id is optional, and recommends
  create_now to report wrong constructor arguments at the start.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
new_python_object makes an object that Python constructs on first use;
create_python_object makes one that is constructed by the call, so a
wrong class or constructor argument is reported there. They replace
new_python_object with create_now, and the step that constructs an
object on first use is private.

create_python_object only works in a process: NVC cannot find the
foreign functions of the bridge during elaboration ("foreign function
vpy_buffer_clear not found") and GHDL cannot call them then. The user
guide has a table of when to use each, and errors name the one called.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Python objects chapter moves from Running Python code to its own page, after
Sessions, and python_object_pkg gets a page in the VHDL API reference.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Remove p_sessions_supported: on Riviera-PRO/Active-HDL, the first use of an
object already fails in the VHPI primitives, and whether Riviera-PRO is
supported is a question of its own. The shared object example of the docs
is a memory model instead of a scoreboard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
get_python_object(id) and get_python_object(name) return the object made
with that identity, like get_logger, but report an identity without an
object as a failure instead of making one. new_python_object and
create_python_object take the identity by its full name as well. The
get_python_object giving a component a default backend is replaced by an
if in the constructor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
self is also the first parameter of the methods of classes defined in
the session, which made code mixing the two hard to read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Objects without an id are enumerated per class, the dotted class name
giving the identity hierarchy under python_bridge:python:object, e.g.
python_bridge:python:object:models:counter_model:Counter:1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
get_python_object no longer reports a failure for an identity without
an object, so a testbench can check whether the object is made and make
it otherwise.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A component making its own object and a component given one have
constructors of their own instead of an optional model parameter. The
user guide describes the identities of components and their objects.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The handle holds the step and the id. Without an id the component still
takes its instance path as the identity, in its process, and the
testbench reaches the object by that path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The tests and the user guide use eval_integer, call_real and the like
rather than qualified expressions. The call returning integer,
call_integer_w_arg, gets the alias call_integer in python_pkg and
python_object_pkg, named like the other results.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
eval and call of std_ulogic_vector, signed and unsigned results, in
python_pkg and for objects, are also functions taking the width as a
length or as size_res. They call the procedure forms, so an error
returns a vector of 'U'.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A failure stops the test before the function returns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The python_pkg comment and the example no longer say that signed and
  unsigned results are procedures only; the example shows the sized
  function too.
- The aliasing of call is described for the functions without a width.
- The new features of the release notes are listed under New features.
- One class name in the identity examples of the user guide, and a class
  name given to a constructor rather than as a generic.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- counter_vc and filter_vc read p_model of their handle, like the user
  guide and VUnit's own verification components, instead of a get_model
  accessor.
- The Python objects page no longer repeats that string arguments are
  passed verbatim, a semantic of python_pkg that VUnit#16 changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The details are in the user guide, and the call_integer alias is nothing
a user needs to know about.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The default identity was python_bridge:python:object:package:module:Class:<n>.
python_object:package.module.Class:<n> is shorter and as unique: the
counter separates the objects of a class and the dotted class name the
classes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants