From 3c6a087163dd8e05ba2701f51853ed435782e422 Mon Sep 17 00:00:00 2001 From: Sebastian Abrahamsson Date: Thu, 8 Oct 2026 18:49:36 +0200 Subject: [PATCH 01/36] Prototype Python objects owned by VHDL 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 --- .../hdl/src/python_context.vhd | 1 + .../hdl/src/python_object_pkg.vhd | 175 ++++++++++++++++++ src/vunit_python_bridge/runtime.py | 25 +++ src/vunit_python_bridge/vunit_pkg.toml | 2 +- tests/counter_vc.vhd | 48 +++++ tests/models/counter_model.py | 23 +++ tests/run.py | 1 + tests/tb_python_objects.vhd | 77 ++++++++ tests/test_python_bridge.py | 2 +- 9 files changed, 352 insertions(+), 2 deletions(-) create mode 100644 src/vunit_python_bridge/hdl/src/python_object_pkg.vhd create mode 100644 tests/counter_vc.vhd create mode 100644 tests/models/counter_model.py create mode 100644 tests/tb_python_objects.vhd 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_object_pkg.vhd b/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd new file mode 100644 index 0000000..926d1cf --- /dev/null +++ b/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd @@ -0,0 +1,175 @@ +-- 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 +-- +-- PROTOTYPE: Python objects owned by VHDL. An object is an instance of a +-- Python class living in a session of its own, created on first use. + +library vunit_lib; +use vunit_lib.dict_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 ("package.module.Class", "package.module:Class", + -- or "Class" defined in the default session) created with args on first use. + -- The identity names the object and its session; it gets a unique one when + -- none is given. Two objects with the same identity are an error. + impure function new_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: the default backend of a + -- verification component that can also be given one + impure function object_or_new( + 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 session of the object, in which it is bound to self + impure function get_session(object : python_object_t) return python_session_t; + + -- Create the object now rather than on first use + procedure create(object : python_object_t); + + impure function call_integer(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[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_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_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_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]; + procedure call(object : python_object_t; method : string; 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 + impure function eval_integer(object : python_object_t; expr : string) return integer; + impure function eval_string(object : python_object_t; expr : string) return string; + + procedure p_create(object : python_object_t); + function p_self(method : string) return string; +end package; + +package body python_object_pkg is + constant session_idx : natural := 0; + constant code_idx : natural := 1; + constant created_idx : natural := 2; + constant num_anonymous : integer_vector_ptr_t := new_integer_vector_ptr(1); + -- The identities of the objects, by full name + constant identities : dict_t := new_dict; + + impure function new_object(class_name : string; args : arg_t := null_arg; id : id_t := null_id) + return python_object_t is + variable object_id : id_t := id; + variable data : integer_vector_ptr_t := new_integer_vector_ptr(3); + begin + if object_id = null_id then + object_id := get_id("object_" & integer'image(get(num_anonymous, 0)), parent => p_python_id); + set(num_anonymous, 0, get(num_anonymous, 0) + 1); + end if; + if has_key(identities, full_name(object_id)) then + failure( + get_logger(object_id), + "Two Python objects have the identity " & full_name(object_id) & ", give each one an id of its own" + ); + end if; + set_string(identities, full_name(object_id), ""); + 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__.create", arg(class_name), args)))); + return (p_data => data); + end; + + impure function object_or_new( + 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_object(class_name, args, id); + end; + + impure function get_session(object : python_object_t) return python_session_t is + begin + 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; + + procedure p_create(object : python_object_t) is + begin + if get(object.p_data, created_idx) = 0 then + set(object.p_data, created_idx, 1); + exec(to_string(to_string_ptr(get(object.p_data, code_idx))), get_session(object)); + end if; + end; + + procedure create(object : python_object_t) is + begin + p_create(object); + end; + + function p_self(method : string) return string is + begin + return "self." & method; + end; + + impure function call_integer(object : python_object_t; method : string; arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg) return integer is + begin + p_create(object); + return call_integer_w_arg(p_self(method), arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object)); + 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 + p_create(object); + return call_real(p_self(method), arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object)); + 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 + p_create(object); + return call_string(p_self(method), arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object)); + 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 + p_create(object); + return call_boolean(p_self(method), arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object)); + 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 + p_create(object); + return call_integer_vector(p_self(method), arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object)); + end; + procedure call(object : python_object_t; method : string; arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg) is + begin + p_create(object); + call(p_self(method), arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object)); + end; + + impure function eval_integer(object : python_object_t; expr : string) return integer is + begin + p_create(object); + return eval_integer(expr, get_session(object)); + end; + + impure function eval_string(object : python_object_t; expr : string) return string is + begin + p_create(object); + return eval_string(expr, get_session(object)); + end; +end package body; diff --git a/src/vunit_python_bridge/runtime.py b/src/vunit_python_bridge/runtime.py index 1231889..5252f78 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 @@ -136,6 +137,23 @@ def error(message): """ raise RuntimeError(message) + def create(self, class_name, *args, **kwargs): + """ + Create the object of a VHDL python_object_t and bind it to self in its + session: an instance of "package.module.Class", "package.module:Class" + or "Class" defined in the default session. + """ + namespace = self._runtime.namespace + if "self" in namespace: + raise RuntimeError("Two Python objects have the same identity, give each one an id of its own") + module_name, separator, attributes = class_name.partition(":") + if not separator: + module_name, _, attributes = class_name.rpartition(".") + target = importlib.import_module(module_name) if module_name else __main__ + for attribute in attributes.split("."): + target = getattr(target, attribute) + namespace["self"] = target(*args, **kwargs) + def __repr__(self): return "" @@ -250,6 +268,13 @@ def select_session(self, name): self._result = _NO_VALUE self._array_meta = {} + @property + def namespace(self): + """ + The namespace of the current session. + """ + return self._namespace + @property def _namespace(self): """ 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..3f01fcc --- /dev/null +++ b/tests/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 whose behaviour is a Python object: a Counter of +-- its own unless the bench gives it one. + +library vunit_lib; +context vunit_lib.vunit_context; + +library python_bridge; +context python_bridge.python_context; + +entity 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 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(counter_vc'path_name); + end if; + backend := object_or_new(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/models/counter_model.py b/tests/models/counter_model.py new file mode 100644 index 0000000..f366bca --- /dev/null +++ b/tests/models/counter_model.py @@ -0,0 +1,23 @@ +# 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 diff --git a/tests/run.py b/tests/run.py index 9307061..c9f8282 100644 --- a/tests/run.py +++ b/tests/run.py @@ -124,6 +124,7 @@ def main(): lib.add_source_file(ROOT / "tb_python_pkg_bridge.vhd") lib.add_source_file(ROOT / "tb_elaboration_array.vhd") expected_failures.append(ELABORATION_ARRAY_TEST) + lib.add_source_files([ROOT / "tb_python_objects.vhd", ROOT / "counter_vc.vhd"]) 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..47bd000 --- /dev/null +++ b/tests/tb_python_objects.vhd @@ -0,0 +1,77 @@ +-- 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 vunit_lib; +context vunit_lib.vunit_context; + +library python_bridge; +context python_bridge.python_context; + +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_object("models.counter_model:Counter", kwarg("start", 100)); + + signal tick : natural := 0; + signal count_a, count_b, count_c : integer; +begin + main : process + constant duplicate_id : id_t := get_id("duplicate"); + variable object : python_object_t; + 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(integer'(call(shared_model, "value")), 103); + check_equal(eval_string(shared_model, "type(self).__name__"), "Counter"); + + 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_object("Doubler"); + check_equal(integer'(call(object, "apply", arg(21))), 42); + + elsif run("Test that two objects with the same identity fail") then + mock(get_logger(duplicate_id), failure); + object := new_object("models.counter_model.Counter", id => duplicate_id); + check_no_log; + object := new_object("models.counter_model.Counter", id => duplicate_id); + check_only_log( + get_logger(duplicate_id), + "Two Python objects have the identity duplicate, give each one an id of its own", + failure + ); + unmock(get_logger(duplicate_id)); + end if; + end loop; + + test_runner_cleanup(runner); + end process; + + vc_a : entity work.counter_vc + generic map(id => get_id("vc_a")) + port map(tick => tick, count => count_a); + + -- No id: the instance path is the identity + vc_b : entity work.counter_vc + generic map(step => 2) + port map(tick => tick, count => count_b); + + vc_c : entity work.counter_vc + generic map(model => shared_model) + port map(tick => tick, count => count_c); +end architecture; diff --git a/tests/test_python_bridge.py b/tests/test_python_bridge.py index a8e5582..592b37d 100644 --- a/tests/test_python_bridge.py +++ b/tests/test_python_bridge.py @@ -157,7 +157,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) From fbf7e3939ef2775ca1ae97e74e3a124f1d544e0e Mon Sep 17 00:00:00 2001 From: Sebastian Abrahamsson Date: Thu, 8 Oct 2026 18:55:32 +0200 Subject: [PATCH 02/36] Generate the call and eval overloads of python_object_t 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 --- .../hdl/src/python_object_pkg.vhd | 349 ++++++++++++++++-- .../hdl/tools/generate_python_pkg.py | 134 ++++++- .../hdl/tools/python_object_pkg.vhd.in | 129 +++++++ tests/models/counter_model.py | 6 + tests/tb_python_objects.vhd | 15 + tests/test_python_bridge.py | 18 + 6 files changed, 623 insertions(+), 28 deletions(-) create mode 100644 src/vunit_python_bridge/hdl/tools/python_object_pkg.vhd.in diff --git a/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd b/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd index 926d1cf..659489d 100644 --- a/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd +++ b/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd @@ -6,9 +6,18 @@ -- -- PROTOTYPE: Python objects owned by VHDL. An object is an instance of a -- Python class living in a session of its own, created on first use. +-- +-- This file is a template, generated into hdl/src/python_object_pkg.vhd by +-- generate_python_pkg.py: the call and eval overloads of the objects are +-- those of python_pkg with the object first. + +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; @@ -43,21 +52,115 @@ package python_object_pkg is -- Create the object now rather than on first use procedure create(object : python_object_t); - impure function call_integer(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[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_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_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_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]; - procedure call(object : python_object_t; method : string; 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 + -- 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); procedure p_create(object : python_object_t); function p_self(method : string) return string; @@ -130,46 +233,238 @@ package body python_object_pkg is return "self." & method; end; - impure function call_integer(object : python_object_t; method : string; arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg) return integer is + procedure call( + object : python_object_t; method : string; + arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg + ) is + begin + p_create(object); + call( + p_self(method), arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => 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 + p_create(object); + return call_integer_w_arg( + p_self(method), arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object) + ); + 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 + p_create(object); + return call_real( + p_self(method), arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object) + ); + 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 + p_create(object); + return call_integer_vector( + p_self(method), arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object) + ); + 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 + p_create(object); + return call_real_vector( + p_self(method), arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object) + ); + 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 + p_create(object); + return call_string( + p_self(method), arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object) + ); + 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 + p_create(object); + return call_integer_vector_ptr( + p_self(method), arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object) + ); + 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 p_create(object); - return call_integer_w_arg(p_self(method), arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object)); + return call_boolean( + p_self(method), arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object) + ); 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 + + 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 p_create(object); - return call_real(p_self(method), arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object)); + return call_std_ulogic( + p_self(method), arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object) + ); 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 + + 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 p_create(object); - return call_string(p_self(method), arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object)); + return call_std_ulogic_vector( + p_self(method), arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object) + ); 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 + + 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 p_create(object); - return call_boolean(p_self(method), arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object)); + return call_integer_array( + p_self(method), arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object) + ); 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 + + 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 p_create(object); - return call_integer_vector(p_self(method), arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object)); + call_std_ulogic_vector( + p_self(method), result, + arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object) + ); end; - procedure call(object : python_object_t; method : string; arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg) is + + 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 p_create(object); - call(p_self(method), arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object)); + call_signed( + p_self(method), result, + arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object) + ); + 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 + p_create(object); + call_unsigned( + p_self(method), result, + arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object) + ); end; impure function eval_integer(object : python_object_t; expr : string) return integer is begin p_create(object); - return eval_integer(expr, get_session(object)); + return eval_integer(expr, session => get_session(object)); + end; + + impure function eval_real(object : python_object_t; expr : string) return real is + begin + p_create(object); + 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 + p_create(object); + 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 + p_create(object); + return eval_real_vector(expr, session => get_session(object)); end; impure function eval_string(object : python_object_t; expr : string) return string is begin p_create(object); - return eval_string(expr, get_session(object)); + 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 + p_create(object); + 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 + p_create(object); + 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 + p_create(object); + 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 + p_create(object); + 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 + p_create(object); + 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 + p_create(object); + 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 + p_create(object); + eval_signed(expr, result, session => get_session(object)); + end; + + procedure eval_unsigned(object : python_object_t; expr : string; result : out unsigned) is + begin + p_create(object); + eval_unsigned(expr, result, session => get_session(object)); end; end package body; 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..12e5942 100644 --- a/src/vunit_python_bridge/hdl/tools/generate_python_pkg.py +++ b/src/vunit_python_bridge/hdl/tools/generate_python_pkg.py @@ -5,7 +5,7 @@ # Copyright (c) 2014-2026, Lars Asplund lars.anders.asplund@gmail.com """ -Generate python_pkg.vhd from python_pkg.vhd.in in this directory. +Generate python_pkg.vhd and python_object_pkg.vhd from the templates 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 @@ -363,6 +363,137 @@ 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): + lines.append(f" alias call is {name}[") + lines.append(f" {OBJECT_ARG_SIGNATURE} 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 python_pkg operation in the session of the object. + """ + session = "session => get_session(object)" + parts = [ + f"""\ + procedure call( + {OBJECT}; method : string; + {ARG_PARAMETERS} + ) is + begin + p_create(object); + call( + p_self(method), {ARG_ACTUALS}, {session} + ); + 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 + p_create(object); + return {name}( + p_self(method), {ARG_ACTUALS}, {session} + ); + end; +""" + ) + for result in PROCEDURE_RESULTS: + parts.append( + f"""\ + procedure call_{result['name']}( + {OBJECT}; method : string; result : out {result['vhdl']}; + {ARG_PARAMETERS} + ) is + begin + p_create(object); + call_{result['name']}( + p_self(method), result, + {ARG_ACTUALS}, {session} + ); + end; +""" + ) + for result in RESULTS: + parts.append( + f"""\ + impure function eval_{result['name']}({OBJECT}; expr : string) return {result['vhdl']} is + begin + p_create(object); + 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 + p_create(object); + 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 +511,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..d3f2493 --- /dev/null +++ b/src/vunit_python_bridge/hdl/tools/python_object_pkg.vhd.in @@ -0,0 +1,129 @@ +-- 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 +-- +-- PROTOTYPE: Python objects owned by VHDL. An object is an instance of a +-- Python class living in a session of its own, created on first use. +-- +-- This file is a template, generated into hdl/src/python_object_pkg.vhd by +-- generate_python_pkg.py: the call and eval overloads of the objects are +-- those of python_pkg with the object first. + +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 ("package.module.Class", "package.module:Class", + -- or "Class" defined in the default session) created with args on first use. + -- The identity names the object and its session; it gets a unique one when + -- none is given. Two objects with the same identity are an error. + impure function new_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: the default backend of a + -- verification component that can also be given one + impure function object_or_new( + 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 session of the object, in which it is bound to self + impure function get_session(object : python_object_t) return python_session_t; + + -- Create the object now rather than on first use + procedure create(object : python_object_t); + +$object_declarations + + procedure p_create(object : python_object_t); + function p_self(method : string) return string; +end package; + +package body python_object_pkg is + constant session_idx : natural := 0; + constant code_idx : natural := 1; + constant created_idx : natural := 2; + constant num_anonymous : integer_vector_ptr_t := new_integer_vector_ptr(1); + -- The identities of the objects, by full name + constant identities : dict_t := new_dict; + + impure function new_object(class_name : string; args : arg_t := null_arg; id : id_t := null_id) + return python_object_t is + variable object_id : id_t := id; + variable data : integer_vector_ptr_t := new_integer_vector_ptr(3); + begin + if object_id = null_id then + object_id := get_id("object_" & integer'image(get(num_anonymous, 0)), parent => p_python_id); + set(num_anonymous, 0, get(num_anonymous, 0) + 1); + end if; + if has_key(identities, full_name(object_id)) then + failure( + get_logger(object_id), + "Two Python objects have the identity " & full_name(object_id) & ", give each one an id of its own" + ); + end if; + set_string(identities, full_name(object_id), ""); + 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__.create", arg(class_name), args)))); + return (p_data => data); + end; + + impure function object_or_new( + 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_object(class_name, args, id); + end; + + impure function get_session(object : python_object_t) return python_session_t is + begin + 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; + + procedure p_create(object : python_object_t) is + begin + if get(object.p_data, created_idx) = 0 then + set(object.p_data, created_idx, 1); + exec(to_string(to_string_ptr(get(object.p_data, code_idx))), get_session(object)); + end if; + end; + + procedure create(object : python_object_t) is + begin + p_create(object); + end; + + function p_self(method : string) return string is + begin + return "self." & method; + end; + +$object_subprograms +end package body; diff --git a/tests/models/counter_model.py b/tests/models/counter_model.py index f366bca..684be51 100644 --- a/tests/models/counter_model.py +++ b/tests/models/counter_model.py @@ -21,3 +21,9 @@ def add(self, times): def value(self): return self.count + + def history(self): + return list(range(self.count + 1)) + + def ratio(self): + return self.count / 4 diff --git a/tests/tb_python_objects.vhd b/tests/tb_python_objects.vhd index 47bd000..5bf7f69 100644 --- a/tests/tb_python_objects.vhd +++ b/tests/tb_python_objects.vhd @@ -5,6 +5,9 @@ -- Copyright (c) 2014-2026, Lars Asplund lars.anders.asplund@gmail.com -- +library ieee; +use ieee.numeric_std.all; + library vunit_lib; context vunit_lib.vunit_context; @@ -25,6 +28,7 @@ begin main : process constant duplicate_id : id_t := get_id("duplicate"); variable object : python_object_t; + variable byte : unsigned(7 downto 0); begin test_runner_setup(runner, runner_cfg); @@ -40,6 +44,17 @@ begin 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_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")); + 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_object("Doubler"); diff --git a/tests/test_python_bridge.py b/tests/test_python_bridge.py index 592b37d..3f427b8 100644 --- a/tests/test_python_bridge.py +++ b/tests/test_python_bridge.py @@ -11,6 +11,7 @@ library with the system C compiler, and reading/writing files, is fine. """ +import importlib.util import os import re import shutil @@ -168,6 +169,23 @@ def test_the_simulator_dependent_vhdl_is_not_listed(self): self.assertNotIn(f"hdl/src/{name}", includes) +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 From 049f6009cc48d7e30d758501a657aef77ae3b8e3 Mon Sep 17 00:00:00 2001 From: Sebastian Abrahamsson Date: Thu, 8 Oct 2026 18:58:33 +0200 Subject: [PATCH 03/36] Say that python_object_pkg.vhd is generated Co-Authored-By: Claude Opus 5.5 --- src/vunit_python_bridge/hdl/src/python_object_pkg.vhd | 8 +++++--- .../hdl/tools/python_object_pkg.vhd.in | 8 +++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd b/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd index 659489d..33e15f0 100644 --- a/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd +++ b/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd @@ -7,9 +7,11 @@ -- PROTOTYPE: Python objects owned by VHDL. An object is an instance of a -- Python class living in a session of its own, created on first use. -- --- This file is a template, generated into hdl/src/python_object_pkg.vhd by --- generate_python_pkg.py: the call and eval overloads of the objects are --- those of python_pkg with the object first. +-- 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; 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 index d3f2493..c4d63fe 100644 --- a/src/vunit_python_bridge/hdl/tools/python_object_pkg.vhd.in +++ b/src/vunit_python_bridge/hdl/tools/python_object_pkg.vhd.in @@ -7,9 +7,11 @@ -- PROTOTYPE: Python objects owned by VHDL. An object is an instance of a -- Python class living in a session of its own, created on first use. -- --- This file is a template, generated into hdl/src/python_object_pkg.vhd by --- generate_python_pkg.py: the call and eval overloads of the objects are --- those of python_pkg with the object first. +-- 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; From 5e8f1d1d17c4d20443845c9a9c541602a412b2d5 Mon Sep 17 00:00:00 2001 From: Sebastian Abrahamsson Date: Thu, 8 Oct 2026 19:03:57 +0200 Subject: [PATCH 04/36] Rename object_or_new to get Like get_logger and get_id, it gives the object it is given or a new one. Co-Authored-By: Claude Opus 5.5 --- src/vunit_python_bridge/hdl/src/python_object_pkg.vhd | 8 ++++---- .../hdl/tools/python_object_pkg.vhd.in | 8 ++++---- tests/counter_vc.vhd | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd b/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd index 33e15f0..71eb2ef 100644 --- a/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd +++ b/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd @@ -41,9 +41,9 @@ package python_object_pkg is impure function new_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: the default backend of a - -- verification component that can also be given one - impure function object_or_new( + -- 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( object : python_object_t; class_name : string; args : arg_t := null_arg; id : id_t := null_id ) return python_object_t; @@ -197,7 +197,7 @@ package body python_object_pkg is return (p_data => data); end; - impure function object_or_new( + impure function get( object : python_object_t; class_name : string; args : arg_t := null_arg; id : id_t := null_id ) return python_object_t is begin 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 index c4d63fe..da57281 100644 --- a/src/vunit_python_bridge/hdl/tools/python_object_pkg.vhd.in +++ b/src/vunit_python_bridge/hdl/tools/python_object_pkg.vhd.in @@ -41,9 +41,9 @@ package python_object_pkg is impure function new_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: the default backend of a - -- verification component that can also be given one - impure function object_or_new( + -- 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( object : python_object_t; class_name : string; args : arg_t := null_arg; id : id_t := null_id ) return python_object_t; @@ -89,7 +89,7 @@ package body python_object_pkg is return (p_data => data); end; - impure function object_or_new( + impure function get( object : python_object_t; class_name : string; args : arg_t := null_arg; id : id_t := null_id ) return python_object_t is begin diff --git a/tests/counter_vc.vhd b/tests/counter_vc.vhd index 3f01fcc..c6ad473 100644 --- a/tests/counter_vc.vhd +++ b/tests/counter_vc.vhd @@ -38,7 +38,7 @@ begin if vc_id = null_id then vc_id := get_id(counter_vc'path_name); end if; - backend := object_or_new(model, "models.counter_model.Counter", kwarg("step", step), vc_id); + backend := get(model, "models.counter_model.Counter", kwarg("step", step), vc_id); loop wait on tick; From 2db5d93f6dca7de471aabaf4bb5a6188dc7058fd Mon Sep 17 00:00:00 2001 From: Sebastian Abrahamsson Date: Thu, 8 Oct 2026 19:10:53 +0200 Subject: [PATCH 05/36] Document Python objects and report objects that cannot be created 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 --- README.md | 4 +- docs/running_code.rst | 132 +++++++++++++++++++++++++++++ docs/sessions.rst | 3 + src/vunit_python_bridge/runtime.py | 45 ++++++++-- tests/test_python_bridge.py | 40 +++++++++ 5 files changed, 215 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index ad88b08..8c81614 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/running_code.rst b/docs/running_code.rst index ed3b1de..c0e90d8 100644 --- a/docs/running_code.rst +++ b/docs/running_code.rst @@ -24,6 +24,138 @@ depend on ``python_cleanup``. python_cleanup; -- Optional, releases the staged values early test_runner_cleanup(runner); +.. _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_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()")); + +``new_object(class_name, args, id)`` names the class and its constructor +arguments, given like the arguments of ``call``, including groups made with +``&``. The class is + +* ``"package.module.Class"`` or ``"package.module:Class"``, imported by module + name from ``sys.path``: an installed package, the directory of the run + script or a directory on ``PYTHONPATH``. No file name is involved, so the + class is found the same way wherever the object is created. +* ``"Class"``, a class defined in the default session, for example by ``exec`` + or ``exec_file`` in the testbench. + +The object is created on first use, not by ``new_object``. An object can +therefore be a constant or a generic, created during elaboration, and does not +depend on the order of the processes or on ``test_runner_setup``. ``create`` +creates it explicitly. + +``call(object, method, ...)`` calls a method and ``eval(object, expr)`` +evaluates an expression in the session of the object, where the object is +``self``. They have the same arguments, result types and aliases as ``call`` +and ``eval``, including the procedure forms taking a ``std_ulogic_vector``, +``signed`` or ``unsigned`` result as an ``out`` parameter. + +Identity +~~~~~~~~ + +The ``id`` of an object names its session and its logger, which reports the +errors of its calls. Without an ``id``, an object gets a unique identity of +its own (``python_bridge:python:object_``). Two objects with the same +identity would share a session, so the second ``new_object`` of an identity is +reported as a failure on its logger. + +A verification component that has an identity, given by its handle or a +generic as is usual for VUnit verification components, gives it to its object. +One without takes the path of its instance, ``'path_name``, 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. + +Backends of verification components +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``get(object, class_name, args, id)`` returns ``object`` when it is given, and +a new object otherwise, like ``get_logger`` and ``get_id``. A component taking +an object as a generic thereby gets a backend of its own by default and the +one of the testbench when given: + +.. code-block:: vhdl + + entity counter_vc is + generic( + id : id_t := null_id; + model : python_object_t := null_python_object; + step : natural := 1 + ); + port(tick : in natural; count : out integer); + end entity; + + architecture python of counter_vc is + begin + process + variable vc_id : id_t := id; + variable backend : python_object_t; + begin + if vc_id = null_id then + vc_id := get_id(counter_vc'path_name); + end if; + backend := get(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; + +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: + +.. code-block:: vhdl + + constant scoreboard : python_object_t := new_object("my_models.Scoreboard"); + + ... + + monitor : entity work.my_monitor generic map(model => scoreboard) ...; + + ... + + check_equal(integer'(call(scoreboard, "num_mismatches")), 0); + +The class can be made a choice of the user in the same way, with a ``string`` +generic as its name. + +Errors and limitations +~~~~~~~~~~~~~~~~~~~~~~ + +* An object that cannot be created, for example since its module is not + found, reports the error of the import or the constructor once. Its later + calls fail 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. +* ``integer_array_t`` constructor arguments are transferred when the object is + given them, which is during elaboration for an object made there. +* Objects need sessions, and so the Python bridge: NVC, GHDL or Questa. + exec ---- diff --git a/docs/sessions.rst b/docs/sessions.rst index 8050975..78db2b4 100644 --- a/docs/sessions.rst +++ b/docs/sessions.rst @@ -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/src/vunit_python_bridge/runtime.py b/src/vunit_python_bridge/runtime.py index 5252f78..fec2ea4 100644 --- a/src/vunit_python_bridge/runtime.py +++ b/src/vunit_python_bridge/runtime.py @@ -110,6 +110,36 @@ def _type_error(kind, value, expected): ) +def _resolve_class(class_name): + """ + The class named "package.module.Class", "package.module:Class" or "Class" + defined in the default session (__main__). + """ + module_name, separator, attributes = class_name.partition(":") + if not separator: + module_name, _, attributes = class_name.rpartition(".") + target = importlib.import_module(module_name) if module_name else __main__ + for attribute in attributes.split("."): + target = getattr(target, attribute) + return target + + +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): + self._message = f"The {class_name} object could not be created: {type(exc).__name__}: {exc}" + + def __getattr__(self, name): + raise RuntimeError(self._message) + + def __repr__(self): + return f"<{self._message}>" + + class BridgeHandle: """ The ``__vunit__`` object that VHDL-generated Python expressions use. @@ -141,18 +171,17 @@ def create(self, class_name, *args, **kwargs): """ Create the object of a VHDL python_object_t and bind it to self in its session: an instance of "package.module.Class", "package.module:Class" - or "Class" defined in the default session. + or "Class" defined in the default session. If that fails, self raises + the reason on every use instead of being undefined. """ namespace = self._runtime.namespace if "self" in namespace: raise RuntimeError("Two Python objects have the same identity, give each one an id of its own") - module_name, separator, attributes = class_name.partition(":") - if not separator: - module_name, _, attributes = class_name.rpartition(".") - target = importlib.import_module(module_name) if module_name else __main__ - for attribute in attributes.split("."): - target = getattr(target, attribute) - namespace["self"] = target(*args, **kwargs) + try: + namespace["self"] = _resolve_class(class_name)(*args, **kwargs) + except Exception as exc: + namespace["self"] = _UncreatedObject(class_name, exc) + raise def __repr__(self): return "" diff --git a/tests/test_python_bridge.py b/tests/test_python_bridge.py index 3f427b8..323c78a 100644 --- a/tests/test_python_bridge.py +++ b/tests/test_python_bridge.py @@ -169,6 +169,46 @@ def test_the_simulator_dependent_vhdl_is_not_listed(self): self.assertNotIn(f"hdl/src/{name}", includes) +class TestObjectCreation(unittest.TestCase): + """ + __vunit__.create, 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_module_class_to_self(self): + for class_name in ("collections.OrderedDict", "collections:OrderedDict"): + self.namespace.clear() + self.handle.create(class_name, a=1) + self.assertEqual(self.namespace["self"], {"a": 1}, class_name) + + 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.create("Model") + self.assertIsInstance(self.namespace["self"], Model) + + def test_a_second_object_in_a_session_is_an_error(self): + self.handle.create("collections.OrderedDict") + with self.assertRaisesRegex(RuntimeError, "same identity"): + self.handle.create("collections.OrderedDict") + + def test_an_object_that_cannot_be_created_tells_why_when_used(self): + with self.assertRaises(ModuleNotFoundError): + self.handle.create("no_such_module.Model", 1) + with self.assertRaisesRegex( + RuntimeError, + "The no_such_module.Model object could not be created: ModuleNotFoundError: No module named 'no_such_module'", + ): + self.namespace["self"].step() + + class TestGeneratedPackages(unittest.TestCase): """ The generated VHDL packages are those the generator gives for the templates. From 1e5efafc7a3fc49c7cfe4d8e87bb1d305a84a527 Mon Sep 17 00:00:00 2001 From: Sebastian Abrahamsson Date: Thu, 8 Oct 2026 19:22:10 +0200 Subject: [PATCH 06/36] Name the constructor new_python_object and enumerate anonymous objects 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:, 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 --- docs/running_code.rst | 106 +++++++++++------- .../hdl/src/python_object_pkg.vhd | 22 ++-- .../hdl/tools/python_object_pkg.vhd.in | 22 ++-- tests/counter_vc.vhd | 28 +---- tests/counter_vc_pkg.vhd | 41 +++++++ tests/path_counter_vc.vhd | 48 ++++++++ tests/run.py | 3 +- tests/tb_python_objects.vhd | 30 +++-- 8 files changed, 204 insertions(+), 96 deletions(-) create mode 100644 tests/counter_vc_pkg.vhd create mode 100644 tests/path_counter_vc.vhd diff --git a/docs/running_code.rst b/docs/running_code.rst index c0e90d8..b8494da 100644 --- a/docs/running_code.rst +++ b/docs/running_code.rst @@ -38,7 +38,7 @@ own, and a testbench can create one and give it to a component. .. code-block:: vhdl - constant model : python_object_t := new_object( + constant model : python_object_t := new_python_object( "my_models.uart:UartModel", kwarg("baud", 115200), id => get_id("uart") ); @@ -48,7 +48,7 @@ own, and a testbench can create one and give it to a component. call(model, "reset"); -- no return value check_true(eval_boolean(model, "self.is_idle()")); -``new_object(class_name, args, id)`` names the class and its constructor +``new_python_object(class_name, args, id)`` names the class and its constructor arguments, given like the arguments of ``call``, including groups made with ``&``. The class is @@ -59,7 +59,7 @@ arguments, given like the arguments of ``call``, including groups made with * ``"Class"``, a class defined in the default session, for example by ``exec`` or ``exec_file`` in the testbench. -The object is created on first use, not by ``new_object``. An object can +The object is created on first use, not by ``new_python_object``. An object can therefore be a constant or a generic, created during elaboration, and does not depend on the order of the processes or on ``test_runner_setup``. ``create`` creates it explicitly. @@ -74,54 +74,74 @@ Identity ~~~~~~~~ The ``id`` of an object names its session and its logger, which reports the -errors of its calls. Without an ``id``, an object gets a unique identity of -its own (``python_bridge:python:object_``). Two objects with the same -identity would share a session, so the second ``new_object`` of an identity is -reported as a failure on its logger. - -A verification component that has an identity, given by its handle or a -generic as is usual for VUnit verification components, gives it to its object. -One without takes the path of its instance, ``'path_name``, 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. +errors of its calls. 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. Two objects with the same +identity would share a session, so the second ``new_python_object`` of an +identity is reported as a failure on its logger. Backends of verification components ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -``get(object, class_name, args, id)`` returns ``object`` when it is given, and -a new object otherwise, like ``get_logger`` and ``get_id``. A component taking -an object as a generic thereby gets a backend of its own by default and the -one of the testbench when given: +``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: .. code-block:: vhdl - entity counter_vc is - generic( - id : id_t := null_id; - model : python_object_t := null_python_object; - step : natural := 1 + 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)) ); - port(tick : in natural; count : out integer); - end entity; + end; + +The component itself only calls the object of its handle: + +.. code-block:: vhdl + + entity uart_master is + generic(master : uart_master_t); + ... - architecture python of counter_vc is + busy <= call_boolean(master.p_model, "transmit", arg(to_integer(data))); + +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. ``tests/counter_vc_pkg.vhd`` and ``tests/counter_vc.vhd`` of the +repository are a complete component of this kind. + +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 - process - variable vc_id : id_t := id; - variable backend : python_object_t; - begin - if vc_id = null_id then - vc_id := get_id(counter_vc'path_name); - end if; - backend := get(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; + 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); + ... 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 @@ -129,11 +149,11 @@ monitor and checked by the test: .. code-block:: vhdl - constant scoreboard : python_object_t := new_object("my_models.Scoreboard"); + constant scoreboard : python_object_t := new_python_object("my_models.Scoreboard"); ... - monitor : entity work.my_monitor generic map(model => scoreboard) ...; + monitor : entity work.my_monitor generic map(monitor => new_my_monitor(model => scoreboard)) ...; ... diff --git a/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd b/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd index 71eb2ef..9d7a1c4 100644 --- a/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd +++ b/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd @@ -36,14 +36,15 @@ package python_object_pkg is -- An instance of class_name ("package.module.Class", "package.module:Class", -- or "Class" defined in the default session) created with args on first use. - -- The identity names the object and its session; it gets a unique one when - -- none is given. Two objects with the same identity are an error. - impure function new_object(class_name : string; args : arg_t := null_arg; id : id_t := null_id) + -- The identity names the object and its session. Without one, objects are + -- enumerated like VUnit's verification components: python_bridge:python:object:. + -- Two objects with the same identity are an error. + impure function new_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( + 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; @@ -172,18 +173,19 @@ package body python_object_pkg is constant session_idx : natural := 0; constant code_idx : natural := 1; constant created_idx : natural := 2; - constant num_anonymous : integer_vector_ptr_t := new_integer_vector_ptr(1); + -- The parent of the identities of the objects created without one + constant anonymous_id : id_t := get_id("object", parent => p_python_id); -- The identities of the objects, by full name constant identities : dict_t := new_dict; - impure function new_object(class_name : string; args : arg_t := null_arg; id : id_t := null_id) + impure function new_python_object(class_name : string; args : arg_t := null_arg; id : id_t := null_id) return python_object_t is variable object_id : id_t := id; variable data : integer_vector_ptr_t := new_integer_vector_ptr(3); begin if object_id = null_id then - object_id := get_id("object_" & integer'image(get(num_anonymous, 0)), parent => p_python_id); - set(num_anonymous, 0, get(num_anonymous, 0) + 1); + -- 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 has_key(identities, full_name(object_id)) then failure( @@ -197,14 +199,14 @@ package body python_object_pkg is return (p_data => data); end; - impure function get( + 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_object(class_name, args, id); + return new_python_object(class_name, args, id); end; impure function get_session(object : python_object_t) return python_session_t is 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 index da57281..18b76aa 100644 --- a/src/vunit_python_bridge/hdl/tools/python_object_pkg.vhd.in +++ b/src/vunit_python_bridge/hdl/tools/python_object_pkg.vhd.in @@ -36,14 +36,15 @@ package python_object_pkg is -- An instance of class_name ("package.module.Class", "package.module:Class", -- or "Class" defined in the default session) created with args on first use. - -- The identity names the object and its session; it gets a unique one when - -- none is given. Two objects with the same identity are an error. - impure function new_object(class_name : string; args : arg_t := null_arg; id : id_t := null_id) + -- The identity names the object and its session. Without one, objects are + -- enumerated like VUnit's verification components: python_bridge:python:object:. + -- Two objects with the same identity are an error. + impure function new_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( + 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; @@ -64,18 +65,19 @@ package body python_object_pkg is constant session_idx : natural := 0; constant code_idx : natural := 1; constant created_idx : natural := 2; - constant num_anonymous : integer_vector_ptr_t := new_integer_vector_ptr(1); + -- The parent of the identities of the objects created without one + constant anonymous_id : id_t := get_id("object", parent => p_python_id); -- The identities of the objects, by full name constant identities : dict_t := new_dict; - impure function new_object(class_name : string; args : arg_t := null_arg; id : id_t := null_id) + impure function new_python_object(class_name : string; args : arg_t := null_arg; id : id_t := null_id) return python_object_t is variable object_id : id_t := id; variable data : integer_vector_ptr_t := new_integer_vector_ptr(3); begin if object_id = null_id then - object_id := get_id("object_" & integer'image(get(num_anonymous, 0)), parent => p_python_id); - set(num_anonymous, 0, get(num_anonymous, 0) + 1); + -- 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 has_key(identities, full_name(object_id)) then failure( @@ -89,14 +91,14 @@ package body python_object_pkg is return (p_data => data); end; - impure function get( + 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_object(class_name, args, id); + return new_python_object(class_name, args, id); end; impure function get_session(object : python_object_t) return python_session_t is diff --git a/tests/counter_vc.vhd b/tests/counter_vc.vhd index c6ad473..0fdda31 100644 --- a/tests/counter_vc.vhd +++ b/tests/counter_vc.vhd @@ -4,8 +4,7 @@ -- -- Copyright (c) 2014-2026, Lars Asplund lars.anders.asplund@gmail.com -- --- A verification component whose behaviour is a Python object: a Counter of --- its own unless the bench gives it one. +-- A verification component whose behaviour is the Python object of its handle. library vunit_lib; context vunit_lib.vunit_context; @@ -13,14 +12,10 @@ context vunit_lib.vunit_context; library python_bridge; context python_bridge.python_context; +use work.counter_vc_pkg.all; + entity 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 - ); + generic(vc : counter_vc_t); port( tick : in natural; count : out integer @@ -30,19 +25,8 @@ end entity; architecture python of 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(counter_vc'path_name); - end if; - backend := get(model, "models.counter_model.Counter", kwarg("step", step), vc_id); - - loop - wait on tick; - count <= call(backend, "add", arg(1)); - end loop; + 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/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 c9f8282..b752e36 100644 --- a/tests/run.py +++ b/tests/run.py @@ -124,7 +124,8 @@ def main(): lib.add_source_file(ROOT / "tb_python_pkg_bridge.vhd") lib.add_source_file(ROOT / "tb_elaboration_array.vhd") expected_failures.append(ELABORATION_ARRAY_TEST) - lib.add_source_files([ROOT / "tb_python_objects.vhd", ROOT / "counter_vc.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 index 5bf7f69..62f4aa0 100644 --- a/tests/tb_python_objects.vhd +++ b/tests/tb_python_objects.vhd @@ -14,16 +14,18 @@ context vunit_lib.vunit_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_object("models.counter_model:Counter", kwarg("start", 100)); + 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 : integer; + signal count_a, count_b, count_c, count_d : integer; begin main : process constant duplicate_id : id_t := get_id("duplicate"); @@ -41,11 +43,12 @@ begin 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_object("models.counter_model.Counter", kwarg("start", 5)); + 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)); @@ -57,14 +60,16 @@ begin 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_object("Doubler"); + 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 two objects with the same identity fail") then mock(get_logger(duplicate_id), failure); - object := new_object("models.counter_model.Counter", id => duplicate_id); + object := new_python_object("models.counter_model.Counter", id => duplicate_id); check_no_log; - object := new_object("models.counter_model.Counter", id => duplicate_id); + object := new_python_object("models.counter_model.Counter", id => duplicate_id); check_only_log( get_logger(duplicate_id), "Two Python objects have the identity duplicate, give each one an id of its own", @@ -78,15 +83,20 @@ begin end process; vc_a : entity work.counter_vc - generic map(id => get_id("vc_a")) + generic map(vc => new_counter_vc(id => get_id("vc_a"))) port map(tick => tick, count => count_a); - -- No id: the instance path is the identity + -- No id: an enumerated anonymous identity vc_b : entity work.counter_vc - generic map(step => 2) + generic map(vc => new_counter_vc(step => 2)) port map(tick => tick, count => count_b); vc_c : entity work.counter_vc - generic map(model => shared_model) + 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; From 5d71165027866a53b832aa984a25a8541c51cd70 Mon Sep 17 00:00:00 2001 From: Sebastian Abrahamsson Date: Thu, 8 Oct 2026 19:39:55 +0200 Subject: [PATCH 07/36] Add exec and get_logger for objects and report objects on VHPI 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 --- docs/running_code.rst | 22 ++++++++++++------- .../hdl/src/python_ffi_pkg_bridge.vhd | 3 +++ .../hdl/src/python_object_pkg.vhd | 20 ++++++++++++++++- .../hdl/src/python_pkg_vhpi.vhd | 3 +++ .../hdl/tools/python_object_pkg.vhd.in | 20 ++++++++++++++++- tests/tb_python_objects.vhd | 6 +++++ 6 files changed, 64 insertions(+), 10 deletions(-) diff --git a/docs/running_code.rst b/docs/running_code.rst index b8494da..2f1ab60 100644 --- a/docs/running_code.rst +++ b/docs/running_code.rst @@ -47,6 +47,7 @@ own, and a testbench can create one and give it to a component. 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 its constructor arguments, given like the arguments of ``call``, including groups made with @@ -64,17 +65,17 @@ therefore be a constant or a generic, created during elaboration, and does not depend on the order of the processes or on ``test_runner_setup``. ``create`` creates it explicitly. -``call(object, method, ...)`` calls a method and ``eval(object, expr)`` -evaluates an expression in the session of the object, where the object is -``self``. They have the same arguments, result types and aliases as ``call`` +``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`` and ``eval``, including the procedure forms taking a ``std_ulogic_vector``, ``signed`` or ``unsigned`` result as an ``out`` parameter. Identity ~~~~~~~~ -The ``id`` of an object names its session and its logger, which reports the -errors of its calls. Without an ``id``, objects are enumerated the way VUnit +The ``id`` of an object names its session and its logger, ``get_logger(object)``, +which reports the errors of its operations. 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. Two objects with the same identity would share a session, so the second ``new_python_object`` of an @@ -172,9 +173,14 @@ Errors and limitations it, is shared by all objects. * String arguments are passed to Python verbatim, see :ref:`python_bridge:semantics`, which applies to constructor arguments too. -* ``integer_array_t`` constructor arguments are transferred when the object is - given them, which is during elaboration for an object made there. -* Objects need sessions, and so the Python bridge: NVC, GHDL or Questa. +* 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))``. +* 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 their first use reports that. exec ---- 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 ab9260d..6ac181a 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 @@ -43,6 +43,9 @@ package python_ffi_pkg is -- that cannot be converted to Python and the transfer of an integer_array_t. 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; + -- A session is a Python namespace. Sessions are isolated from each other -- and have an identity, which is what they are created from: -- diff --git a/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd b/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd index 9d7a1c4..bfde6e1 100644 --- a/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd +++ b/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd @@ -49,12 +49,17 @@ package python_object_pkg is ) 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; -- Create the object now rather than on first use procedure create(object : python_object_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; @@ -221,7 +226,9 @@ package body python_object_pkg is procedure p_create(object : python_object_t) is begin - if get(object.p_data, created_idx) = 0 then + if not p_supports_sessions then + failure(get_logger(object), "Python objects require NVC, GHDL or Questa"); + elsif get(object.p_data, created_idx) = 0 then set(object.p_data, created_idx, 1); exec(to_string(to_string_ptr(get(object.p_data, code_idx))), get_session(object)); end if; @@ -232,6 +239,17 @@ package body python_object_pkg is p_create(object); end; + procedure exec(object : python_object_t; code : string) is + begin + p_create(object); + exec(code, get_session(object)); + end; + + impure function get_logger(object : python_object_t) return logger_t is + begin + return get_logger(get_id(object)); + end; + function p_self(method : string) return string is begin return "self." & method; 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..3f6efd1 100644 --- a/src/vunit_python_bridge/hdl/src/python_pkg_vhpi.vhd +++ b/src/vunit_python_bridge/hdl/src/python_pkg_vhpi.vhd @@ -93,6 +93,9 @@ package python_ffi_pkg is -- Logger of the Python interface, the parent of the session loggers 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 := false; + -- Result kinds, must match src/vunit_python_bridge/runtime.py constant p_kind_integer : integer := 0; constant p_kind_real : integer := 1; 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 index 18b76aa..c58c8dd 100644 --- a/src/vunit_python_bridge/hdl/tools/python_object_pkg.vhd.in +++ b/src/vunit_python_bridge/hdl/tools/python_object_pkg.vhd.in @@ -49,12 +49,17 @@ package python_object_pkg is ) 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; -- Create the object now rather than on first use procedure create(object : python_object_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 procedure p_create(object : python_object_t); @@ -113,7 +118,9 @@ package body python_object_pkg is procedure p_create(object : python_object_t) is begin - if get(object.p_data, created_idx) = 0 then + if not p_supports_sessions then + failure(get_logger(object), "Python objects require NVC, GHDL or Questa"); + elsif get(object.p_data, created_idx) = 0 then set(object.p_data, created_idx, 1); exec(to_string(to_string_ptr(get(object.p_data, code_idx))), get_session(object)); end if; @@ -124,6 +131,17 @@ package body python_object_pkg is p_create(object); end; + procedure exec(object : python_object_t; code : string) is + begin + p_create(object); + exec(code, get_session(object)); + end; + + impure function get_logger(object : python_object_t) return logger_t is + begin + return get_logger(get_id(object)); + end; + function p_self(method : string) return string is begin return "self." & method; diff --git a/tests/tb_python_objects.vhd b/tests/tb_python_objects.vhd index 62f4aa0..5580cda 100644 --- a/tests/tb_python_objects.vhd +++ b/tests/tb_python_objects.vhd @@ -58,6 +58,12 @@ begin check_equal(length(call_integer_array(object, "history")), 7); check_true(eval_boolean(object, "self.count == 6")); + elsif run("Test exec and get_logger of an object") then + object := new_python_object("models.counter_model.Counter", id => get_id("exec_test")); + 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"); From b28b3152d17424ced01cdb16387224410f1ce3be Mon Sep 17 00:00:00 2001 From: Sebastian Abrahamsson Date: Thu, 8 Oct 2026 19:41:50 +0200 Subject: [PATCH 08/36] Show Python objects as backends in the embedded_python example 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 --- docs/other_simulators.rst | 7 ++-- docs/running_code.rst | 11 ++++-- examples/embedded_python/filter_model.py | 24 +++++++++++++ examples/embedded_python/filter_vc.vhd | 33 +++++++++++++++++ examples/embedded_python/filter_vc_pkg.vhd | 42 ++++++++++++++++++++++ examples/embedded_python/tb_example.vhd | 36 +++++++++++++++++++ 6 files changed, 147 insertions(+), 6 deletions(-) create mode 100644 examples/embedded_python/filter_model.py create mode 100644 examples/embedded_python/filter_vc.vhd create mode 100644 examples/embedded_python/filter_vc_pkg.vhd 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/running_code.rst b/docs/running_code.rst index 2f1ab60..567d432 100644 --- a/docs/running_code.rst +++ b/docs/running_code.rst @@ -123,8 +123,12 @@ 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. ``tests/counter_vc_pkg.vhd`` and ``tests/counter_vc.vhd`` of the -repository are a complete component of this kind. +objects. ``filter_vc_pkg.vhd`` and ``filter_vc.vhd`` of the `embedded_python +example `__ +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. 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 @@ -176,7 +180,8 @@ Errors and limitations * 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))``. + 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 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..7e9aa62 --- /dev/null +++ b/examples/embedded_python/filter_vc.vhd @@ -0,0 +1,33 @@ +-- 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. +-- Its object is not created in Python until the first input arrives. + +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( + x : in integer; + y : out integer + ); +end entity; + +architecture python of filter_vc is +begin + process + begin + wait on x; + y <= call(get_model(filter), "push", arg(x)); + 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 6cccf63..fd11e04 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,14 @@ 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_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 +720,18 @@ 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 + filter_x <= 10 * idx; + wait for clk_period; + end loop; + 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 -- @@ -800,4 +822,18 @@ begin acc_b_inst : entity work.accumulator_model 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(x => filter_x, y => filter_a_y); + + filter_b_inst : entity work.filter_vc + generic map(filter => new_filter_vc(window => 4)) + port map(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(x => filter_x, y => filter_c_y); end; From c6f71a35413a072d72fe0ef4a1600c3af55a61c7 Mon Sep 17 00:00:00 2001 From: Sebastian Abrahamsson Date: Thu, 8 Oct 2026 19:47:41 +0200 Subject: [PATCH 09/36] Show how a model class is made importable, fix the class example From the verification component pattern of #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 --- docs/running_code.rst | 36 ++++++++++++++++++++++++++++++++++++ docs/sessions.rst | 2 +- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/docs/running_code.rst b/docs/running_code.rst index 567d432..04a7119 100644 --- a/docs/running_code.rst +++ b/docs/running_code.rst @@ -71,6 +71,42 @@ of the object, where the object is ``self``. They have the same arguments, resul and ``eval``, including the procedure forms taking a ``std_ulogic_vector``, ``signed`` or ``unsigned`` result as an ``out`` parameter. +Making a class importable +~~~~~~~~~~~~~~~~~~~~~~~~~ + +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 + +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: + +.. 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() + ... + +An alternative is to make the model an installable Python package and install +it in the virtual environment of the project with ``pip install -e``. + Identity ~~~~~~~~ diff --git a/docs/sessions.rst b/docs/sessions.rst index 78db2b4..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); From 189d04dd764b22662bbeb158f86869c98310d73e Mon Sep 17 00:00:00 2001 From: Sebastian Abrahamsson Date: Thu, 8 Oct 2026 19:56:11 +0200 Subject: [PATCH 10/36] Polish python_object_pkg Name the failures of objects after what failed: new_python_object("") for a failed creation and call("") 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 --- docs/running_code.rst | 27 +++++++++----- .../hdl/src/python_object_pkg.vhd | 35 ++++++++++++++----- .../hdl/src/python_pkg_vhpi.vhd | 14 ++++++++ .../hdl/tools/generate_python_pkg.py | 7 ++-- .../hdl/tools/python_object_pkg.vhd.in | 28 +++++++++++---- 5 files changed, 84 insertions(+), 27 deletions(-) diff --git a/docs/running_code.rst b/docs/running_code.rst index 04a7119..f874eba 100644 --- a/docs/running_code.rst +++ b/docs/running_code.rst @@ -67,9 +67,10 @@ creates it explicitly. ``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`` -and ``eval``, including the procedure forms taking a ``std_ulogic_vector``, -``signed`` or ``unsigned`` result as an ``out`` parameter. +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. Making a class importable ~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -110,9 +111,10 @@ it in the virtual environment of the project with ``pip install -e``. Identity ~~~~~~~~ -The ``id`` of an object names its session and its logger, ``get_logger(object)``, -which reports the errors of its operations. Without an ``id``, objects are enumerated the way VUnit -enumerates verification components: ``python_bridge:python:object:1``, +The ``id`` of an object names its session and its logger, +``get_logger(object)``, which reports the errors of its operations. 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. Two objects with the same identity would share a session, so the second ``new_python_object`` of an identity is reported as a failure on its logger. @@ -153,7 +155,11 @@ The component itself only calls the object of its handle: generic(master : uart_master_t); ... - busy <= call_boolean(master.p_model, "transmit", arg(to_integer(data))); + 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 the object, which is not created in Python until its first call. ``create_std_cfg`` gives @@ -206,9 +212,12 @@ generic as its name. Errors and limitations ~~~~~~~~~~~~~~~~~~~~~~ +* Errors are reported on the logger of the object, named after what failed, + for example ``call("push") failed:`` followed by the Python traceback. * An object that cannot be created, for example since its module is not - found, reports the error of the import or the constructor once. Its later - calls fail with ``The object could not be created: ``. + found, reports the error of the import or the constructor once, as + ``new_python_object("") failed:``. Its later calls fail 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 diff --git a/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd b/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd index bfde6e1..200b79e 100644 --- a/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd +++ b/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd @@ -4,8 +4,10 @@ -- -- Copyright (c) 2014-2026, Lars Asplund lars.anders.asplund@gmail.com -- --- PROTOTYPE: Python objects owned by VHDL. An object is an instance of a --- Python class living in a session of its own, created on first use. +-- 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. @@ -169,15 +171,13 @@ package python_object_pkg is 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); - - procedure p_create(object : python_object_t); - function p_self(method : string) return string; end package; package body python_object_pkg is constant session_idx : natural := 0; constant code_idx : natural := 1; constant created_idx : natural := 2; + constant class_name_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 identities of the objects, by full name @@ -186,7 +186,7 @@ package body python_object_pkg is impure function new_python_object(class_name : string; args : arg_t := null_arg; id : id_t := null_id) return python_object_t is variable object_id : id_t := id; - variable data : integer_vector_ptr_t := new_integer_vector_ptr(3); + variable data : integer_vector_ptr_t := new_integer_vector_ptr(4); begin if object_id = null_id then -- Like enumerate of vc_pkg, which is only there with the verification components @@ -201,6 +201,7 @@ package body python_object_pkg is set_string(identities, full_name(object_id), ""); 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__.create", arg(class_name), args)))); + set(data, class_name_idx, to_integer(new_string_ptr(class_name))); return (p_data => data); end; @@ -224,13 +225,20 @@ package body python_object_pkg is return get_id(get_session(object)); end; + -- Create the object unless it is created. A failure is reported once, as the + -- failure of new_python_object, and the object then raises it on every use. procedure p_create(object : python_object_t) is + variable ok : boolean; begin if not p_supports_sessions then failure(get_logger(object), "Python objects require NVC, GHDL or Questa"); elsif get(object.p_data, created_idx) = 0 then set(object.p_data, created_idx, 1); - exec(to_string(to_string_ptr(get(object.p_data, code_idx))), get_session(object)); + ok := p_exec( + to_string(to_string_ptr(get(object.p_data, code_idx))), 0, + "new_python_object(""" & to_string(to_string_ptr(get(object.p_data, class_name_idx))) & """)", + get_session(object) + ); end if; end; @@ -255,14 +263,23 @@ package body python_object_pkg is return "self." & method; 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 p_create(object); - call( - p_self(method), arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object) + -- An error is reported as that of the method rather than of the code executed + ok := p_exec( + to_call_str(p_self(method), arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10), 0, p_call_operation(method), + get_session(object) ); end; 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 3f6efd1..d69ca5e 100644 --- a/src/vunit_python_bridge/hdl/src/python_pkg_vhpi.vhd +++ b/src/vunit_python_bridge/hdl/src/python_pkg_vhpi.vhd @@ -80,6 +80,11 @@ 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); ----------------------------------------------------------------------------- @@ -174,6 +179,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 + p_check_session(session); + p_exec(text); + return true; + end; + impure function eval_integer(expr : string; session : python_session_t := default_session) return integer is begin p_check_session(session); 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 12e5942..e365925 100644 --- a/src/vunit_python_bridge/hdl/tools/generate_python_pkg.py +++ b/src/vunit_python_bridge/hdl/tools/generate_python_pkg.py @@ -420,10 +420,13 @@ def object_subprograms(): {OBJECT}; method : string; {ARG_PARAMETERS} ) is + variable ok : boolean; begin p_create(object); - call( - p_self(method), {ARG_ACTUALS}, {session} + -- An error is reported as that of the method rather than of the code executed + ok := p_exec( + to_call_str(p_self(method), {ARG_ACTUALS}), 0, p_call_operation(method), + get_session(object) ); end; """ 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 index c58c8dd..14cd632 100644 --- a/src/vunit_python_bridge/hdl/tools/python_object_pkg.vhd.in +++ b/src/vunit_python_bridge/hdl/tools/python_object_pkg.vhd.in @@ -4,8 +4,10 @@ -- -- Copyright (c) 2014-2026, Lars Asplund lars.anders.asplund@gmail.com -- --- PROTOTYPE: Python objects owned by VHDL. An object is an instance of a --- Python class living in a session of its own, created on first use. +-- 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. @@ -61,15 +63,13 @@ package python_object_pkg is procedure exec(object : python_object_t; code : string); $object_declarations - - procedure p_create(object : python_object_t); - function p_self(method : string) return string; end package; package body python_object_pkg is constant session_idx : natural := 0; constant code_idx : natural := 1; constant created_idx : natural := 2; + constant class_name_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 identities of the objects, by full name @@ -78,7 +78,7 @@ package body python_object_pkg is impure function new_python_object(class_name : string; args : arg_t := null_arg; id : id_t := null_id) return python_object_t is variable object_id : id_t := id; - variable data : integer_vector_ptr_t := new_integer_vector_ptr(3); + variable data : integer_vector_ptr_t := new_integer_vector_ptr(4); begin if object_id = null_id then -- Like enumerate of vc_pkg, which is only there with the verification components @@ -93,6 +93,7 @@ package body python_object_pkg is set_string(identities, full_name(object_id), ""); 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__.create", arg(class_name), args)))); + set(data, class_name_idx, to_integer(new_string_ptr(class_name))); return (p_data => data); end; @@ -116,13 +117,20 @@ package body python_object_pkg is return get_id(get_session(object)); end; + -- Create the object unless it is created. A failure is reported once, as the + -- failure of new_python_object, and the object then raises it on every use. procedure p_create(object : python_object_t) is + variable ok : boolean; begin if not p_supports_sessions then failure(get_logger(object), "Python objects require NVC, GHDL or Questa"); elsif get(object.p_data, created_idx) = 0 then set(object.p_data, created_idx, 1); - exec(to_string(to_string_ptr(get(object.p_data, code_idx))), get_session(object)); + ok := p_exec( + to_string(to_string_ptr(get(object.p_data, code_idx))), 0, + "new_python_object(""" & to_string(to_string_ptr(get(object.p_data, class_name_idx))) & """)", + get_session(object) + ); end if; end; @@ -147,5 +155,11 @@ package body python_object_pkg is return "self." & method; 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; From ce644c06b4000a4e2921e4872d5bcbb277dd6ec3 Mon Sep 17 00:00:00 2001 From: Sebastian Abrahamsson Date: Thu, 8 Oct 2026 19:57:51 +0200 Subject: [PATCH 11/36] Cover create and get_session of objects Co-Authored-By: Claude Opus 5.5 --- tests/tb_python_objects.vhd | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/tb_python_objects.vhd b/tests/tb_python_objects.vhd index 5580cda..08350a9 100644 --- a/tests/tb_python_objects.vhd +++ b/tests/tb_python_objects.vhd @@ -60,6 +60,8 @@ begin elsif run("Test exec and get_logger of an object") then object := new_python_object("models.counter_model.Counter", id => get_id("exec_test")); + create(object); + 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"); From a6072b788e2a57960a40f3c19cf4a3085faceadb Mon Sep 17 00:00:00 2001 From: Sebastian Abrahamsson Date: Thu, 8 Oct 2026 22:14:28 +0200 Subject: [PATCH 12/36] Lay out the instantiations like standard ones 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 --- docs/running_code.rst | 8 +++- examples/embedded_python/tb_example.vhd | 54 +++++++++++++++++++------ tests/tb_python_objects.vhd | 36 +++++++++++++---- 3 files changed, 77 insertions(+), 21 deletions(-) diff --git a/docs/running_code.rst b/docs/running_code.rst index f874eba..da5939e 100644 --- a/docs/running_code.rst +++ b/docs/running_code.rst @@ -200,7 +200,13 @@ monitor and checked by the test: ... - monitor : entity work.my_monitor generic map(monitor => new_my_monitor(model => scoreboard)) ...; + monitor_inst : entity work.my_monitor + generic map ( + monitor => new_my_monitor(model => scoreboard) + ) + port map ( + ... + ); ... diff --git a/examples/embedded_python/tb_example.vhd b/examples/embedded_python/tb_example.vhd index fd11e04..c35b3c5 100644 --- a/examples/embedded_python/tb_example.vhd +++ b/examples/embedded_python/tb_example.vhd @@ -811,29 +811,59 @@ 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(x => filter_x, y => filter_a_y); + generic map ( + filter => new_filter_vc(window => 2, id => get_id("filter_a")) + ) + port map ( + x => filter_x, + y => filter_a_y + ); filter_b_inst : entity work.filter_vc - generic map(filter => new_filter_vc(window => 4)) - port map(x => filter_x, y => filter_b_y); + generic map ( + filter => new_filter_vc(window => 4) + ) + port map ( + 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(x => filter_x, y => filter_c_y); + generic map ( + filter => new_filter_vc(model => shared_average) + ) + port map ( + x => filter_x, + y => filter_c_y + ); end; diff --git a/tests/tb_python_objects.vhd b/tests/tb_python_objects.vhd index 08350a9..d01b788 100644 --- a/tests/tb_python_objects.vhd +++ b/tests/tb_python_objects.vhd @@ -91,20 +91,40 @@ begin 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); + 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); + 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); + 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); + generic map ( + step => 3 + ) + port map ( + tick => tick, + count => count_d + ); end architecture; From ef237728706c7008952fe34bcf5093f393d0d36c Mon Sep 17 00:00:00 2001 From: Sebastian Abrahamsson Date: Thu, 8 Oct 2026 22:44:04 +0200 Subject: [PATCH 13/36] Address the review of Python objects - 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 --- docs/errors.rst | 55 +++++++++ docs/running_code.rst | 113 +++++++----------- examples/embedded_python/filter_vc.vhd | 15 ++- examples/embedded_python/tb_example.vhd | 25 ++-- .../hdl/src/python_ffi_pkg_bridge.vhd | 2 - .../hdl/src/python_object_pkg.vhd | 10 +- .../hdl/src/python_pkg_vhpi.vhd | 2 - .../hdl/tools/python_object_pkg.vhd.in | 10 +- src/vunit_python_bridge/runtime.py | 37 +++--- tests/tb_python_objects.vhd | 2 +- tests/test_python_bridge.py | 33 +++-- 11 files changed, 179 insertions(+), 125 deletions(-) diff --git a/docs/errors.rst b/docs/errors.rst index 8eecf3a..8a35692 100644 --- a/docs/errors.rst +++ b/docs/errors.rst @@ -74,3 +74,58 @@ Semantics to be aware of escaped, so the Python function receives the text exactly as given. Pass plain text, not text already escaped for Python. Bytes >= 128 are kept as they are, so UTF-8 text arrives as the same Python characters. +.. _python_bridge:finding_code: + +Where Python code is found +-------------------------- + +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, which is 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. That includes packages installed with ``pip install -e``. + +Anything else has to be added to ``PYTHONPATH``. The simulator inherits the +environment of VUnit, so the run script can set it before VUnit starts the +simulations. A verification component that is used by many testbenches keeps +its Python code in a folder of its own, for example: + +.. 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() + ... + +Files given by name are found differently. A relative file name given to +``exec_file`` or ``import_module_from_file`` is relative to the directory of +the testbench file, ``tb_path``, and an absolute one is used as given. +``exec_file`` also puts the directory of the file on ``sys.path`` while it +executes, so the file can import the modules next to it. + +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. + diff --git a/docs/running_code.rst b/docs/running_code.rst index da5939e..9a14c72 100644 --- a/docs/running_code.rst +++ b/docs/running_code.rst @@ -39,7 +39,7 @@ 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") + "my_models.uart.UartModel", kwarg("baud", 115200), id => get_id("uart") ); ... @@ -49,21 +49,21 @@ own, and a testbench can create one and give it to a component. check_true(eval_boolean(model, "self.is_idle()")); exec(model, "self.trace = True"); -``new_python_object(class_name, args, id)`` names the class and its constructor -arguments, given like the arguments of ``call``, including groups made with -``&``. The class is +``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 -* ``"package.module.Class"`` or ``"package.module:Class"``, imported by module - name from ``sys.path``: an installed package, the directory of the run - script or a directory on ``PYTHONPATH``. No file name is involved, so the - class is found the same way wherever the object is created. -* ``"Class"``, a class defined in the default session, for example by ``exec`` - or ``exec_file`` in the testbench. +* a dotted name like ``"package.module.Class"``, imported from the module it + names, which is found like any other import: see :ref:`Where Python code is + found `. 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. -The object is created on first use, not by ``new_python_object``. An object can -therefore be a constant or a generic, created during elaboration, and does not -depend on the order of the processes or on ``test_runner_setup``. ``create`` -creates it explicitly. +The constructor is called when the object is first used, not by +``new_python_object``. An object can therefore be a constant or a generic, +made during elaboration, and does not depend on the order of the processes or +on ``test_runner_setup``. ``create`` calls the constructor right away. ``call(object, method, ...)`` calls a method, and ``eval(object, expr)`` and ``exec(object, code)`` evaluate an expression and execute code in the session @@ -72,42 +72,6 @@ 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. -Making a class importable -~~~~~~~~~~~~~~~~~~~~~~~~~ - -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 - -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: - -.. 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() - ... - -An alternative is to make the model an installable Python package and install -it in the virtual environment of the project with ``pip install -e``. - Identity ~~~~~~~~ @@ -122,12 +86,12 @@ identity is reported as a failure on its logger. Backends of verification components ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -``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: +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 @@ -143,7 +107,7 @@ given one: begin return ( p_std_cfg => std_cfg, - p_model => get_python_object(model, "my_vcs.uart:UartModel", kwarg("baud", baud), get_id(std_cfg)) + p_model => get_python_object(model, "my_vcs.uart.UartModel", kwarg("baud", baud), get_id(std_cfg)) ); end; @@ -161,16 +125,18 @@ The component itself only calls the object of its handle: call(master.p_model, "transmit", arg(to_integer(data))); end process; -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 `__ -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. +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 @@ -190,9 +156,9 @@ during elaboration, which would give all instances the same identity. backend := get_python_object(model, "models.counter_model.Counter", kwarg("step", step), vc_id); ... -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: +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 @@ -212,8 +178,8 @@ monitor and checked by the test: check_equal(integer'(call(scoreboard, "num_mismatches")), 0); -The class can be made a choice of the user in the same way, with a ``string`` -generic as its name. +In the same way, a component can let its user choose the class, by taking its +name as a ``string`` generic. Errors and limitations ~~~~~~~~~~~~~~~~~~~~~~ @@ -236,7 +202,8 @@ Errors and limitations * 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 their first use reports that. + Riviera-PRO/Active-HDL, the first use of an object reports that sessions are + only supported there. exec ---- diff --git a/examples/embedded_python/filter_vc.vhd b/examples/embedded_python/filter_vc.vhd index 7e9aa62..2a7bb7b 100644 --- a/examples/embedded_python/filter_vc.vhd +++ b/examples/embedded_python/filter_vc.vhd @@ -5,7 +5,11 @@ -- Copyright (c) 2014-2026, Lars Asplund lars.anders.asplund@gmail.com -- -- A verification component whose behaviour is the Python object of its handle. --- Its object is not created in Python until the first input arrives. +-- 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; @@ -18,6 +22,8 @@ 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 ); @@ -25,9 +31,10 @@ end entity; architecture python of filter_vc is begin - process + process(clk) begin - wait on x; - y <= call(get_model(filter), "push", arg(x)); + 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/tb_example.vhd b/examples/embedded_python/tb_example.vhd index c35b3c5..c0523a8 100644 --- a/examples/embedded_python/tb_example.vhd +++ b/examples/embedded_python/tb_example.vhd @@ -48,6 +48,7 @@ architecture tb of tb_example is 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 @@ -724,9 +725,13 @@ begin -- 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; - wait for clk_period; 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")); @@ -845,8 +850,10 @@ begin filter => new_filter_vc(window => 2, id => get_id("filter_a")) ) port map ( - x => filter_x, - y => filter_a_y + clk => clk, + valid => filter_valid, + x => filter_x, + y => filter_a_y ); filter_b_inst : entity work.filter_vc @@ -854,8 +861,10 @@ begin filter => new_filter_vc(window => 4) ) port map ( - x => filter_x, - y => filter_b_y + clk => clk, + valid => filter_valid, + x => filter_x, + y => filter_b_y ); filter_c_inst : entity work.filter_vc @@ -863,7 +872,9 @@ begin filter => new_filter_vc(model => shared_average) ) port map ( - x => filter_x, - y => filter_c_y + clk => clk, + valid => filter_valid, + x => filter_x, + y => filter_c_y ); end; 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 6ac181a..d7f59aa 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 @@ -43,8 +43,6 @@ package python_ffi_pkg is -- that cannot be converted to Python and the transfer of an integer_array_t. 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; -- A session is a Python namespace. Sessions are isolated from each other -- and have an identity, which is what they are created from: diff --git a/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd b/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd index 200b79e..3259d1c 100644 --- a/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd +++ b/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd @@ -36,8 +36,8 @@ package python_object_pkg is end record; constant null_python_object : python_object_t := (p_data => null_ptr); - -- An instance of class_name ("package.module.Class", "package.module:Class", - -- or "Class" defined in the default session) created with args on first use. + -- 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:. -- Two objects with the same identity are an error. @@ -200,7 +200,7 @@ package body python_object_pkg is end if; set_string(identities, full_name(object_id), ""); 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__.create", arg(class_name), args)))); + set(data, code_idx, to_integer(new_string_ptr(to_call_str("__vunit__.instantiate_as_self", arg(class_name), args)))); set(data, class_name_idx, to_integer(new_string_ptr(class_name))); return (p_data => data); end; @@ -230,9 +230,7 @@ package body python_object_pkg is procedure p_create(object : python_object_t) is variable ok : boolean; begin - if not p_supports_sessions then - failure(get_logger(object), "Python objects require NVC, GHDL or Questa"); - elsif get(object.p_data, created_idx) = 0 then + 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, 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 d69ca5e..c0e9538 100644 --- a/src/vunit_python_bridge/hdl/src/python_pkg_vhpi.vhd +++ b/src/vunit_python_bridge/hdl/src/python_pkg_vhpi.vhd @@ -98,8 +98,6 @@ package python_ffi_pkg is -- Logger of the Python interface, the parent of the session loggers 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 := false; -- Result kinds, must match src/vunit_python_bridge/runtime.py constant p_kind_integer : integer := 0; 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 index 14cd632..f4dc063 100644 --- a/src/vunit_python_bridge/hdl/tools/python_object_pkg.vhd.in +++ b/src/vunit_python_bridge/hdl/tools/python_object_pkg.vhd.in @@ -36,8 +36,8 @@ package python_object_pkg is end record; constant null_python_object : python_object_t := (p_data => null_ptr); - -- An instance of class_name ("package.module.Class", "package.module:Class", - -- or "Class" defined in the default session) created with args on first use. + -- 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:. -- Two objects with the same identity are an error. @@ -92,7 +92,7 @@ package body python_object_pkg is end if; set_string(identities, full_name(object_id), ""); 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__.create", arg(class_name), args)))); + set(data, code_idx, to_integer(new_string_ptr(to_call_str("__vunit__.instantiate_as_self", arg(class_name), args)))); set(data, class_name_idx, to_integer(new_string_ptr(class_name))); return (p_data => data); end; @@ -122,9 +122,7 @@ package body python_object_pkg is procedure p_create(object : python_object_t) is variable ok : boolean; begin - if not p_supports_sessions then - failure(get_logger(object), "Python objects require NVC, GHDL or Questa"); - elsif get(object.p_data, created_idx) = 0 then + 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, diff --git a/src/vunit_python_bridge/runtime.py b/src/vunit_python_bridge/runtime.py index fec2ea4..9a4e380 100644 --- a/src/vunit_python_bridge/runtime.py +++ b/src/vunit_python_bridge/runtime.py @@ -112,16 +112,26 @@ def _type_error(kind, value, expected): def _resolve_class(class_name): """ - The class named "package.module.Class", "package.module:Class" or "Class" - defined in the default session (__main__). + The class with a dotted name like "package.module.Class", imported from + the longest prefix that is a module, or a class defined in the default + session (__main__) when the name has no dot. """ - module_name, separator, attributes = class_name.partition(":") - if not separator: - module_name, _, attributes = class_name.rpartition(".") - target = importlib.import_module(module_name) if module_name else __main__ - for attribute in attributes.split("."): - target = getattr(target, attribute) - return target + parts = class_name.split(".") + if len(parts) == 1: + return getattr(__main__, class_name) + for split in range(len(parts) - 1, 0, -1): + module_name = ".".join(parts[:split]) + try: + target = importlib.import_module(module_name) + except ModuleNotFoundError as exc: + # Only a missing prefix means trying a shorter one, not an import failing inside it + if exc.name is None or not module_name.startswith(exc.name): + raise + continue + for attribute in parts[split:]: + target = getattr(target, attribute) + return target + raise ModuleNotFoundError(f"No module named {parts[0]!r}", name=parts[0]) class _UncreatedObject: # pylint: disable=too-few-public-methods @@ -167,12 +177,11 @@ def error(message): """ raise RuntimeError(message) - def create(self, class_name, *args, **kwargs): + def instantiate_as_self(self, class_name, *args, **kwargs): """ - Create the object of a VHDL python_object_t and bind it to self in its - session: an instance of "package.module.Class", "package.module:Class" - or "Class" defined in the default session. If that fails, self raises - the reason on every use instead of being undefined. + 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 if "self" in namespace: diff --git a/tests/tb_python_objects.vhd b/tests/tb_python_objects.vhd index d01b788..f3f316e 100644 --- a/tests/tb_python_objects.vhd +++ b/tests/tb_python_objects.vhd @@ -22,7 +22,7 @@ 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)); + 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; diff --git a/tests/test_python_bridge.py b/tests/test_python_bridge.py index 323c78a..b258073 100644 --- a/tests/test_python_bridge.py +++ b/tests/test_python_bridge.py @@ -18,6 +18,7 @@ import subprocess import sys import tempfile +import types import unittest from contextlib import contextmanager from glob import glob @@ -171,7 +172,7 @@ def test_the_simulator_dependent_vhdl_is_not_listed(self): class TestObjectCreation(unittest.TestCase): """ - __vunit__.create, which creates the object of a python_object_t in its session. + __vunit__.instantiate_as_self, which creates the object of a python_object_t in its session. """ def setUp(self): @@ -180,28 +181,40 @@ def setUp(self): self.namespace = {} self.handle = runtime.BridgeHandle(mock.Mock(namespace=self.namespace)) - def test_binds_an_instance_of_a_module_class_to_self(self): - for class_name in ("collections.OrderedDict", "collections:OrderedDict"): - self.namespace.clear() - self.handle.create(class_name, a=1) - self.assertEqual(self.namespace["self"], {"a": 1}, class_name) + 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.create("Model") + self.handle.instantiate_as_self("Model") self.assertIsInstance(self.namespace["self"], Model) + 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_such_dependency"): + self.handle.instantiate_as_self("broken_model.Model") + def test_a_second_object_in_a_session_is_an_error(self): - self.handle.create("collections.OrderedDict") + self.handle.instantiate_as_self("collections.OrderedDict") with self.assertRaisesRegex(RuntimeError, "same identity"): - self.handle.create("collections.OrderedDict") + self.handle.instantiate_as_self("collections.OrderedDict") def test_an_object_that_cannot_be_created_tells_why_when_used(self): with self.assertRaises(ModuleNotFoundError): - self.handle.create("no_such_module.Model", 1) + self.handle.instantiate_as_self("no_such_module.Model", 1) with self.assertRaisesRegex( RuntimeError, "The no_such_module.Model object could not be created: ModuleNotFoundError: No module named 'no_such_module'", From 4b2fb27e781798a72ea9e91a5d4c7319fd7023f1 Mon Sep 17 00:00:00 2001 From: Sebastian Abrahamsson Date: Thu, 8 Oct 2026 22:48:37 +0200 Subject: [PATCH 14/36] Say plainly when Python code is not found without setup 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 --- docs/errors.rst | 42 ++++++++++++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/docs/errors.rst b/docs/errors.rst index 8a35692..f612a1f 100644 --- a/docs/errors.rst +++ b/docs/errors.rst @@ -80,17 +80,22 @@ Where Python code is found -------------------------- The interpreter in the simulator imports modules the way Python started on the -run script does. Without any setup, it finds: +run script does. + +Found without setup +~~~~~~~~~~~~~~~~~~~ * Modules in the directory of the run script, which is 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. That includes packages installed with ``pip install -e``. -Anything else has to be added to ``PYTHONPATH``. The simulator inherits the -environment of VUnit, so the run script can set it before VUnit starts the -simulations. A verification component that is used by many testbenches keeps -its Python code in a folder of its own, for example: +Adding a folder +~~~~~~~~~~~~~~~ + +Python code anywhere else is not found until its folder is on ``PYTHONPATH``. A +typical case is a verification component that is used by many testbenches and +keeps its Python model in a folder of its own: .. code-block:: text @@ -100,8 +105,10 @@ its Python code in a folder of its own, for example: 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: +``my_vc/python`` is neither the directory of the run script nor part of the +Python environment, so ``my_vc_model`` cannot be imported as it is. The run +script adds the folder to ``PYTHONPATH`` before VUnit starts the simulations, +which inherit the environment of VUnit: .. code-block:: python @@ -117,11 +124,22 @@ and the run script puts that folder on the path, after which the model is vu = VUnit.from_argv() ... -Files given by name are found differently. A relative file name given to -``exec_file`` or ``import_module_from_file`` is relative to the directory of -the testbench file, ``tb_path``, and an absolute one is used as given. -``exec_file`` also puts the directory of the file on ``sys.path`` while it -executes, so the file can import the modules next to it. +After that, ``my_vc_model`` is importable in every simulation, wherever the +component is used, for example as the class ``"my_vc_model.Model"`` of a +:ref:`Python object `. Installing the model as a Python +package, for example with ``pip install -e``, has the same effect without +changing the run script. + +Files given by name +~~~~~~~~~~~~~~~~~~~ + +A relative file name given to ``exec_file`` or ``import_module_from_file`` is +relative to the directory of the testbench file, ``tb_path``, and an absolute +one is used as given. ``exec_file`` also puts the directory of the file on +``sys.path`` while it executes, so the file can import the modules next to it. + +Module names +~~~~~~~~~~~~ There is one ``sys.path`` and one ``sys.modules`` per simulation, shared by all :ref:`sessions `. Two modules with the same name are From 4759d620c6a5bcfc2cdddd4795a1281d8662ad44 Mon Sep 17 00:00:00 2001 From: Sebastian Abrahamsson Date: Thu, 8 Oct 2026 23:49:58 +0200 Subject: [PATCH 15/36] Simplify python_object_pkg and the runtime - 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 --- .../hdl/src/python_ffi_pkg_bridge.vhd | 1 - .../hdl/src/python_object_pkg.vhd | 106 ++++++++---------- .../hdl/src/python_pkg_vhpi.vhd | 1 - .../hdl/tools/generate_python_pkg.py | 16 +-- .../hdl/tools/python_object_pkg.vhd.in | 24 ++-- src/vunit_python_bridge/runtime.py | 45 ++------ tests/test_python_bridge.py | 12 +- 7 files changed, 78 insertions(+), 127 deletions(-) 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 d7f59aa..ab9260d 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 @@ -43,7 +43,6 @@ package python_ffi_pkg is -- that cannot be converted to Python and the transfer of an integer_array_t. constant python_logger : logger_t := get_logger(p_python_id); - -- A session is a Python namespace. Sessions are isolated from each other -- and have an identity, which is what they are created from: -- diff --git a/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd b/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd index 3259d1c..20d6bd5 100644 --- a/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd +++ b/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd @@ -177,7 +177,7 @@ package body python_object_pkg is constant session_idx : natural := 0; constant code_idx : natural := 1; constant created_idx : natural := 2; - constant class_name_idx : natural := 3; + 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 identities of the objects, by full name @@ -201,7 +201,7 @@ package body python_object_pkg is set_string(identities, full_name(object_id), ""); 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, class_name_idx, to_integer(new_string_ptr(class_name))); + set(data, operation_idx, to_integer(new_string_ptr("new_python_object(""" & class_name & """)"))); return (p_data => data); end; @@ -225,29 +225,24 @@ package body python_object_pkg is return get_id(get_session(object)); end; - -- Create the object unless it is created. A failure is reported once, as the - -- failure of new_python_object, and the object then raises it on every use. - procedure p_create(object : python_object_t) is + -- A failure is reported once, as the failure of new_python_object, and the + -- object then raises it on every use + procedure create(object : python_object_t) is variable ok : boolean; begin 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, - "new_python_object(""" & to_string(to_string_ptr(get(object.p_data, class_name_idx))) & """)", + to_string(to_string_ptr(get(object.p_data, operation_idx))), get_session(object) ); end if; end; - procedure create(object : python_object_t) is - begin - p_create(object); - end; - procedure exec(object : python_object_t; code : string) is begin - p_create(object); + create(object); exec(code, get_session(object)); end; @@ -256,11 +251,6 @@ package body python_object_pkg is return get_logger(get_id(object)); end; - function p_self(method : string) return string is - begin - return "self." & method; - end; - -- The name of a method call in error messages function p_call_operation(method : string) return string is begin @@ -273,10 +263,10 @@ package body python_object_pkg is ) is variable ok : boolean; begin - p_create(object); + create(object); -- An error is reported as that of the method rather than of the code executed ok := p_exec( - to_call_str(p_self(method), arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10), 0, p_call_operation(method), + to_call_str("self." & method, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10), 0, p_call_operation(method), get_session(object) ); end; @@ -286,9 +276,9 @@ package body python_object_pkg is arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg ) return integer is begin - p_create(object); + create(object); return call_integer_w_arg( - p_self(method), arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object) + "self." & method, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object) ); end; @@ -297,9 +287,9 @@ package body python_object_pkg is arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg ) return real is begin - p_create(object); + create(object); return call_real( - p_self(method), arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object) + "self." & method, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object) ); end; @@ -308,9 +298,9 @@ package body python_object_pkg is arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg ) return integer_vector is begin - p_create(object); + create(object); return call_integer_vector( - p_self(method), arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object) + "self." & method, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object) ); end; @@ -319,9 +309,9 @@ package body python_object_pkg is arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg ) return real_vector is begin - p_create(object); + create(object); return call_real_vector( - p_self(method), arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object) + "self." & method, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object) ); end; @@ -330,9 +320,9 @@ package body python_object_pkg is arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg ) return string is begin - p_create(object); + create(object); return call_string( - p_self(method), arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object) + "self." & method, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object) ); end; @@ -341,9 +331,9 @@ package body python_object_pkg is arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg ) return integer_vector_ptr_t is begin - p_create(object); + create(object); return call_integer_vector_ptr( - p_self(method), arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object) + "self." & method, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object) ); end; @@ -352,9 +342,9 @@ package body python_object_pkg is arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg ) return boolean is begin - p_create(object); + create(object); return call_boolean( - p_self(method), arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object) + "self." & method, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object) ); end; @@ -363,9 +353,9 @@ package body python_object_pkg is arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg ) return std_ulogic is begin - p_create(object); + create(object); return call_std_ulogic( - p_self(method), arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object) + "self." & method, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object) ); end; @@ -374,9 +364,9 @@ package body python_object_pkg is arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg ) return std_ulogic_vector is begin - p_create(object); + create(object); return call_std_ulogic_vector( - p_self(method), arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object) + "self." & method, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object) ); end; @@ -385,9 +375,9 @@ package body python_object_pkg is arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg ) return integer_array_t is begin - p_create(object); + create(object); return call_integer_array( - p_self(method), arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object) + "self." & method, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object) ); end; @@ -396,9 +386,9 @@ package body python_object_pkg is arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg ) is begin - p_create(object); + create(object); call_std_ulogic_vector( - p_self(method), result, + "self." & method, result, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object) ); end; @@ -408,9 +398,9 @@ package body python_object_pkg is arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg ) is begin - p_create(object); + create(object); call_signed( - p_self(method), result, + "self." & method, result, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object) ); end; @@ -420,88 +410,88 @@ package body python_object_pkg is arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg ) is begin - p_create(object); + create(object); call_unsigned( - p_self(method), result, + "self." & method, result, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object) ); end; impure function eval_integer(object : python_object_t; expr : string) return integer is begin - p_create(object); + create(object); return eval_integer(expr, session => get_session(object)); end; impure function eval_real(object : python_object_t; expr : string) return real is begin - p_create(object); + create(object); 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 - p_create(object); + create(object); 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 - p_create(object); + create(object); return eval_real_vector(expr, session => get_session(object)); end; impure function eval_string(object : python_object_t; expr : string) return string is begin - p_create(object); + create(object); 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 - p_create(object); + create(object); 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 - p_create(object); + create(object); 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 - p_create(object); + create(object); 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 - p_create(object); + create(object); 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 - p_create(object); + create(object); 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 - p_create(object); + create(object); 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 - p_create(object); + create(object); eval_signed(expr, result, session => get_session(object)); end; procedure eval_unsigned(object : python_object_t; expr : string; result : out unsigned) is begin - p_create(object); + create(object); eval_unsigned(expr, result, session => get_session(object)); end; end package body; 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 c0e9538..0986c65 100644 --- a/src/vunit_python_bridge/hdl/src/python_pkg_vhpi.vhd +++ b/src/vunit_python_bridge/hdl/src/python_pkg_vhpi.vhd @@ -98,7 +98,6 @@ package python_ffi_pkg is -- Logger of the Python interface, the parent of the session loggers constant python_logger : logger_t := get_logger(p_python_id); - -- Result kinds, must match src/vunit_python_bridge/runtime.py constant p_kind_integer : integer := 0; constant p_kind_real : integer := 1; 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 e365925..832071e 100644 --- a/src/vunit_python_bridge/hdl/tools/generate_python_pkg.py +++ b/src/vunit_python_bridge/hdl/tools/generate_python_pkg.py @@ -422,10 +422,10 @@ def object_subprograms(): ) is variable ok : boolean; begin - p_create(object); + create(object); -- An error is reported as that of the method rather than of the code executed ok := p_exec( - to_call_str(p_self(method), {ARG_ACTUALS}), 0, p_call_operation(method), + to_call_str("self." & method, {ARG_ACTUALS}), 0, p_call_operation(method), get_session(object) ); end; @@ -440,9 +440,9 @@ def object_subprograms(): {ARG_PARAMETERS} ) return {result['vhdl']} is begin - p_create(object); + create(object); return {name}( - p_self(method), {ARG_ACTUALS}, {session} + "self." & method, {ARG_ACTUALS}, {session} ); end; """ @@ -455,9 +455,9 @@ def object_subprograms(): {ARG_PARAMETERS} ) is begin - p_create(object); + create(object); call_{result['name']}( - p_self(method), result, + "self." & method, result, {ARG_ACTUALS}, {session} ); end; @@ -468,7 +468,7 @@ def object_subprograms(): f"""\ impure function eval_{result['name']}({OBJECT}; expr : string) return {result['vhdl']} is begin - p_create(object); + create(object); return eval_{result['name']}(expr, {session}); end; """ @@ -478,7 +478,7 @@ def object_subprograms(): f"""\ procedure eval_{result['name']}({OBJECT}; expr : string; result : out {result['vhdl']}) is begin - p_create(object); + create(object); eval_{result['name']}(expr, result, {session}); end; """ 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 index f4dc063..87e5af8 100644 --- a/src/vunit_python_bridge/hdl/tools/python_object_pkg.vhd.in +++ b/src/vunit_python_bridge/hdl/tools/python_object_pkg.vhd.in @@ -69,7 +69,7 @@ package body python_object_pkg is constant session_idx : natural := 0; constant code_idx : natural := 1; constant created_idx : natural := 2; - constant class_name_idx : natural := 3; + 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 identities of the objects, by full name @@ -93,7 +93,7 @@ package body python_object_pkg is set_string(identities, full_name(object_id), ""); 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, class_name_idx, to_integer(new_string_ptr(class_name))); + set(data, operation_idx, to_integer(new_string_ptr("new_python_object(""" & class_name & """)"))); return (p_data => data); end; @@ -117,29 +117,24 @@ package body python_object_pkg is return get_id(get_session(object)); end; - -- Create the object unless it is created. A failure is reported once, as the - -- failure of new_python_object, and the object then raises it on every use. - procedure p_create(object : python_object_t) is + -- A failure is reported once, as the failure of new_python_object, and the + -- object then raises it on every use + procedure create(object : python_object_t) is variable ok : boolean; begin 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, - "new_python_object(""" & to_string(to_string_ptr(get(object.p_data, class_name_idx))) & """)", + to_string(to_string_ptr(get(object.p_data, operation_idx))), get_session(object) ); end if; end; - procedure create(object : python_object_t) is - begin - p_create(object); - end; - procedure exec(object : python_object_t; code : string) is begin - p_create(object); + create(object); exec(code, get_session(object)); end; @@ -148,11 +143,6 @@ package body python_object_pkg is return get_logger(get_id(object)); end; - function p_self(method : string) return string is - begin - return "self." & method; - end; - -- The name of a method call in error messages function p_call_operation(method : string) return string is begin diff --git a/src/vunit_python_bridge/runtime.py b/src/vunit_python_bridge/runtime.py index 9a4e380..4bda2a4 100644 --- a/src/vunit_python_bridge/runtime.py +++ b/src/vunit_python_bridge/runtime.py @@ -15,11 +15,11 @@ """ import builtins -import importlib import linecache import math import numbers import os +import pydoc import struct import sys import traceback @@ -112,26 +112,15 @@ def _type_error(kind, value, expected): def _resolve_class(class_name): """ - The class with a dotted name like "package.module.Class", imported from - the longest prefix that is a module, or a class defined in the default - session (__main__) when the name has no dot. + The class with a dotted name like "package.module.Class", or a class + defined in the default session (__main__) when the name has no dot. """ - parts = class_name.split(".") - if len(parts) == 1: + if "." not in class_name: return getattr(__main__, class_name) - for split in range(len(parts) - 1, 0, -1): - module_name = ".".join(parts[:split]) - try: - target = importlib.import_module(module_name) - except ModuleNotFoundError as exc: - # Only a missing prefix means trying a shorter one, not an import failing inside it - if exc.name is None or not module_name.startswith(exc.name): - raise - continue - for attribute in parts[split:]: - target = getattr(target, attribute) - return target - raise ModuleNotFoundError(f"No module named {parts[0]!r}", name=parts[0]) + target = pydoc.locate(class_name) + if target is None: + raise ImportError(f"No class named {class_name!r}") + return target class _UncreatedObject: # pylint: disable=too-few-public-methods @@ -146,9 +135,6 @@ def __init__(self, class_name, exc): def __getattr__(self, name): raise RuntimeError(self._message) - def __repr__(self): - return f"<{self._message}>" - class BridgeHandle: """ @@ -184,8 +170,6 @@ def instantiate_as_self(self, class_name, *args, **kwargs): fails, self raises the reason on every use instead of being undefined. """ namespace = self._runtime.namespace - if "self" in namespace: - raise RuntimeError("Two Python objects have the same identity, give each one an id of its own") try: namespace["self"] = _resolve_class(class_name)(*args, **kwargs) except Exception as exc: @@ -308,13 +292,6 @@ def select_session(self, name): @property def namespace(self): - """ - The namespace of the current session. - """ - return self._namespace - - @property - def _namespace(self): """ Namespace of the current session. """ @@ -353,7 +330,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): @@ -374,7 +351,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) @@ -413,7 +390,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/tests/test_python_bridge.py b/tests/test_python_bridge.py index b258073..15f3177 100644 --- a/tests/test_python_bridge.py +++ b/tests/test_python_bridge.py @@ -13,6 +13,7 @@ import importlib.util import os +import pydoc import re import shutil import subprocess @@ -204,20 +205,15 @@ 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_such_dependency"): + with self.assertRaisesRegex(pydoc.ErrorDuringImport, "No module named 'no_such_dependency'"): self.handle.instantiate_as_self("broken_model.Model") - def test_a_second_object_in_a_session_is_an_error(self): - self.handle.instantiate_as_self("collections.OrderedDict") - with self.assertRaisesRegex(RuntimeError, "same identity"): - self.handle.instantiate_as_self("collections.OrderedDict") - def test_an_object_that_cannot_be_created_tells_why_when_used(self): - with self.assertRaises(ModuleNotFoundError): + 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: ModuleNotFoundError: No module named 'no_such_module'", + "The no_such_module.Model object could not be created: ImportError: No class named 'no_such_module.Model'", ): self.namespace["self"].step() From d7065feccca7963ac79aa2fc12ace1a55ef4c088 Mon Sep 17 00:00:00 2001 From: Sebastian Abrahamsson Date: Tue, 13 Oct 2026 20:30:43 +0200 Subject: [PATCH 16/36] Find classes of objects more clearly and report a failed object on every 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 --- src/vunit_python_bridge/runtime.py | 69 ++++++++++++++++++++++++------ tests/test_python_bridge.py | 21 ++++++++- 2 files changed, 76 insertions(+), 14 deletions(-) diff --git a/src/vunit_python_bridge/runtime.py b/src/vunit_python_bridge/runtime.py index 4bda2a4..874b58d 100644 --- a/src/vunit_python_bridge/runtime.py +++ b/src/vunit_python_bridge/runtime.py @@ -15,11 +15,11 @@ """ import builtins +import importlib import linecache import math import numbers import os -import pydoc import struct import sys import traceback @@ -112,17 +112,46 @@ def _type_error(kind, value, expected): def _resolve_class(class_name): """ - The class with a dotted name like "package.module.Class", or a class - defined in the default session (__main__) when the name has no dot. + 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. """ - if "." not in class_name: - return getattr(__main__, class_name) - target = pydoc.locate(class_name) - if target is None: - raise ImportError(f"No class named {class_name!r}") + 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): + raise ImportError(f"No class named {class_name!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 @@ -130,10 +159,17 @@ class _UncreatedObject: # pylint: disable=too-few-public-methods """ def __init__(self, class_name, exc): - self._message = f"The {class_name} object could not be created: {type(exc).__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 RuntimeError(self._message) + 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: @@ -171,7 +207,12 @@ def instantiate_as_self(self, class_name, *args, **kwargs): """ namespace = self._runtime.namespace try: - namespace["self"] = _resolve_class(class_name)(*args, **kwargs) + 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 @@ -245,8 +286,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 ( diff --git a/tests/test_python_bridge.py b/tests/test_python_bridge.py index 15f3177..10ac93e 100644 --- a/tests/test_python_bridge.py +++ b/tests/test_python_bridge.py @@ -13,7 +13,6 @@ import importlib.util import os -import pydoc import re import shutil import subprocess @@ -201,11 +200,27 @@ class Model: # pylint: disable=too-few-public-methods 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_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(pydoc.ErrorDuringImport, "No module named 'no_such_dependency'"): + 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): @@ -216,6 +231,8 @@ def test_an_object_that_cannot_be_created_tells_why_when_used(self): "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): From aa79b92e1dea588c4d241a54d99ba4760e0ed74d Mon Sep 17 00:00:00 2001 From: Sebastian Abrahamsson Date: Tue, 13 Oct 2026 20:30:43 +0200 Subject: [PATCH 17/36] Fail early on objects that cannot work and name the call in its errors - 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(""), 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 --- .../hdl/src/python_ffi_pkg_bridge.vhd | 22 ++ .../hdl/src/python_object_pkg.vhd | 349 +++++++++++++----- .../hdl/src/python_pkg.vhd | 4 + .../hdl/src/python_pkg_vhpi.vhd | 45 ++- .../hdl/tools/generate_python_pkg.py | 80 ++-- .../hdl/tools/python_object_pkg.vhd.in | 80 +++- .../hdl/tools/python_pkg.vhd.in | 4 + tests/models/counter_model.py | 3 + tests/run.py | 2 + tests/tb_python_objects.vhd | 115 +++++- 10 files changed, 568 insertions(+), 136 deletions(-) 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 ab9260d..d50e648 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; @@ -111,6 +112,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; @@ -171,6 +178,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; @@ -191,11 +199,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)); @@ -337,6 +354,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 index 20d6bd5..52e7f49 100644 --- a/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd +++ b/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd @@ -40,7 +40,9 @@ package python_object_pkg is -- 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:. - -- Two objects with the same identity are an error. + -- 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; @@ -57,7 +59,7 @@ package python_object_pkg is impure function get_session(object : python_object_t) return python_session_t; -- Create the object now rather than on first use - procedure create(object : python_object_t); + procedure create_now(object : python_object_t); -- Execute code in the session of the object, where it is self, like exec procedure exec(object : python_object_t; code : string); @@ -73,56 +75,64 @@ package python_object_pkg is 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]; + 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]; + 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]; + 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]; + 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]; + 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]; + 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]; + 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]; + 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; @@ -134,7 +144,8 @@ package python_object_pkg is 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]; + 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; @@ -180,28 +191,50 @@ package body python_object_pkg is 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 identities of the objects, by full name - constant identities : dict_t := new_dict; + + -- 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; impure function new_python_object(class_name : string; args : arg_t := null_arg; id : id_t := null_id) return python_object_t is + constant operation : string := "new_python_object(""" & class_name & """)"; variable object_id : id_t := id; - variable data : integer_vector_ptr_t := new_integer_vector_ptr(4); + 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 has_key(identities, full_name(object_id)) then + 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), - "Two Python objects have the identity " & full_name(object_id) & ", give each one an id of its own" + 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; - set_string(identities, full_name(object_id), ""); + 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("new_python_object(""" & class_name & """)"))); + 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; @@ -215,8 +248,24 @@ package body python_object_pkg is 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; @@ -227,9 +276,12 @@ package body python_object_pkg is -- A failure is reported once, as the failure of new_python_object, and the -- object then raises it on every use - procedure create(object : python_object_t) is + procedure create_now(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( @@ -240,10 +292,21 @@ package body python_object_pkg is end if; 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; + create_now(object); + return true; + end; + procedure exec(object : python_object_t; code : string) is begin - create(object); - exec(code, get_session(object)); + 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 @@ -263,11 +326,13 @@ package body python_object_pkg is ) is variable ok : boolean; begin - create(object); + 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) + to_call_str("self." & method, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10), + 0, p_call_operation(method), get_session(object) ); end; @@ -276,10 +341,16 @@ package body python_object_pkg is arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg ) return integer is begin - create(object); - return call_integer_w_arg( - "self." & method, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object) - ); + 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( @@ -287,10 +358,16 @@ package body python_object_pkg is arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg ) return real is begin - create(object); - return call_real( - "self." & method, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object) - ); + 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( @@ -298,10 +375,16 @@ package body python_object_pkg is arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg ) return integer_vector is begin - create(object); - return call_integer_vector( - "self." & method, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object) - ); + 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( @@ -309,10 +392,16 @@ package body python_object_pkg is arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg ) return real_vector is begin - create(object); - return call_real_vector( - "self." & method, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object) - ); + 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( @@ -320,10 +409,16 @@ package body python_object_pkg is arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg ) return string is begin - create(object); - return call_string( - "self." & method, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object) - ); + 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( @@ -331,10 +426,16 @@ package body python_object_pkg is arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg ) return integer_vector_ptr_t is begin - create(object); - return call_integer_vector_ptr( - "self." & method, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object) - ); + 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( @@ -342,10 +443,16 @@ package body python_object_pkg is arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg ) return boolean is begin - create(object); - return call_boolean( - "self." & method, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object) - ); + 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( @@ -353,10 +460,16 @@ package body python_object_pkg is arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg ) return std_ulogic is begin - create(object); - return call_std_ulogic( - "self." & method, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object) - ); + 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( @@ -364,10 +477,16 @@ package body python_object_pkg is arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg ) return std_ulogic_vector is begin - create(object); - return call_std_ulogic_vector( - "self." & method, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object) - ); + 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( @@ -375,10 +494,16 @@ package body python_object_pkg is arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg ) return integer_array_t is begin - create(object); - return call_integer_array( - "self." & method, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object) - ); + 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( @@ -386,11 +511,15 @@ package body python_object_pkg is arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg ) is begin - create(object); - call_std_ulogic_vector( - "self." & method, result, - arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object) - ); + 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( @@ -398,11 +527,15 @@ package body python_object_pkg is arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg ) is begin - create(object); - call_signed( - "self." & method, result, - arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object) - ); + 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( @@ -410,88 +543,118 @@ package body python_object_pkg is arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg ) is begin - create(object); - call_unsigned( - "self." & method, result, - arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session => get_session(object) - ); + 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 - create(object); + 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 - create(object); + 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 - create(object); + 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 - create(object); + 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 - create(object); + 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 - create(object); + 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 - create(object); + 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 - create(object); + 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 - create(object); + 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 - create(object); + 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 - create(object); + 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 - create(object); + 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 - create(object); + 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 449e4b1..aba7e58 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 0986c65..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; @@ -90,6 +91,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 := 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, @@ -135,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 @@ -149,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)); @@ -180,9 +199,9 @@ package body python_ffi_pkg is text : string; is_file : integer; operation : string; session : python_session_t := default_session ) return boolean is begin - p_check_session(session); - p_exec(text); - return true; + -- 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 @@ -277,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 832071e..9def0b9 100644 --- a/src/vunit_python_bridge/hdl/tools/generate_python_pkg.py +++ b/src/vunit_python_bridge/hdl/tools/generate_python_pkg.py @@ -7,13 +7,14 @@ """ Generate python_pkg.vhd and python_object_pkg.vhd from the templates 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. +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, ), @@ -390,8 +403,11 @@ def object_declarations(): 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" {OBJECT_ARG_SIGNATURE} return {result['vhdl']}];") + 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']}(") @@ -411,7 +427,9 @@ def object_declarations(): def object_subprograms(): """ Bodies of the call and eval overloads of an object. Each creates the object - if needed and performs the python_pkg operation in the session of 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 = [ @@ -422,11 +440,13 @@ def object_subprograms(): ) is variable ok : boolean; begin - create(object); + 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) + to_call_str("self." & method, {ARG_ACTUALS}), + 0, p_call_operation(method), get_session(object) ); end; """ @@ -440,10 +460,16 @@ def object_subprograms(): {ARG_PARAMETERS} ) return {result['vhdl']} is begin - create(object); - return {name}( - "self." & method, {ARG_ACTUALS}, {session} - ); + 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; """ ) @@ -455,11 +481,15 @@ def object_subprograms(): {ARG_PARAMETERS} ) is begin - create(object); - call_{result['name']}( - "self." & method, result, - {ARG_ACTUALS}, {session} - ); + 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; """ ) @@ -468,7 +498,9 @@ def object_subprograms(): f"""\ impure function eval_{result['name']}({OBJECT}; expr : string) return {result['vhdl']} is begin - create(object); + if not p_created(object) then + return {result['default']}; + end if; return eval_{result['name']}(expr, {session}); end; """ @@ -478,7 +510,9 @@ def object_subprograms(): f"""\ procedure eval_{result['name']}({OBJECT}; expr : string; result : out {result['vhdl']}) is begin - create(object); + if not p_created(object) then + return; + end if; eval_{result['name']}(expr, result, {session}); end; """ 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 index 87e5af8..abb9e5c 100644 --- a/src/vunit_python_bridge/hdl/tools/python_object_pkg.vhd.in +++ b/src/vunit_python_bridge/hdl/tools/python_object_pkg.vhd.in @@ -40,7 +40,9 @@ package python_object_pkg is -- 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:. - -- Two objects with the same identity are an error. + -- 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; @@ -57,7 +59,7 @@ package python_object_pkg is impure function get_session(object : python_object_t) return python_session_t; -- Create the object now rather than on first use - procedure create(object : python_object_t); + procedure create_now(object : python_object_t); -- Execute code in the session of the object, where it is self, like exec procedure exec(object : python_object_t; code : string); @@ -72,28 +74,50 @@ package body python_object_pkg is 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 identities of the objects, by full name - constant identities : dict_t := new_dict; + + -- 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; impure function new_python_object(class_name : string; args : arg_t := null_arg; id : id_t := null_id) return python_object_t is + constant operation : string := "new_python_object(""" & class_name & """)"; variable object_id : id_t := id; - variable data : integer_vector_ptr_t := new_integer_vector_ptr(4); + 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 has_key(identities, full_name(object_id)) then + 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), - "Two Python objects have the identity " & full_name(object_id) & ", give each one an id of its own" + 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; - set_string(identities, full_name(object_id), ""); + 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("new_python_object(""" & class_name & """)"))); + 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; @@ -107,8 +131,24 @@ package body python_object_pkg is 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; @@ -119,9 +159,12 @@ package body python_object_pkg is -- A failure is reported once, as the failure of new_python_object, and the -- object then raises it on every use - procedure create(object : python_object_t) is + procedure create_now(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( @@ -132,10 +175,21 @@ package body python_object_pkg is end if; 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; + create_now(object); + return true; + end; + procedure exec(object : python_object_t; code : string) is begin - create(object); - exec(code, get_session(object)); + 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 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 27372e6..3b97fec 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/tests/models/counter_model.py b/tests/models/counter_model.py index 684be51..56d0123 100644 --- a/tests/models/counter_model.py +++ b/tests/models/counter_model.py @@ -27,3 +27,6 @@ def history(self): def ratio(self): return self.count / 4 + + def name(self): + return "counter" diff --git a/tests/run.py b/tests/run.py index b752e36..c9942fd 100644 --- a/tests/run.py +++ b/tests/run.py @@ -112,6 +112,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() diff --git a/tests/tb_python_objects.vhd b/tests/tb_python_objects.vhd index f3f316e..14158e8 100644 --- a/tests/tb_python_objects.vhd +++ b/tests/tb_python_objects.vhd @@ -6,10 +6,12 @@ -- 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; @@ -29,8 +31,14 @@ architecture tb of tb_python_objects is begin main : process constant duplicate_id : id_t := get_id("duplicate"); - variable object : python_object_t; + constant session_id : id_t := get_id("session_first"); + constant broken_id : id_t := get_id("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); @@ -57,10 +65,15 @@ begin 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 := new_python_object("models.counter_model.Counter", id => get_id("exec_test")); - create(object); + create_now(object); 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); @@ -73,17 +86,111 @@ begin -- 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 two objects with the same identity fail") then + 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), - "Two Python objects have the identity duplicate, give each one an id of its own", + "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 + object := new_python_object("models.no_such_module.Model", id => broken_id); + mock(get_logger(broken_id), failure); + create_now(object); + check_only_log( + get_logger(broken_id), + "new_python_object(""models.no_such_module.Model"") failed:" & LF & + "ImportError: No class named 'models.no_such_module.Model'", + failure + ); + -- Reported once, then every use fails with the reason + create_now(object); + check_no_log; + 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'", + failure + ); + unmock(get_logger(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; From 685cba91457517f905c8d1bbd06aea177dd7f00c Mon Sep 17 00:00:00 2001 From: Sebastian Abrahamsson Date: Tue, 13 Oct 2026 20:30:43 +0200 Subject: [PATCH 18/36] Merge where Python code is found into File paths and update the objects 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 --- docs/errors.rst | 73 ------------------------------------------- docs/file_paths.rst | 37 ++++++++++++++++++---- docs/running_code.rst | 38 +++++++++++++--------- 3 files changed, 54 insertions(+), 94 deletions(-) diff --git a/docs/errors.rst b/docs/errors.rst index f612a1f..8eecf3a 100644 --- a/docs/errors.rst +++ b/docs/errors.rst @@ -74,76 +74,3 @@ Semantics to be aware of escaped, so the Python function receives the text exactly as given. Pass plain text, not text already escaped for Python. Bytes >= 128 are kept as they are, so UTF-8 text arrives as the same Python characters. -.. _python_bridge:finding_code: - -Where Python code is found --------------------------- - -The interpreter in the simulator imports modules the way Python started on the -run script does. - -Found without setup -~~~~~~~~~~~~~~~~~~~ - -* Modules in the directory of the run script, which is 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. That includes packages installed with ``pip install -e``. - -Adding a folder -~~~~~~~~~~~~~~~ - -Python code anywhere else is not found until its folder is on ``PYTHONPATH``. A -typical case is a verification component that is used by many testbenches and -keeps its Python model in a folder of its own: - -.. code-block:: text - - my_vc/ - my_vc.vhd - python/ - my_vc_model/ - __init__.py - -``my_vc/python`` is neither the directory of the run script nor part of the -Python environment, so ``my_vc_model`` cannot be imported as it is. The run -script adds the folder to ``PYTHONPATH`` before VUnit starts the simulations, -which inherit the environment of VUnit: - -.. 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() - ... - -After that, ``my_vc_model`` is importable in every simulation, wherever the -component is used, for example as the class ``"my_vc_model.Model"`` of a -:ref:`Python object `. Installing the model as a Python -package, for example with ``pip install -e``, has the same effect without -changing the run script. - -Files given by name -~~~~~~~~~~~~~~~~~~~ - -A relative file name given to ``exec_file`` or ``import_module_from_file`` is -relative to the directory of the testbench file, ``tb_path``, and an absolute -one is used as given. ``exec_file`` also puts the directory of the file on -``sys.path`` while it executes, so the file can import the modules next to it. - -Module names -~~~~~~~~~~~~ - -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. - 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/running_code.rst b/docs/running_code.rst index 9a14c72..72bc4b3 100644 --- a/docs/running_code.rst +++ b/docs/running_code.rst @@ -54,16 +54,20 @@ 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:`Where Python code is - found `. No file name is involved, so the class - is found the same way wherever the object is created. + 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. + 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 +``new_python_object``. The constructor is called when the object is first used, not by ``new_python_object``. An object can therefore be a constant or a generic, made during elaboration, and does not depend on the order of the processes or -on ``test_runner_setup``. ``create`` calls the constructor right away. +on ``test_runner_setup``. ``create_now`` calls the constructor right away. ``call(object, method, ...)`` calls a method, and ``eval(object, expr)`` and ``exec(object, code)`` evaluate an expression and execute code in the session @@ -79,9 +83,11 @@ The ``id`` of an object names its session and its logger, ``get_logger(object)``, which reports the errors of its operations. 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. Two objects with the same -identity would share a session, so the second ``new_python_object`` of an -identity is reported as a failure on its logger. +``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``. Backends of verification components ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -184,11 +190,13 @@ name as a ``string`` generic. Errors and limitations ~~~~~~~~~~~~~~~~~~~~~~ -* Errors are reported on the logger of the object, named after what failed, - for example ``call("push") failed:`` followed by the Python traceback. -* An object that cannot be created, for example since its module is not - found, reports the error of the import or the constructor once, as - ``new_python_object("") failed:``. Its later calls fail with +* 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:``. + 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. @@ -202,8 +210,8 @@ Errors and limitations * 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, the first use of an object reports that sessions are - only supported there. + Riviera-PRO/Active-HDL, ``new_python_object`` reports that as a failure on + the logger of the object and returns ``null_python_object``. exec ---- From 5dcdf91706d7ee538224ef160f94397a7c29b112 Mon Sep 17 00:00:00 2001 From: Sebastian Abrahamsson Date: Tue, 13 Oct 2026 21:00:35 +0200 Subject: [PATCH 19/36] Apply the learnings of trying objects in a real project 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 --- docs/running_code.rst | 49 ++++++++++++++++++++++++++++-- src/vunit_python_bridge/runtime.py | 3 +- tests/tb_python_objects.vhd | 4 +-- tests/test_python_bridge.py | 4 +++ 4 files changed, 55 insertions(+), 5 deletions(-) diff --git a/docs/running_code.rst b/docs/running_code.rst index 72bc4b3..f2f100f 100644 --- a/docs/running_code.rst +++ b/docs/running_code.rst @@ -67,7 +67,9 @@ A name that is neither, or that is not a class, is reported as an error of The constructor is called when the object is first used, not by ``new_python_object``. An object can therefore be a constant or a generic, made during elaboration, and does not depend on the order of the processes or -on ``test_runner_setup``. ``create_now`` calls the constructor right away. +on ``test_runner_setup``. ``create_now`` calls the constructor right away: +call it early in the test, for example after ``test_runner_setup``, to have a +wrong constructor argument reported at the start rather than at the first call. ``call(object, method, ...)`` calls a method, and ``eval(object, expr)`` and ``exec(object, code)`` evaluate an expression and execute code in the session @@ -80,7 +82,8 @@ Identity ~~~~~~~~ The ``id`` of an object names its session and its logger, -``get_logger(object)``, which reports the errors of its operations. Without an +``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 @@ -89,6 +92,48 @@ 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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/vunit_python_bridge/runtime.py b/src/vunit_python_bridge/runtime.py index 874b58d..3d6f672 100644 --- a/src/vunit_python_bridge/runtime.py +++ b/src/vunit_python_bridge/runtime.py @@ -136,7 +136,8 @@ def _resolve_class(class_name): 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): - raise ImportError(f"No class named {class_name!r}") + 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__}") diff --git a/tests/tb_python_objects.vhd b/tests/tb_python_objects.vhd index 14158e8..88f4a9a 100644 --- a/tests/tb_python_objects.vhd +++ b/tests/tb_python_objects.vhd @@ -121,7 +121,7 @@ begin check_only_log( get_logger(broken_id), "new_python_object(""models.no_such_module.Model"") failed:" & LF & - "ImportError: No class named 'models.no_such_module.Model'", + "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 @@ -132,7 +132,7 @@ begin 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'", + "ImportError: No class named 'models.no_such_module.Model': models has no attribute 'no_such_module'", failure ); unmock(get_logger(broken_id)); diff --git a/tests/test_python_bridge.py b/tests/test_python_bridge.py index 10ac93e..d2fe71a 100644 --- a/tests/test_python_bridge.py +++ b/tests/test_python_bridge.py @@ -212,6 +212,10 @@ 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") From 0d40102cefc965e708400d51ad26f7cada64aad3 Mon Sep 17 00:00:00 2001 From: Sebastian Abrahamsson Date: Tue, 13 Oct 2026 21:12:07 +0200 Subject: [PATCH 20/36] Pair new_python_object with create_python_object 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 --- docs/running_code.rst | 53 +++++++++++++++---- .../hdl/src/python_object_pkg.vhd | 40 ++++++++++---- .../hdl/tools/python_object_pkg.vhd.in | 40 ++++++++++---- tests/tb_python_objects.vhd | 32 ++++++++--- 4 files changed, 131 insertions(+), 34 deletions(-) diff --git a/docs/running_code.rst b/docs/running_code.rst index f2f100f..c589069 100644 --- a/docs/running_code.rst +++ b/docs/running_code.rst @@ -62,14 +62,7 @@ with ``&``. The class is 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 -``new_python_object``. - -The constructor is called when the object is first used, not by -``new_python_object``. An object can therefore be a constant or a generic, -made during elaboration, and does not depend on the order of the processes or -on ``test_runner_setup``. ``create_now`` calls the constructor right away: -call it early in the test, for example after ``test_runner_setup``, to have a -wrong constructor argument reported at the start rather than at the first call. +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 @@ -78,6 +71,47 @@ 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 ~~~~~~~~ @@ -240,7 +274,8 @@ Errors and limitations 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:``. + 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 diff --git a/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd b/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd index 52e7f49..4d6df19 100644 --- a/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd +++ b/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd @@ -46,6 +46,13 @@ package python_object_pkg is 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( @@ -58,9 +65,6 @@ package python_object_pkg is -- The session of the object, in which it is bound to self impure function get_session(object : python_object_t) return python_session_t; - -- Create the object now rather than on first use - procedure create_now(object : python_object_t); - -- Execute code in the session of the object, where it is self, like exec procedure exec(object : python_object_t; code : string); @@ -206,9 +210,9 @@ package body python_object_pkg is return result; end; - impure function new_python_object(class_name : string; args : arg_t := null_arg; id : id_t := null_id) + -- 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 - constant operation : string := "new_python_object(""" & class_name & """)"; variable object_id : id_t := id; variable data : integer_vector_ptr_t; begin @@ -238,6 +242,12 @@ package body python_object_pkg is 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 @@ -274,9 +284,10 @@ package body python_object_pkg is return get_id(get_session(object)); end; - -- A failure is reported once, as the failure of new_python_object, and the - -- object then raises it on every use - procedure create_now(object : python_object_t) is + -- 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 @@ -292,13 +303,24 @@ package body python_object_pkg is 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; - create_now(object); + p_create(object); return true; end; 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 index abb9e5c..d95e8e8 100644 --- a/src/vunit_python_bridge/hdl/tools/python_object_pkg.vhd.in +++ b/src/vunit_python_bridge/hdl/tools/python_object_pkg.vhd.in @@ -46,6 +46,13 @@ package python_object_pkg is 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( @@ -58,9 +65,6 @@ package python_object_pkg is -- The session of the object, in which it is bound to self impure function get_session(object : python_object_t) return python_session_t; - -- Create the object now rather than on first use - procedure create_now(object : python_object_t); - -- Execute code in the session of the object, where it is self, like exec procedure exec(object : python_object_t; code : string); @@ -89,9 +93,9 @@ package body python_object_pkg is return result; end; - impure function new_python_object(class_name : string; args : arg_t := null_arg; id : id_t := null_id) + -- 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 - constant operation : string := "new_python_object(""" & class_name & """)"; variable object_id : id_t := id; variable data : integer_vector_ptr_t; begin @@ -121,6 +125,12 @@ package body python_object_pkg is 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 @@ -157,9 +167,10 @@ package body python_object_pkg is return get_id(get_session(object)); end; - -- A failure is reported once, as the failure of new_python_object, and the - -- object then raises it on every use - procedure create_now(object : python_object_t) is + -- 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 @@ -175,13 +186,24 @@ package body python_object_pkg is 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; - create_now(object); + p_create(object); return true; end; diff --git a/tests/tb_python_objects.vhd b/tests/tb_python_objects.vhd index 88f4a9a..58d55f1 100644 --- a/tests/tb_python_objects.vhd +++ b/tests/tb_python_objects.vhd @@ -33,6 +33,7 @@ begin 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; @@ -72,8 +73,7 @@ begin check(bits = "00001000", "eval_std_ulogic_vector"); elsif run("Test exec and get_logger of an object") then - object := new_python_object("models.counter_model.Counter", id => get_id("exec_test")); - create_now(object); + 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); @@ -115,18 +115,16 @@ begin check(object = null_python_object, "no object for the identity of a session"); elsif run("Test an object that cannot be created") then - object := new_python_object("models.no_such_module.Model", id => broken_id); + -- create_python_object reports it right away mock(get_logger(broken_id), failure); - create_now(object); + object := create_python_object("models.no_such_module.Model", id => broken_id); check_only_log( get_logger(broken_id), - "new_python_object(""models.no_such_module.Model"") failed:" & LF & + "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 - create_now(object); - check_no_log; check_equal(integer'(call(object, "value")), integer'low); check_only_log( get_logger(broken_id), @@ -137,6 +135,26 @@ begin ); 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); From cba45245f0b8734abaedb2f0d9ed9e1fd2ea3859 Mon Sep 17 00:00:00 2001 From: Sebastian Abrahamsson Date: Wed, 14 Oct 2026 11:23:23 +0200 Subject: [PATCH 21/36] Give Python objects a page of their own in the documentation 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 --- docs/index.rst | 4 + docs/python_objects.rst | 268 +++++++++++++++++++++++++++++++++++++++ docs/running_code.rst | 269 ---------------------------------------- docs/vhdl_api.py | 6 + 4 files changed, 278 insertions(+), 269 deletions(-) create mode 100644 docs/python_objects.rst 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/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/running_code.rst b/docs/running_code.rst index c589069..ed3b1de 100644 --- a/docs/running_code.rst +++ b/docs/running_code.rst @@ -24,275 +24,6 @@ depend on ``python_cleanup``. python_cleanup; -- Optional, releases the staged values early test_runner_cleanup(runner); -.. _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``. - exec ---- 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*$") From daf8068983920b3a428ce7ff2c3b683b363b57f5 Mon Sep 17 00:00:00 2001 From: Sebastian Abrahamsson Date: Wed, 14 Oct 2026 14:36:13 +0200 Subject: [PATCH 22/36] Address review: drop the simulator check of objects, new docs example 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 --- docs/python_objects.rst | 14 ++++++-------- .../hdl/src/python_ffi_pkg_bridge.vhd | 3 --- .../hdl/src/python_object_pkg.vhd | 6 +----- .../hdl/src/python_pkg_vhpi.vhd | 4 ---- .../hdl/tools/generate_python_pkg.py | 2 +- .../hdl/tools/python_object_pkg.vhd.in | 4 ---- 6 files changed, 8 insertions(+), 25 deletions(-) diff --git a/docs/python_objects.rst b/docs/python_objects.rst index e6b1997..fe0f321 100644 --- a/docs/python_objects.rst +++ b/docs/python_objects.rst @@ -217,17 +217,18 @@ during elaboration, which would give all instances the same identity. 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: +memory model written by a bus slave and read back by the test is a typical +case: .. code-block:: vhdl - constant scoreboard : python_object_t := new_python_object("my_models.Scoreboard"); + constant memory : python_object_t := new_python_object("my_models.Memory"); ... - monitor_inst : entity work.my_monitor + slave_inst : entity work.my_bus_slave generic map ( - monitor => new_my_monitor(model => scoreboard) + slave => new_my_bus_slave(model => memory) ) port map ( ... @@ -235,7 +236,7 @@ scoreboard filled by a monitor and checked by the test is a typical case: ... - check_equal(integer'(call(scoreboard, "num_mismatches")), 0); + check_equal(integer'(call(memory, "read", arg(16#100#))), 16#cafe#); In the same way, a component can let its user choose the class, by taking its name as a ``string`` generic. @@ -263,6 +264,3 @@ Errors and limitations `__. * 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/src/vunit_python_bridge/hdl/src/python_ffi_pkg_bridge.vhd b/src/vunit_python_bridge/hdl/src/python_ffi_pkg_bridge.vhd index d50e648..c9052c7 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 @@ -112,9 +112,6 @@ 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; diff --git a/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd b/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd index 4d6df19..9ddb50f 100644 --- a/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd +++ b/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd @@ -68,7 +68,7 @@ package python_object_pkg is -- 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 + -- Call a method of the object procedure call( object : python_object_t; method : string; arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg @@ -220,10 +220,6 @@ package body python_object_pkg is -- 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( 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 1c12cdd..52da6cf 100644 --- a/src/vunit_python_bridge/hdl/src/python_pkg_vhpi.vhd +++ b/src/vunit_python_bridge/hdl/src/python_pkg_vhpi.vhd @@ -91,9 +91,6 @@ 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 := false; - -- Whether new_session has made a session with the identity impure function p_has_session(id : id_t) return boolean; @@ -199,7 +196,6 @@ package body python_ffi_pkg is 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; 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 9def0b9..2297e75 100644 --- a/src/vunit_python_bridge/hdl/tools/generate_python_pkg.py +++ b/src/vunit_python_bridge/hdl/tools/generate_python_pkg.py @@ -389,7 +389,7 @@ def object_declarations(): python_pkg with the object first, the method or expression second. """ lines = [ - " -- Call a method of the object, like call", + " -- Call a method of the object", " procedure call(", f" {OBJECT}; method : string;", f" {ARG_PARAMETERS}", 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 index d95e8e8..3b71e34 100644 --- a/src/vunit_python_bridge/hdl/tools/python_object_pkg.vhd.in +++ b/src/vunit_python_bridge/hdl/tools/python_object_pkg.vhd.in @@ -103,10 +103,6 @@ package body python_object_pkg is -- 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( From caf74f97d33819f03e61cf52389e74c4f181c435 Mon Sep 17 00:00:00 2001 From: Sebastian Abrahamsson Date: Thu, 15 Oct 2026 13:36:34 +0200 Subject: [PATCH 23/36] Get Python objects by identity or full name 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 --- docs/python_objects.rst | 39 ++++++++--- examples/embedded_python/filter_vc_pkg.vhd | 6 +- .../hdl/src/python_object_pkg.vhd | 70 +++++++++++++++---- .../hdl/tools/python_object_pkg.vhd.in | 70 +++++++++++++++---- tests/counter_vc_pkg.vhd | 6 +- tests/path_counter_vc.vhd | 6 +- tests/tb_python_objects.vhd | 33 ++++++++- 7 files changed, 186 insertions(+), 44 deletions(-) diff --git a/docs/python_objects.rst b/docs/python_objects.rst index fe0f321..276cca7 100644 --- a/docs/python_objects.rst +++ b/docs/python_objects.rst @@ -100,6 +100,24 @@ 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``. +The identity can also be given by its full name, like ``get_logger(name)``: +``new_python_object("models.Uart", name => "tb:uart_model")`` is +``new_python_object("models.Uart", id => get_id("tb:uart_model"))``, and so +for ``create_python_object``. + +``get_python_object(id)`` and ``get_python_object(name)`` return the object +made with that identity, so a testbench can reach the object of a component +without being given it: + +.. code-block:: vhdl + + check_true(eval_boolean(get_python_object("tb:uart_model"), "self.is_idle()")); + +Unlike ``get_logger``, it does not make one: an identity without an object is +reported as a failure on ``python_logger`` and returns ``null_python_object``. +An object is found once it is made, which for one made by another constant +during elaboration depends on the order of elaboration, so get it in a process. + From a model with module state to an object ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -147,10 +165,8 @@ 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: +Every instance then gets a backend of its own, unless the testbench gives it +one: .. code-block:: vhdl @@ -163,11 +179,12 @@ unless the testbench gives it one: 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"); + variable result : uart_master_t := (p_std_cfg => std_cfg, p_model => model); begin - return ( - p_std_cfg => std_cfg, - p_model => get_python_object(model, "my_vcs.uart.UartModel", kwarg("baud", baud), get_id(std_cfg)) - ); + if model = null_python_object then + result.p_model := new_python_object("my_vcs.uart.UartModel", kwarg("baud", baud), get_id(std_cfg)); + end if; + return result; end; The component itself only calls the object of its handle: @@ -207,12 +224,14 @@ during elaboration, which would give all instances the same identity. process variable vc_id : id_t := id; - variable backend : python_object_t; + variable backend : python_object_t := model; 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); + if backend = null_python_object then + backend := new_python_object("models.counter_model.Counter", kwarg("step", step), vc_id); + end if; ... Several components can share an object, and so can a component and the diff --git a/examples/embedded_python/filter_vc_pkg.vhd b/examples/embedded_python/filter_vc_pkg.vhd index 8b2cc53..ddaa7ce 100644 --- a/examples/embedded_python/filter_vc_pkg.vhd +++ b/examples/embedded_python/filter_vc_pkg.vhd @@ -31,8 +31,12 @@ 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 + variable result : filter_vc_t := (p_model => model); begin - return (p_model => get_python_object(model, "filter_model.MovingAverage", kwarg("window", window), id)); + if model = null_python_object then + result.p_model := new_python_object("filter_model.MovingAverage", kwarg("window", window), id); + end if; + return result; end; impure function get_model(filter : filter_vc_t) return python_object_t is diff --git a/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd b/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd index 9ddb50f..1878db0 100644 --- a/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd +++ b/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd @@ -40,11 +40,13 @@ package python_object_pkg is -- 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. + -- An identity already used by an object or a session is 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; + -- The identity given by its full name, like get_logger(name) + impure function new_python_object(class_name : string; args : arg_t := null_arg; name : string) + 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. @@ -52,12 +54,14 @@ package python_object_pkg is -- 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; + impure function create_python_object(class_name : string; args : arg_t := null_arg; name : string) + 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; + -- The object made with the identity, or with the identity of the full name. + -- Unlike get_logger, it does not make one: an identity without an object is + -- reported as a failure on python_logger, and null_python_object is returned. + impure function get_python_object(id : id_t) return python_object_t; + impure function get_python_object(name : string) 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 @@ -210,6 +214,9 @@ package body python_object_pkg is return result; end; + -- The objects by the full names of their identities, as integers of their p_data + constant p_objects : dict_t := new_dict; + -- 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 @@ -235,6 +242,7 @@ package body python_object_pkg is to_call_str("__vunit__.instantiate_as_self", arg(class_name), args) ))); set(data, operation_idx, to_integer(new_string_ptr(operation))); + set_string(p_objects, full_name(object_id), integer'image(to_integer(data))); return (p_data => data); end; @@ -244,14 +252,42 @@ package body python_object_pkg is 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 + impure function new_python_object(class_name : string; args : arg_t := null_arg; name : string) + return python_object_t is begin - if object /= null_python_object then - return object; + return new_python_object(class_name, args, get_id(name)); + end; + + procedure p_no_object(name : string) is + begin + failure( + python_logger, + "get_python_object(""" & name & """) failed:" & LF & "No Python object has the identity " & name & + ". Make it with new_python_object or create_python_object before getting it." + ); + end; + + impure function get_python_object(id : id_t) return python_object_t is + begin + if id = null_id then + p_no_object("null_id"); + return null_python_object; + end if; + if not has_key(p_objects, full_name(id)) then + p_no_object(full_name(id)); + return null_python_object; + end if; + return (p_data => to_integer_vector_ptr(integer'value(get_string(p_objects, full_name(id))))); + end; + + impure function get_python_object(name : string) return python_object_t is + begin + -- has_id first: get_id would make the identity of a wrong name + if not has_id(name) then + p_no_object(name); + return null_python_object; end if; - return new_python_object(class_name, args, id); + return get_python_object(get_id(name)); end; -- Report the use of null_python_object, e.g. the result of a failed new_python_object @@ -310,6 +346,12 @@ package body python_object_pkg is return object; end; + impure function create_python_object(class_name : string; args : arg_t := null_arg; name : string) + return python_object_t is + begin + return create_python_object(class_name, args, get_id(name)); + 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 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 index 3b71e34..e41e3e6 100644 --- a/src/vunit_python_bridge/hdl/tools/python_object_pkg.vhd.in +++ b/src/vunit_python_bridge/hdl/tools/python_object_pkg.vhd.in @@ -40,11 +40,13 @@ package python_object_pkg is -- 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. + -- An identity already used by an object or a session is 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; + -- The identity given by its full name, like get_logger(name) + impure function new_python_object(class_name : string; args : arg_t := null_arg; name : string) + 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. @@ -52,12 +54,14 @@ package python_object_pkg is -- 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; + impure function create_python_object(class_name : string; args : arg_t := null_arg; name : string) + 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; + -- The object made with the identity, or with the identity of the full name. + -- Unlike get_logger, it does not make one: an identity without an object is + -- reported as a failure on python_logger, and null_python_object is returned. + impure function get_python_object(id : id_t) return python_object_t; + impure function get_python_object(name : string) 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 @@ -93,6 +97,9 @@ package body python_object_pkg is return result; end; + -- The objects by the full names of their identities, as integers of their p_data + constant p_objects : dict_t := new_dict; + -- 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 @@ -118,6 +125,7 @@ package body python_object_pkg is to_call_str("__vunit__.instantiate_as_self", arg(class_name), args) ))); set(data, operation_idx, to_integer(new_string_ptr(operation))); + set_string(p_objects, full_name(object_id), integer'image(to_integer(data))); return (p_data => data); end; @@ -127,14 +135,42 @@ package body python_object_pkg is 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 + impure function new_python_object(class_name : string; args : arg_t := null_arg; name : string) + return python_object_t is begin - if object /= null_python_object then - return object; + return new_python_object(class_name, args, get_id(name)); + end; + + procedure p_no_object(name : string) is + begin + failure( + python_logger, + "get_python_object(""" & name & """) failed:" & LF & "No Python object has the identity " & name & + ". Make it with new_python_object or create_python_object before getting it." + ); + end; + + impure function get_python_object(id : id_t) return python_object_t is + begin + if id = null_id then + p_no_object("null_id"); + return null_python_object; end if; - return new_python_object(class_name, args, id); + if not has_key(p_objects, full_name(id)) then + p_no_object(full_name(id)); + return null_python_object; + end if; + return (p_data => to_integer_vector_ptr(integer'value(get_string(p_objects, full_name(id))))); + end; + + impure function get_python_object(name : string) return python_object_t is + begin + -- has_id first: get_id would make the identity of a wrong name + if not has_id(name) then + p_no_object(name); + return null_python_object; + end if; + return get_python_object(get_id(name)); end; -- Report the use of null_python_object, e.g. the result of a failed new_python_object @@ -193,6 +229,12 @@ package body python_object_pkg is return object; end; + impure function create_python_object(class_name : string; args : arg_t := null_arg; name : string) + return python_object_t is + begin + return create_python_object(class_name, args, get_id(name)); + 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 diff --git a/tests/counter_vc_pkg.vhd b/tests/counter_vc_pkg.vhd index a6e1b30..36c2c80 100644 --- a/tests/counter_vc_pkg.vhd +++ b/tests/counter_vc_pkg.vhd @@ -30,8 +30,12 @@ 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 + variable result : counter_vc_t := (p_model => model); begin - return (p_model => get_python_object(model, "models.counter_model.Counter", kwarg("step", step), id)); + if model = null_python_object then + result.p_model := new_python_object("models.counter_model.Counter", kwarg("step", step), id); + end if; + return result; end; impure function get_model(vc : counter_vc_t) return python_object_t is diff --git a/tests/path_counter_vc.vhd b/tests/path_counter_vc.vhd index 1bb91a3..1d98379 100644 --- a/tests/path_counter_vc.vhd +++ b/tests/path_counter_vc.vhd @@ -31,14 +31,16 @@ architecture python of path_counter_vc is begin process variable vc_id : id_t := id; - variable backend : python_object_t; + variable backend : python_object_t := model; 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); + if backend = null_python_object then + backend := new_python_object("models.counter_model.Counter", kwarg("step", step), vc_id); + end if; loop wait on tick; diff --git a/tests/tb_python_objects.vhd b/tests/tb_python_objects.vhd index 58d55f1..8b5fdb8 100644 --- a/tests/tb_python_objects.vhd +++ b/tests/tb_python_objects.vhd @@ -53,6 +53,7 @@ begin 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(get_python_object("vc_a"), "value")), 3, "the model of vc_a by its identity"); check_equal(integer'(call(shared_model, "value")), 103); check_equal(eval_string(shared_model, "type(self).__name__"), "Counter"); @@ -192,6 +193,34 @@ begin exec(object, "self.count = 7"); check_equal(integer'(call(object, "value")), 7); + elsif run("Test that get_python_object finds an object by its identity or full name") then + object := new_python_object("models.counter_model.Counter", kwarg("start", 5), name => "tb:by_name"); + check_equal(full_name(get_id(object)), "tb:by_name"); + check(get_python_object("tb:by_name") = object, "by full name"); + check(get_python_object(get_id("tb:by_name")) = object, "by identity"); + other := create_python_object("models.counter_model.Counter", kwarg("start", 6), name => "tb:created"); + check_equal(integer'(call(get_python_object("tb:created"), "value")), 6); + + elsif run("Test that get_python_object reports an identity without an object") then + mock(python_logger, failure); + check(get_python_object("tb:no_object") = null_python_object, "unknown name"); + check_only_log( + python_logger, + "get_python_object(""tb:no_object"") failed:" & LF & "No Python object has the identity tb:no_object." & + " Make it with new_python_object or create_python_object before getting it.", + failure + ); + check_false(has_id("tb:no_object"), "a failed lookup makes no identity"); + check(get_python_object(get_id("tb:id_without_object")) = null_python_object, "identity without object"); + check_only_log( + python_logger, + "get_python_object(""tb:id_without_object"") failed:" & LF & + "No Python object has the identity tb:id_without_object." & + " Make it with new_python_object or create_python_object before getting it.", + failure + ); + unmock(python_logger); + elsif run("Test that null_python_object is reported") then mock(python_logger, failure); call(null_python_object, "value"); @@ -204,8 +233,8 @@ begin -- 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)); + object := new_python_object("models.counter_model.Counter", id => get_id(std_cfg)); + other := new_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); From 8f74cbbd219bebe92caf82714ee9dffafbbf9ec9 Mon Sep 17 00:00:00 2001 From: Sebastian Abrahamsson Date: Thu, 15 Oct 2026 16:20:14 +0200 Subject: [PATCH 24/36] Bind a Python object to obj instead of self in its session 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 --- docs/python_objects.rst | 12 +++--- .../hdl/src/python_object_pkg.vhd | 40 +++++++++---------- .../hdl/tools/generate_python_pkg.py | 8 ++-- .../hdl/tools/python_object_pkg.vhd.in | 10 ++--- src/vunit_python_bridge/runtime.py | 14 +++---- tests/tb_python_objects.vhd | 16 ++++---- tests/test_python_bridge.py | 32 +++++++-------- 7 files changed, 66 insertions(+), 66 deletions(-) diff --git a/docs/python_objects.rst b/docs/python_objects.rst index 276cca7..4d37ed3 100644 --- a/docs/python_objects.rst +++ b/docs/python_objects.rst @@ -5,7 +5,7 @@ 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 +is bound to ``obj``, 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. @@ -18,10 +18,10 @@ own, and a testbench can create one and give it to a component. ... - count := call(model, "push", arg(byte)); -- calls self.push(byte) + count := call(model, "push", arg(byte)); -- calls obj.push(byte) call(model, "reset"); -- no return value - check_true(eval_boolean(model, "self.is_idle()")); - exec(model, "self.trace = True"); + check_true(eval_boolean(model, "obj.is_idle()")); + exec(model, "obj.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 @@ -40,7 +40,7 @@ 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, +of the object, where the object is ``obj``. 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. @@ -111,7 +111,7 @@ without being given it: .. code-block:: vhdl - check_true(eval_boolean(get_python_object("tb:uart_model"), "self.is_idle()")); + check_true(eval_boolean(get_python_object("tb:uart_model"), "obj.is_idle()")); Unlike ``get_logger``, it does not make one: an identity without an object is reported as a failure on ``python_logger`` and returns ``null_python_object``. diff --git a/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd b/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd index 1878db0..bbad00c 100644 --- a/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd +++ b/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd @@ -5,7 +5,7 @@ -- 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 +-- living in a session of its own, where it is obj, created on first use. It -- is typically the backend of a verification component, see "Python objects" -- in the user guide. -- @@ -66,10 +66,10 @@ package python_object_pkg is 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 + -- The session of the object, in which it is bound to obj 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 + -- Execute code in the session of the object, where it is obj, like exec procedure exec(object : python_object_t; code : string); -- Call a method of the object @@ -168,7 +168,7 @@ package python_object_pkg is 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 + -- Evaluate an expression in the session of the object, where it is obj, 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; @@ -239,7 +239,7 @@ package body python_object_pkg is 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) + to_call_str("__vunit__.instantiate_as_obj", arg(class_name), args) ))); set(data, operation_idx, to_integer(new_string_ptr(operation))); set_string(p_objects, full_name(object_id), integer'image(to_integer(data))); @@ -300,7 +300,7 @@ package body python_object_pkg is return false; end; - -- The session of null_python_object, where nothing is bound to self + -- The session of null_python_object, where nothing is bound to obj 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 @@ -391,7 +391,7 @@ package body python_object_pkg is 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), + to_call_str("obj." & method, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10), 0, p_call_operation(method), get_session(object) ); end; @@ -405,7 +405,7 @@ package body python_object_pkg is return integer'low; end if; if p_eval( - to_call_str("self." & method, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10), + to_call_str("obj." & 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; @@ -422,7 +422,7 @@ package body python_object_pkg is return 0.0; end if; if p_eval( - to_call_str("self." & method, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10), + to_call_str("obj." & 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; @@ -439,7 +439,7 @@ package body python_object_pkg is return p_no_integers; end if; if p_eval( - to_call_str("self." & method, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10), + to_call_str("obj." & 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; @@ -456,7 +456,7 @@ package body python_object_pkg is return p_no_reals; end if; if p_eval( - to_call_str("self." & method, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10), + to_call_str("obj." & 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; @@ -473,7 +473,7 @@ package body python_object_pkg is return ""; end if; if p_eval( - to_call_str("self." & method, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10), + to_call_str("obj." & 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; @@ -490,7 +490,7 @@ package body python_object_pkg is return null_ptr; end if; if p_eval( - to_call_str("self." & method, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10), + to_call_str("obj." & 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); @@ -507,7 +507,7 @@ package body python_object_pkg is return false; end if; if p_eval( - to_call_str("self." & method, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10), + to_call_str("obj." & 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; @@ -524,7 +524,7 @@ package body python_object_pkg is return 'U'; end if; if p_eval( - to_call_str("self." & method, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10), + to_call_str("obj." & 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)); @@ -541,7 +541,7 @@ package body python_object_pkg is return ""; end if; if p_eval( - to_call_str("self." & method, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10), + to_call_str("obj." & 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); @@ -558,7 +558,7 @@ package body python_object_pkg is return null_integer_array; end if; if p_eval( - to_call_str("self." & method, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10), + to_call_str("obj." & 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; @@ -575,7 +575,7 @@ package body python_object_pkg is return; end if; if p_eval( - to_call_str("self." & method, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10), + to_call_str("obj." & 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); @@ -591,7 +591,7 @@ package body python_object_pkg is return; end if; if p_eval( - to_call_str("self." & method, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10), + to_call_str("obj." & 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)); @@ -607,7 +607,7 @@ package body python_object_pkg is return; end if; if p_eval( - to_call_str("self." & method, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10), + to_call_str("obj." & 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)); 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 2297e75..b8baf68 100644 --- a/src/vunit_python_bridge/hdl/tools/generate_python_pkg.py +++ b/src/vunit_python_bridge/hdl/tools/generate_python_pkg.py @@ -414,7 +414,7 @@ def object_declarations(): 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"] + lines += ["", " -- Evaluate an expression in the session of the object, where it is obj, 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): @@ -445,7 +445,7 @@ def object_subprograms(): 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}), + to_call_str("obj." & method, {ARG_ACTUALS}), 0, p_call_operation(method), get_session(object) ); end; @@ -464,7 +464,7 @@ def object_subprograms(): return {result['default']}; end if; if p_eval( - to_call_str("self." & method, {ARG_ACTUALS}), + to_call_str("obj." & method, {ARG_ACTUALS}), {result['kind']}, -1, p_call_operation(method), get_session(object) ) then return {result['value']}; @@ -485,7 +485,7 @@ def object_subprograms(): return; end if; if p_eval( - to_call_str("self." & method, {ARG_ACTUALS}), + to_call_str("obj." & method, {ARG_ACTUALS}), {result['kind']}, result'length, p_call_operation(method), get_session(object) ) then result := {result['value']}; 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 index e41e3e6..c693d5f 100644 --- a/src/vunit_python_bridge/hdl/tools/python_object_pkg.vhd.in +++ b/src/vunit_python_bridge/hdl/tools/python_object_pkg.vhd.in @@ -5,7 +5,7 @@ -- 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 +-- living in a session of its own, where it is obj, created on first use. It -- is typically the backend of a verification component, see "Python objects" -- in the user guide. -- @@ -66,10 +66,10 @@ package python_object_pkg is 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 + -- The session of the object, in which it is bound to obj 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 + -- Execute code in the session of the object, where it is obj, like exec procedure exec(object : python_object_t; code : string); $object_declarations @@ -122,7 +122,7 @@ package body python_object_pkg is 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) + to_call_str("__vunit__.instantiate_as_obj", arg(class_name), args) ))); set(data, operation_idx, to_integer(new_string_ptr(operation))); set_string(p_objects, full_name(object_id), integer'image(to_integer(data))); @@ -183,7 +183,7 @@ package body python_object_pkg is return false; end; - -- The session of null_python_object, where nothing is bound to self + -- The session of null_python_object, where nothing is bound to obj 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 diff --git a/src/vunit_python_bridge/runtime.py b/src/vunit_python_bridge/runtime.py index 3d6f672..1497368 100644 --- a/src/vunit_python_bridge/runtime.py +++ b/src/vunit_python_bridge/runtime.py @@ -155,7 +155,7 @@ def _plain(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 + Bound to obj 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. """ @@ -200,22 +200,22 @@ def error(message): """ raise RuntimeError(message) - def instantiate_as_self(self, class_name, *args, **kwargs): + def instantiate_as_obj(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. + named class_name, and bind it to obj in the current session. If that + fails, obj 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) + namespace["obj"] = _UncreatedObject(class_name, exc) raise _plain(exc) try: - namespace["self"] = class_(*args, **kwargs) + namespace["obj"] = class_(*args, **kwargs) except Exception as exc: - namespace["self"] = _UncreatedObject(class_name, exc) + namespace["obj"] = _UncreatedObject(class_name, exc) raise def __repr__(self): diff --git a/tests/tb_python_objects.vhd b/tests/tb_python_objects.vhd index 8b5fdb8..ed6f619 100644 --- a/tests/tb_python_objects.vhd +++ b/tests/tb_python_objects.vhd @@ -55,20 +55,20 @@ begin check_equal(count_d, 9, "vc_d, a Counter of its own with step 3, by instance path"); check_equal(integer'(call(get_python_object("vc_a"), "value")), 3, "the model of vc_a by its identity"); check_equal(integer'(call(shared_model, "value")), 103); - check_equal(eval_string(shared_model, "type(self).__name__"), "Counter"); + check_equal(eval_string(shared_model, "type(obj).__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(integer'(eval(object, "obj.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")); + check_true(eval_boolean(object, "obj.count == 6")); -- The eval procedures of an object - eval_unsigned(object, "self.count + 1", byte); + eval_unsigned(object, "obj.count + 1", byte); check(byte = 7, "eval_unsigned"); eval_std_ulogic_vector(object, "'0000' + '1000'", bits); check(bits = "00001000", "eval_std_ulogic_vector"); @@ -76,7 +76,7 @@ begin 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"); + exec(object, "obj.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"); @@ -179,8 +179,8 @@ begin 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); + check_equal(integer'(eval(object, "obj.count")), 1); + check_equal(integer'(eval(other, "obj.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 @@ -190,7 +190,7 @@ begin 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"); + exec(object, "obj.count = 7"); check_equal(integer'(call(object, "value")), 7); elsif run("Test that get_python_object finds an object by its identity or full name") then diff --git a/tests/test_python_bridge.py b/tests/test_python_bridge.py index d2fe71a..dd38cc9 100644 --- a/tests/test_python_bridge.py +++ b/tests/test_python_bridge.py @@ -172,7 +172,7 @@ def test_the_simulator_dependent_vhdl_is_not_listed(self): class TestObjectCreation(unittest.TestCase): """ - __vunit__.instantiate_as_self, which creates the object of a python_object_t in its session. + __vunit__.instantiate_as_obj, which creates the object of a python_object_t in its session. """ def setUp(self): @@ -182,61 +182,61 @@ def setUp(self): 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}) + self.handle.instantiate_as_obj("collections.OrderedDict", a=1) + self.assertEqual(self.namespace["obj"], {"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) + self.handle.instantiate_as_obj("models_under_test.Outer.Inner") + self.assertIsInstance(self.namespace["obj"], 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) + self.handle.instantiate_as_obj("Model") + self.assertIsInstance(self.namespace["obj"], 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) + self.handle.instantiate_as_obj("alias.Model") + self.assertIsInstance(self.namespace["obj"], 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") + self.handle.instantiate_as_obj("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") + self.handle.instantiate_as_obj("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") + self.handle.instantiate_as_obj("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") + self.handle.instantiate_as_obj("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) + self.handle.instantiate_as_obj("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() + self.namespace["obj"].step() with self.assertRaisesRegex(RuntimeError, "could not be created"): - self.namespace["self"].trace = True + self.namespace["obj"].trace = True class TestGeneratedPackages(unittest.TestCase): From 1a9e3d968d7de25bf20d5d5e0b965b16f944cebc Mon Sep 17 00:00:00 2001 From: Sebastian Abrahamsson Date: Thu, 15 Oct 2026 16:23:53 +0200 Subject: [PATCH 25/36] Name a Python object made without an id after its class 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 --- docs/python_objects.rst | 8 +++++--- .../hdl/src/python_object_pkg.vhd | 18 ++++++++++++++++-- .../hdl/tools/python_object_pkg.vhd.in | 18 ++++++++++++++++-- tests/tb_python_objects.vhd | 10 ++++++---- 4 files changed, 43 insertions(+), 11 deletions(-) diff --git a/docs/python_objects.rst b/docs/python_objects.rst index 4d37ed3..509d399 100644 --- a/docs/python_objects.rst +++ b/docs/python_objects.rst @@ -92,9 +92,11 @@ 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 +``id``, objects are enumerated per class, the way VUnit enumerates +verification components, with the class name as the hierarchy: +``new_python_object("models.uart.UartModel")`` gives +``python_bridge:python:object:models:uart:UartModel:1``, then ``…: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 diff --git a/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd b/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd index bbad00c..22dfdbf 100644 --- a/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd +++ b/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd @@ -39,7 +39,8 @@ package python_object_pkg is -- 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:. + -- enumerated per class like VUnit's verification components, the class name + -- giving the hierarchy: python_bridge:python:object:package:module:Class:. -- An identity already used by an object or a session is 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) @@ -217,15 +218,28 @@ package body python_object_pkg is -- The objects by the full names of their identities, as integers of their p_data constant p_objects : dict_t := new_dict; + function p_dots_to_colons(name : string) return string is + variable result : string(1 to name'length) := name; + begin + for idx in result'range loop + if result(idx) = '.' then + result(idx) := ':'; + end if; + 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 class_id : id_t; 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); + class_id := get_id(p_dots_to_colons(class_name), parent => anonymous_id); + object_id := get_id(to_string(num_children(class_id) + 1), parent => class_id); end if; -- The session of the object is that of its identity, which must be its own if p_has_session(object_id) then 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 index c693d5f..0e20bae 100644 --- a/src/vunit_python_bridge/hdl/tools/python_object_pkg.vhd.in +++ b/src/vunit_python_bridge/hdl/tools/python_object_pkg.vhd.in @@ -39,7 +39,8 @@ package python_object_pkg is -- 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:. + -- enumerated per class like VUnit's verification components, the class name + -- giving the hierarchy: python_bridge:python:object:package:module:Class:. -- An identity already used by an object or a session is 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) @@ -100,15 +101,28 @@ package body python_object_pkg is -- The objects by the full names of their identities, as integers of their p_data constant p_objects : dict_t := new_dict; + function p_dots_to_colons(name : string) return string is + variable result : string(1 to name'length) := name; + begin + for idx in result'range loop + if result(idx) = '.' then + result(idx) := ':'; + end if; + 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 class_id : id_t; 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); + class_id := get_id(p_dots_to_colons(class_name), parent => anonymous_id); + object_id := get_id(to_string(num_children(class_id) + 1), parent => class_id); end if; -- The session of the object is that of its identity, which must be its own if p_has_session(object_id) then diff --git a/tests/tb_python_objects.vhd b/tests/tb_python_objects.vhd index ed6f619..befaecb 100644 --- a/tests/tb_python_objects.vhd +++ b/tests/tb_python_objects.vhd @@ -84,8 +84,8 @@ begin 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); + -- Without an id, objects are enumerated per class + check_equal(full_name(get_id(object)), "python_bridge:python:object:Doubler:1"); elsif run("Test that an identity used by another object or a session fails") then mock(get_logger(duplicate_id), failure); @@ -183,8 +183,10 @@ begin check_equal(integer'(eval(other, "obj.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))); + -- Without an id, objects are enumerated per class, the class name giving the hierarchy + check_equal( + full_name(get_id(object)), "python_bridge:python:object:models:counter_model:Counter:" & 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 From 53ec570f4128951e8656206073220451bfab1134 Mon Sep 17 00:00:00 2001 From: Sebastian Abrahamsson Date: Thu, 15 Oct 2026 16:27:48 +0200 Subject: [PATCH 26/36] Return null_python_object for an identity without an object 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 --- docs/python_objects.rst | 5 +++-- .../hdl/src/python_object_pkg.vhd | 21 +++---------------- .../hdl/tools/python_object_pkg.vhd.in | 21 +++---------------- tests/tb_python_objects.vhd | 18 ++-------------- 4 files changed, 11 insertions(+), 54 deletions(-) diff --git a/docs/python_objects.rst b/docs/python_objects.rst index 509d399..65719fc 100644 --- a/docs/python_objects.rst +++ b/docs/python_objects.rst @@ -115,8 +115,9 @@ without being given it: check_true(eval_boolean(get_python_object("tb:uart_model"), "obj.is_idle()")); -Unlike ``get_logger``, it does not make one: an identity without an object is -reported as a failure on ``python_logger`` and returns ``null_python_object``. +Unlike ``get_logger``, it does not make one, since that takes the class and +the arguments of its constructor: an identity without an object returns +``null_python_object``, and the object is made with ``new_python_object``. An object is found once it is made, which for one made by another constant during elaboration depends on the order of elaboration, so get it in a process. diff --git a/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd b/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd index 22dfdbf..d663546 100644 --- a/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd +++ b/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd @@ -59,8 +59,8 @@ package python_object_pkg is return python_object_t; -- The object made with the identity, or with the identity of the full name. - -- Unlike get_logger, it does not make one: an identity without an object is - -- reported as a failure on python_logger, and null_python_object is returned. + -- Unlike get_logger, it does not make one: null_python_object is returned for + -- an identity without an object. impure function get_python_object(id : id_t) return python_object_t; impure function get_python_object(name : string) return python_object_t; @@ -272,23 +272,9 @@ package body python_object_pkg is return new_python_object(class_name, args, get_id(name)); end; - procedure p_no_object(name : string) is - begin - failure( - python_logger, - "get_python_object(""" & name & """) failed:" & LF & "No Python object has the identity " & name & - ". Make it with new_python_object or create_python_object before getting it." - ); - end; - impure function get_python_object(id : id_t) return python_object_t is begin - if id = null_id then - p_no_object("null_id"); - return null_python_object; - end if; - if not has_key(p_objects, full_name(id)) then - p_no_object(full_name(id)); + if id = null_id or not has_key(p_objects, full_name(id)) then return null_python_object; end if; return (p_data => to_integer_vector_ptr(integer'value(get_string(p_objects, full_name(id))))); @@ -298,7 +284,6 @@ package body python_object_pkg is begin -- has_id first: get_id would make the identity of a wrong name if not has_id(name) then - p_no_object(name); return null_python_object; end if; return get_python_object(get_id(name)); 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 index 0e20bae..fd68583 100644 --- a/src/vunit_python_bridge/hdl/tools/python_object_pkg.vhd.in +++ b/src/vunit_python_bridge/hdl/tools/python_object_pkg.vhd.in @@ -59,8 +59,8 @@ package python_object_pkg is return python_object_t; -- The object made with the identity, or with the identity of the full name. - -- Unlike get_logger, it does not make one: an identity without an object is - -- reported as a failure on python_logger, and null_python_object is returned. + -- Unlike get_logger, it does not make one: null_python_object is returned for + -- an identity without an object. impure function get_python_object(id : id_t) return python_object_t; impure function get_python_object(name : string) return python_object_t; @@ -155,23 +155,9 @@ package body python_object_pkg is return new_python_object(class_name, args, get_id(name)); end; - procedure p_no_object(name : string) is - begin - failure( - python_logger, - "get_python_object(""" & name & """) failed:" & LF & "No Python object has the identity " & name & - ". Make it with new_python_object or create_python_object before getting it." - ); - end; - impure function get_python_object(id : id_t) return python_object_t is begin - if id = null_id then - p_no_object("null_id"); - return null_python_object; - end if; - if not has_key(p_objects, full_name(id)) then - p_no_object(full_name(id)); + if id = null_id or not has_key(p_objects, full_name(id)) then return null_python_object; end if; return (p_data => to_integer_vector_ptr(integer'value(get_string(p_objects, full_name(id))))); @@ -181,7 +167,6 @@ package body python_object_pkg is begin -- has_id first: get_id would make the identity of a wrong name if not has_id(name) then - p_no_object(name); return null_python_object; end if; return get_python_object(get_id(name)); diff --git a/tests/tb_python_objects.vhd b/tests/tb_python_objects.vhd index befaecb..5cb367d 100644 --- a/tests/tb_python_objects.vhd +++ b/tests/tb_python_objects.vhd @@ -203,25 +203,11 @@ begin other := create_python_object("models.counter_model.Counter", kwarg("start", 6), name => "tb:created"); check_equal(integer'(call(get_python_object("tb:created"), "value")), 6); - elsif run("Test that get_python_object reports an identity without an object") then - mock(python_logger, failure); + elsif run("Test that get_python_object returns null_python_object for an identity without an object") then check(get_python_object("tb:no_object") = null_python_object, "unknown name"); - check_only_log( - python_logger, - "get_python_object(""tb:no_object"") failed:" & LF & "No Python object has the identity tb:no_object." & - " Make it with new_python_object or create_python_object before getting it.", - failure - ); check_false(has_id("tb:no_object"), "a failed lookup makes no identity"); check(get_python_object(get_id("tb:id_without_object")) = null_python_object, "identity without object"); - check_only_log( - python_logger, - "get_python_object(""tb:id_without_object"") failed:" & LF & - "No Python object has the identity tb:id_without_object." & - " Make it with new_python_object or create_python_object before getting it.", - failure - ); - unmock(python_logger); + check(get_python_object(null_id) = null_python_object, "null_id"); elsif run("Test that null_python_object is reported") then mock(python_logger, failure); From f75c5dc7ff5abed3f1be121058883b230fdc4aeb Mon Sep 17 00:00:00 2001 From: Sebastian Abrahamsson Date: Thu, 15 Oct 2026 16:32:15 +0200 Subject: [PATCH 27/36] Give verification components a constructor per use case 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 --- docs/python_objects.rst | 41 ++++++++++++++-------- examples/embedded_python/filter_vc_pkg.vhd | 23 ++++++------ tests/counter_vc_pkg.vhd | 23 ++++++------ 3 files changed, 49 insertions(+), 38 deletions(-) diff --git a/docs/python_objects.rst b/docs/python_objects.rst index 65719fc..da1f39e 100644 --- a/docs/python_objects.rst +++ b/docs/python_objects.rst @@ -167,9 +167,9 @@ 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. -Every instance then gets a backend of its own, unless the testbench gives it -one: +handle, the way VUnit's own components make everything they need there. A +second constructor takes an object instead, for a testbench that gives the +component one: .. code-block:: vhdl @@ -178,16 +178,20 @@ one: 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 + -- A UartModel of its own + 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"); - variable result : uart_master_t := (p_std_cfg => std_cfg, p_model => model); begin - if model = null_python_object then - result.p_model := new_python_object("my_vcs.uart.UartModel", kwarg("baud", baud), get_id(std_cfg)); - end if; - return result; + return ( + p_std_cfg => std_cfg, + p_model => new_python_object("my_vcs.uart.UartModel", kwarg("baud", baud), get_id(std_cfg)) + ); + end; + + -- A given model, e.g. shared with other components or the testbench + 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; The component itself only calls the object of its handle: @@ -205,9 +209,18 @@ The component itself only calls the object of its handle: 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. +nothing happens in Python until the first call. + +The identities follow from who makes the object: + +* A component making its own object gives it its own identity: the ``id`` + given, or the one ``create_std_cfg`` numbers, ``my_vcs:uart_master:1``, + ``my_vcs:uart_master:2`` and so on. The log of the component and of its + object then share a name. +* A component given an object is named by its own ``id``, or numbered, never + by the object. The object keeps the identity it was made with, since it is + given in order to be shared, with the testbench or with other components, + each with a name of its own. ``filter_vc`` of the `embedded_python example `__ diff --git a/examples/embedded_python/filter_vc_pkg.vhd b/examples/embedded_python/filter_vc_pkg.vhd index ddaa7ce..17b9afb 100644 --- a/examples/embedded_python/filter_vc_pkg.vhd +++ b/examples/embedded_python/filter_vc_pkg.vhd @@ -19,24 +19,23 @@ package filter_vc_pkg is 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; + -- A MovingAverage of its own, named by id + impure function new_filter_vc(window : positive := 2; id : id_t := null_id) return filter_vc_t; + -- A model given by the testbench, e.g. shared with it + impure function new_filter_vc(model : python_object_t) 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 - variable result : filter_vc_t := (p_model => model); + impure function new_filter_vc(window : positive := 2; id : id_t := null_id) return filter_vc_t is begin - if model = null_python_object then - result.p_model := new_python_object("filter_model.MovingAverage", kwarg("window", window), id); - end if; - return result; + return (p_model => new_python_object("filter_model.MovingAverage", kwarg("window", window), id)); + end; + + impure function new_filter_vc(model : python_object_t) return filter_vc_t is + begin + return (p_model => model); end; impure function get_model(filter : filter_vc_t) return python_object_t is diff --git a/tests/counter_vc_pkg.vhd b/tests/counter_vc_pkg.vhd index 36c2c80..177701a 100644 --- a/tests/counter_vc_pkg.vhd +++ b/tests/counter_vc_pkg.vhd @@ -18,24 +18,23 @@ package counter_vc_pkg is 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; + -- A Counter of its own, with the identity of the VC + impure function new_counter_vc(step : natural := 1; id : id_t := null_id) return counter_vc_t; + -- A given model, e.g. shared with other VCs or the testbench + impure function new_counter_vc(model : python_object_t) 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 - variable result : counter_vc_t := (p_model => model); + impure function new_counter_vc(step : natural := 1; id : id_t := null_id) return counter_vc_t is begin - if model = null_python_object then - result.p_model := new_python_object("models.counter_model.Counter", kwarg("step", step), id); - end if; - return result; + return (p_model => new_python_object("models.counter_model.Counter", kwarg("step", step), id)); + end; + + impure function new_counter_vc(model : python_object_t) return counter_vc_t is + begin + return (p_model => model); end; impure function get_model(vc : counter_vc_t) return python_object_t is From ff4ceb90cf1fdecd2b7eed336b82c8683564d665 Mon Sep 17 00:00:00 2001 From: Sebastian Abrahamsson Date: Thu, 15 Oct 2026 16:33:41 +0200 Subject: [PATCH 28/36] Give path_counter_vc a handle instead of several generics 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 --- docs/python_objects.rst | 22 ++++++++++--------- tests/path_counter_vc.vhd | 43 ++++++++++++++++++++++++++----------- tests/tb_python_objects.vhd | 6 ++++-- 3 files changed, 47 insertions(+), 24 deletions(-) diff --git a/docs/python_objects.rst b/docs/python_objects.rst index da1f39e..1921153 100644 --- a/docs/python_objects.rst +++ b/docs/python_objects.rst @@ -230,26 +230,28 @@ 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: +A component can also take the path of its instance, ``'path_name``, as the +identity when its handle has no ``id``. The object is then made in the +statement part of a process rather than in the constructor: 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 := model; + variable vc_id : id_t := vc.p_id; + variable backend : python_object_t; begin if vc_id = null_id then vc_id := get_id(path_counter_vc'path_name); end if; - if backend = null_python_object then - backend := new_python_object("models.counter_model.Counter", kwarg("step", step), vc_id); - end if; + backend := new_python_object("models.counter_model.Counter", kwarg("step", vc.p_step), vc_id); ... +The testbench reaches such an object by the path of the instance, for example +``get_python_object("tb:counter_inst")``. + 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 memory model written by a bus slave and read back by the test is a typical diff --git a/tests/path_counter_vc.vhd b/tests/path_counter_vc.vhd index 1d98379..9678bae 100644 --- a/tests/path_counter_vc.vhd +++ b/tests/path_counter_vc.vhd @@ -4,8 +4,8 @@ -- -- 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. +-- A verification component whose behaviour is a Python object made by the +-- component: its identity is the id of its handle or its instance path. library vunit_lib; context vunit_lib.vunit_context; @@ -13,13 +13,34 @@ context vunit_lib.vunit_context; library python_bridge; context python_bridge.python_context; +package path_counter_vc_pkg is + type path_counter_vc_t is record + p_step : natural; + p_id : id_t; + end record; + + -- The identity of the VC is id, or its instance path when null_id + impure function new_path_counter_vc(step : natural := 1; id : id_t := null_id) return path_counter_vc_t; +end package; + +package body path_counter_vc_pkg is + impure function new_path_counter_vc(step : natural := 1; id : id_t := null_id) return path_counter_vc_t is + begin + return (p_step => step, p_id => id); + end; +end package body; + +library vunit_lib; +context vunit_lib.vunit_context; + +library python_bridge; +context python_bridge.python_context; + +use work.path_counter_vc_pkg.all; + 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 + vc : path_counter_vc_t ); port( tick : in natural; @@ -30,17 +51,15 @@ end entity; architecture python of path_counter_vc is begin process - variable vc_id : id_t := id; - variable backend : python_object_t := model; + variable vc_id : id_t := vc.p_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; - if backend = null_python_object then - backend := new_python_object("models.counter_model.Counter", kwarg("step", step), vc_id); - end if; + backend := new_python_object("models.counter_model.Counter", kwarg("step", vc.p_step), vc_id); loop wait on tick; diff --git a/tests/tb_python_objects.vhd b/tests/tb_python_objects.vhd index 5cb367d..b20df09 100644 --- a/tests/tb_python_objects.vhd +++ b/tests/tb_python_objects.vhd @@ -17,6 +17,7 @@ library python_bridge; context python_bridge.python_context; use work.counter_vc_pkg.all; +use work.path_counter_vc_pkg.all; entity tb_python_objects is generic(runner_cfg : string); @@ -53,6 +54,7 @@ begin 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(get_python_object("tb_python_objects:vc_d"), "value")), 9, "vc_d by its path"); check_equal(integer'(call(get_python_object("vc_a"), "value")), 3, "the model of vc_a by its identity"); check_equal(integer'(call(shared_model, "value")), 103); check_equal(eval_string(shared_model, "type(obj).__name__"), "Counter"); @@ -260,10 +262,10 @@ begin count => count_c ); - -- Without a handle: the instance path is the identity + -- A handle without an id: the instance path is the identity vc_d : entity work.path_counter_vc generic map ( - step => 3 + vc => new_path_counter_vc(step => 3) ) port map ( tick => tick, From becea547b34609faa5483f34cfa950a3c1e639a6 Mon Sep 17 00:00:00 2001 From: Sebastian Abrahamsson Date: Thu, 15 Oct 2026 16:37:10 +0200 Subject: [PATCH 29/36] Use the named eval and call forms, adding the alias call_integer 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 --- docs/python_objects.rst | 2 +- docs/running_code.rst | 3 +- .../hdl/src/python_object_pkg.vhd | 3 ++ .../hdl/src/python_pkg.vhd | 2 ++ .../hdl/tools/generate_python_pkg.py | 17 +++++++--- .../hdl/tools/python_pkg.vhd.in | 2 ++ tests/tb_python_objects.vhd | 32 +++++++++---------- tests/tb_python_pkg_bridge.vhd | 1 + 8 files changed, 40 insertions(+), 22 deletions(-) diff --git a/docs/python_objects.rst b/docs/python_objects.rst index 1921153..13ac0ac 100644 --- a/docs/python_objects.rst +++ b/docs/python_objects.rst @@ -273,7 +273,7 @@ case: ... - check_equal(integer'(call(memory, "read", arg(16#100#))), 16#cafe#); + check_equal(call_integer(memory, "read", arg(16#100#)), 16#cafe#); In the same way, a component can let its user choose the class, by taking its name as a ``string`` generic. diff --git a/docs/running_code.rst b/docs/running_code.rst index ed3b1de..8eff6d9 100644 --- a/docs/running_code.rst +++ b/docs/running_code.rst @@ -205,7 +205,8 @@ On NVC, GHDL and Questa, ``call`` returns the same types as ``eval``: ``call_integer_vector_ptr``, plus the procedures ``call_std_ulogic_vector``, ``call_signed`` and ``call_unsigned`` taking the result as an ``out`` parameter. All of the functions but ``call_std_ulogic_vector`` are aliased -``call``. +``call``. The ``integer`` one, ``call_integer_w_arg``, is also aliased +``call_integer``, like ``eval_integer``. ``to_call_str`` builds the Python call expression itself, as a string, which is useful to embed a call inside a larger ``exec``/``eval`` string: diff --git a/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd b/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd index d663546..031bd93 100644 --- a/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd +++ b/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd @@ -86,6 +86,9 @@ package python_object_pkg is 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]; + alias call_integer 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; diff --git a/src/vunit_python_bridge/hdl/src/python_pkg.vhd b/src/vunit_python_bridge/hdl/src/python_pkg.vhd index aba7e58..4a8db07 100644 --- a/src/vunit_python_bridge/hdl/src/python_pkg.vhd +++ b/src/vunit_python_bridge/hdl/src/python_pkg.vhd @@ -112,6 +112,8 @@ package python_pkg is ) return integer; alias call is call_integer_w_arg[ string, arg_t, arg_t, arg_t, arg_t, arg_t, arg_t, arg_t, arg_t, arg_t, arg_t, python_session_t return integer]; + alias call_integer is call_integer_w_arg[ + string, arg_t, arg_t, arg_t, arg_t, arg_t, arg_t, arg_t, arg_t, arg_t, arg_t, python_session_t return integer]; procedure call( identifier : string; arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg; 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 b8baf68..20437d4 100644 --- a/src/vunit_python_bridge/hdl/tools/generate_python_pkg.py +++ b/src/vunit_python_bridge/hdl/tools/generate_python_pkg.py @@ -202,6 +202,15 @@ def call_name(result): return "call_integer_w_arg" if result["name"] == "integer" else f"call_{result['name']}" +def call_aliases(result): + """ + Aliases of the call function of a result type: call, and call_integer for + call_integer_w_arg, named like the other results, as in python_pkg.vhd.in. + """ + aliases = ["call"] if result.get("alias_call", True) else [] + return aliases + (["call_integer"] if result["name"] == "integer" else []) + + # ------------------------------------------------------------------------- # arg and kwarg # ------------------------------------------------------------------------- @@ -322,8 +331,8 @@ def call_declarations(): lines.append(f" identifier : string; {ARG_PARAMETERS};") lines.append(f" {SESSION}") lines.append(f" ) return {result['vhdl']};") - if result.get("alias_call", True): - lines.append(f" alias call is {name}[") + for alias in call_aliases(result): + lines.append(f" alias {alias} is {name}[") lines.append(f" {ARG_SIGNATURE} return {result['vhdl']}];") lines.append("") for result in PROCEDURE_RESULTS: @@ -402,10 +411,10 @@ def object_declarations(): lines.append(f" {OBJECT}; method : string;") lines.append(f" {ARG_PARAMETERS}") lines.append(f" ) return {result['vhdl']};") - if result.get("alias_call", True): + for alias in call_aliases(result): # The signature over two lines, to keep them short signature = OBJECT_ARG_SIGNATURE.split(", ") - lines.append(f" alias call is {name}[") + lines.append(f" alias {alias} is {name}[") lines.append(f" {', '.join(signature[:7])},") lines.append(f" {', '.join(signature[7:])} return {result['vhdl']}];") lines.append("") 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 3b97fec..1622a38 100644 --- a/src/vunit_python_bridge/hdl/tools/python_pkg.vhd.in +++ b/src/vunit_python_bridge/hdl/tools/python_pkg.vhd.in @@ -101,6 +101,8 @@ $arg_declarations ) return integer; alias call is call_integer_w_arg[ string, arg_t, arg_t, arg_t, arg_t, arg_t, arg_t, arg_t, arg_t, arg_t, arg_t, python_session_t return integer]; + alias call_integer is call_integer_w_arg[ + string, arg_t, arg_t, arg_t, arg_t, arg_t, arg_t, arg_t, arg_t, arg_t, arg_t, python_session_t return integer]; procedure call( identifier : string; arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg; diff --git a/tests/tb_python_objects.vhd b/tests/tb_python_objects.vhd index b20df09..ec9c6b9 100644 --- a/tests/tb_python_objects.vhd +++ b/tests/tb_python_objects.vhd @@ -54,19 +54,19 @@ begin 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(get_python_object("tb_python_objects:vc_d"), "value")), 9, "vc_d by its path"); - check_equal(integer'(call(get_python_object("vc_a"), "value")), 3, "the model of vc_a by its identity"); - check_equal(integer'(call(shared_model, "value")), 103); + check_equal(call_integer(get_python_object("tb_python_objects:vc_d"), "value"), 9, "vc_d by its path"); + check_equal(call_integer(get_python_object("vc_a"), "value"), 3, "the model of vc_a by its identity"); + check_equal(call_integer(shared_model, "value"), 103); check_equal(eval_string(shared_model, "type(obj).__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, "obj.count")), 5); - check_equal(real'(call(object, "ratio")), 1.25); + check_equal(eval_integer(object, "obj.count"), 5); + check_equal(call_real(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(call_integer_vector(object, "history") = (0, 1, 2, 3, 4, 5, 6)); check_equal(length(call_integer_array(object, "history")), 7); check_true(eval_boolean(object, "obj.count == 6")); -- The eval procedures of an object @@ -79,13 +79,13 @@ begin 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, "obj.count = 41"); - check_equal(integer'(call(object, "add", arg(1))), 42); + check_equal(call_integer(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); + check_equal(call_integer(object, "apply", arg(21)), 42); -- Without an id, objects are enumerated per class check_equal(full_name(get_id(object)), "python_bridge:python:object:Doubler:1"); @@ -128,7 +128,7 @@ begin failure ); -- Reported once, then every use fails with the reason - check_equal(integer'(call(object, "value")), integer'low); + check_equal(call_integer(object, "value"), integer'low); check_only_log( get_logger(broken_id), "call(""value"") failed:" & LF & @@ -142,7 +142,7 @@ begin 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_equal(call_integer(object, "value"), integer'low); check_log( get_logger(deferred_broken_id), "new_python_object(""models.no_such_module.Model"") failed:" & LF & @@ -181,8 +181,8 @@ begin 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, "obj.count")), 1); - check_equal(integer'(eval(other, "obj.count")), 2); + check_equal(eval_integer(object, "obj.count"), 1); + check_equal(eval_integer(other, "obj.count"), 2); exec(object, "shared = 'object'"); check_false(eval_boolean(other, "'shared' in globals()"), "the sessions are separate"); -- Without an id, objects are enumerated per class, the class name giving the hierarchy @@ -195,7 +195,7 @@ begin 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, "obj.count = 7"); - check_equal(integer'(call(object, "value")), 7); + check_equal(call_integer(object, "value"), 7); elsif run("Test that get_python_object finds an object by its identity or full name") then object := new_python_object("models.counter_model.Counter", kwarg("start", 5), name => "tb:by_name"); @@ -203,7 +203,7 @@ begin check(get_python_object("tb:by_name") = object, "by full name"); check(get_python_object(get_id("tb:by_name")) = object, "by identity"); other := create_python_object("models.counter_model.Counter", kwarg("start", 6), name => "tb:created"); - check_equal(integer'(call(get_python_object("tb:created"), "value")), 6); + check_equal(call_integer(get_python_object("tb:created"), "value"), 6); elsif run("Test that get_python_object returns null_python_object for an identity without an object") then check(get_python_object("tb:no_object") = null_python_object, "unknown name"); @@ -215,7 +215,7 @@ begin 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_equal(call_integer(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); @@ -227,7 +227,7 @@ begin other := new_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); + check_equal(call_integer(object, "add", arg(1)), 1); end if; end loop; diff --git a/tests/tb_python_pkg_bridge.vhd b/tests/tb_python_pkg_bridge.vhd index 2842016..ec9aabf 100644 --- a/tests/tb_python_pkg_bridge.vhd +++ b/tests/tb_python_pkg_bridge.vhd @@ -172,6 +172,7 @@ begin ); check_equal(integer'(call("double", arg(21))), 42); check_equal(integer'(call("double", arg(-5))), -10); + check_equal(call_integer("double", arg(4)), 8, "the call_integer alias"); elsif run("Test that + joins source lines") then check_equal(string'("a" + "b"), "a" & LF & "b"); From 3ee834236d75b6faf1ed8f80cf718684efccf59c Mon Sep 17 00:00:00 2001 From: Sebastian Abrahamsson Date: Thu, 15 Oct 2026 16:42:31 +0200 Subject: [PATCH 30/36] Add function forms of the vector results sized like to_unsigned 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 --- docs/python_objects.rst | 3 +- docs/running_code.rst | 10 +- .../hdl/src/python_object_pkg.vhd | 170 ++++++++++++++++ .../hdl/src/python_pkg.vhd | 184 ++++++++++++++++++ .../hdl/tools/generate_python_pkg.py | 91 +++++++++ tests/tb_python_objects.vhd | 8 + tests/tb_python_pkg_bridge.vhd | 11 ++ 7 files changed, 473 insertions(+), 4 deletions(-) diff --git a/docs/python_objects.rst b/docs/python_objects.rst index 13ac0ac..d64438d 100644 --- a/docs/python_objects.rst +++ b/docs/python_objects.rst @@ -43,7 +43,8 @@ the object. of the object, where the object is ``obj``. 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. +result as an ``out`` parameter and the function forms taking its length or +``size_res``, for example ``byte := call_unsigned(model, "pop", byte'length)``. new_python_object or create_python_object ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/running_code.rst b/docs/running_code.rst index 8eff6d9..ae266a6 100644 --- a/docs/running_code.rst +++ b/docs/running_code.rst @@ -115,8 +115,9 @@ since that would make ``check_equal(eval("17"), 17)`` and as the procedures ``eval_std_ulogic_vector``, ``eval_signed`` and ``eval_unsigned``, that take the result as an ``out`` parameter. The width is given by the actual, and a Python value that does not fit that width is an -error. ``signed`` and ``unsigned`` results are only available in the -procedure form, since a function cannot know the width of the result. +error. Functions of the same names take the width like ``to_unsigned`` does, +as a length or as the length of ``size_res``, and return a vector of +``'U'`` on an error. .. code-block:: vhdl @@ -125,6 +126,8 @@ procedure form, since a function cannot know the width of the result. begin eval_std_ulogic_vector("format(value, '08b')", byte); eval_signed("model.level()", level); + level := eval_signed("model.level()", 16); + level := eval_signed("model.level()", level); -- size_res call, arg, kwarg and to_call_str --------------------------------- @@ -204,7 +207,8 @@ On NVC, GHDL and Questa, ``call`` returns the same types as ``eval``: ``call_integer_array``, ``call_string``, ``call_real_vector`` and ``call_integer_vector_ptr``, plus the procedures ``call_std_ulogic_vector``, ``call_signed`` and ``call_unsigned`` taking the result as an ``out`` -parameter. All of the functions but ``call_std_ulogic_vector`` are aliased +parameter, and the functions of the same names taking a length or +``size_res`` after the identifier, like ``eval``. All of the functions but ``call_std_ulogic_vector`` are aliased ``call``. The ``integer`` one, ``call_integer_w_arg``, is also aliased ``call_integer``, like ``eval_integer``. diff --git a/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd b/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd index 031bd93..ed6ddba 100644 --- a/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd +++ b/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd @@ -194,6 +194,50 @@ package python_object_pkg is 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); + + -- Like the procedures, the length given or that of size_res, like to_unsigned + impure function call_std_ulogic_vector( + object : python_object_t; method : string; length : natural; + arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg + ) return std_ulogic_vector; + impure function call_std_ulogic_vector( + object : python_object_t; method : string; size_res : std_ulogic_vector; + arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg + ) return std_ulogic_vector; + impure function eval_std_ulogic_vector( + object : python_object_t; expr : string; length : natural + ) return std_ulogic_vector; + impure function eval_std_ulogic_vector( + object : python_object_t; expr : string; size_res : std_ulogic_vector + ) return std_ulogic_vector; + impure function call_signed( + object : python_object_t; method : string; length : natural; + arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg + ) return signed; + impure function call_signed( + object : python_object_t; method : string; size_res : signed; + arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg + ) return signed; + impure function eval_signed( + object : python_object_t; expr : string; length : natural + ) return signed; + impure function eval_signed( + object : python_object_t; expr : string; size_res : signed + ) return signed; + impure function call_unsigned( + object : python_object_t; method : string; length : natural; + arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg + ) return unsigned; + impure function call_unsigned( + object : python_object_t; method : string; size_res : unsigned; + arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg + ) return unsigned; + impure function eval_unsigned( + object : python_object_t; expr : string; length : natural + ) return unsigned; + impure function eval_unsigned( + object : python_object_t; expr : string; size_res : unsigned + ) return unsigned; end package; package body python_object_pkg is @@ -719,4 +763,130 @@ package body python_object_pkg is end if; eval_unsigned(expr, result, session => get_session(object)); end; + + impure function call_std_ulogic_vector( + object : python_object_t; method : string; length : natural; + arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg + ) return std_ulogic_vector is + variable result : std_ulogic_vector(length - 1 downto 0); + begin + call_std_ulogic_vector( + object, method, result, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 + ); + return result; + end; + + impure function call_std_ulogic_vector( + object : python_object_t; method : string; size_res : std_ulogic_vector; + arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg + ) return std_ulogic_vector is + begin + return call_std_ulogic_vector( + object, method, size_res'length, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 + ); + end; + + impure function eval_std_ulogic_vector( + object : python_object_t; expr : string; length : natural + ) return std_ulogic_vector is + variable result : std_ulogic_vector(length - 1 downto 0); + begin + eval_std_ulogic_vector( + object, expr, result + ); + return result; + end; + + impure function eval_std_ulogic_vector( + object : python_object_t; expr : string; size_res : std_ulogic_vector + ) return std_ulogic_vector is + begin + return eval_std_ulogic_vector( + object, expr, size_res'length + ); + end; + + impure function call_signed( + object : python_object_t; method : string; length : natural; + arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg + ) return signed is + variable result : signed(length - 1 downto 0); + begin + call_signed( + object, method, result, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 + ); + return result; + end; + + impure function call_signed( + object : python_object_t; method : string; size_res : signed; + arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg + ) return signed is + begin + return call_signed( + object, method, size_res'length, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 + ); + end; + + impure function eval_signed( + object : python_object_t; expr : string; length : natural + ) return signed is + variable result : signed(length - 1 downto 0); + begin + eval_signed( + object, expr, result + ); + return result; + end; + + impure function eval_signed( + object : python_object_t; expr : string; size_res : signed + ) return signed is + begin + return eval_signed( + object, expr, size_res'length + ); + end; + + impure function call_unsigned( + object : python_object_t; method : string; length : natural; + arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg + ) return unsigned is + variable result : unsigned(length - 1 downto 0); + begin + call_unsigned( + object, method, result, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 + ); + return result; + end; + + impure function call_unsigned( + object : python_object_t; method : string; size_res : unsigned; + arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg + ) return unsigned is + begin + return call_unsigned( + object, method, size_res'length, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 + ); + end; + + impure function eval_unsigned( + object : python_object_t; expr : string; length : natural + ) return unsigned is + variable result : unsigned(length - 1 downto 0); + begin + eval_unsigned( + object, expr, result + ); + return result; + end; + + impure function eval_unsigned( + object : python_object_t; expr : string; size_res : unsigned + ) return unsigned is + begin + return eval_unsigned( + object, expr, size_res'length + ); + 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 4a8db07..a5a8645 100644 --- a/src/vunit_python_bridge/hdl/src/python_pkg.vhd +++ b/src/vunit_python_bridge/hdl/src/python_pkg.vhd @@ -203,6 +203,26 @@ package python_pkg is expr : string; result : out unsigned; session : python_session_t := default_session ); + -- Like the procedures, the length given or that of size_res, like to_unsigned + impure function eval_std_ulogic_vector( + expr : string; length : natural; session : python_session_t := default_session + ) return std_ulogic_vector; + impure function eval_std_ulogic_vector( + expr : string; size_res : std_ulogic_vector; session : python_session_t := default_session + ) return std_ulogic_vector; + impure function eval_signed( + expr : string; length : natural; session : python_session_t := default_session + ) return signed; + impure function eval_signed( + expr : string; size_res : signed; session : python_session_t := default_session + ) return signed; + impure function eval_unsigned( + expr : string; length : natural; session : python_session_t := default_session + ) return unsigned; + impure function eval_unsigned( + expr : string; size_res : unsigned; session : python_session_t := default_session + ) return unsigned; + ----------------------------------------------------------------------------- -- Results of call: string, boolean, std_ulogic, vectors and arrays ----------------------------------------------------------------------------- @@ -269,6 +289,38 @@ package python_pkg is session : python_session_t := default_session ); + -- Like the procedures, the length given or that of size_res, like to_unsigned + impure function call_std_ulogic_vector( + identifier : string; length : natural; + arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg; + session : python_session_t := default_session + ) return std_ulogic_vector; + impure function call_std_ulogic_vector( + identifier : string; size_res : std_ulogic_vector; + arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg; + session : python_session_t := default_session + ) return std_ulogic_vector; + impure function call_signed( + identifier : string; length : natural; + arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg; + session : python_session_t := default_session + ) return signed; + impure function call_signed( + identifier : string; size_res : signed; + arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg; + session : python_session_t := default_session + ) return signed; + impure function call_unsigned( + identifier : string; length : natural; + arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg; + session : python_session_t := default_session + ) return unsigned; + impure function call_unsigned( + identifier : string; size_res : unsigned; + arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg; + session : python_session_t := default_session + ) return unsigned; + ----------------------------------------------------------------------------- -- Execution of Python files ----------------------------------------------------------------------------- @@ -1037,6 +1089,66 @@ package body python_pkg is end if; end; + impure function eval_std_ulogic_vector( + expr : string; length : natural; session : python_session_t := default_session + ) return std_ulogic_vector is + variable result : std_ulogic_vector(length - 1 downto 0); + begin + eval_std_ulogic_vector( + expr, result, session + ); + return result; + end; + + impure function eval_std_ulogic_vector( + expr : string; size_res : std_ulogic_vector; session : python_session_t := default_session + ) return std_ulogic_vector is + begin + return eval_std_ulogic_vector( + expr, size_res'length, session + ); + end; + + impure function eval_signed( + expr : string; length : natural; session : python_session_t := default_session + ) return signed is + variable result : signed(length - 1 downto 0); + begin + eval_signed( + expr, result, session + ); + return result; + end; + + impure function eval_signed( + expr : string; size_res : signed; session : python_session_t := default_session + ) return signed is + begin + return eval_signed( + expr, size_res'length, session + ); + end; + + impure function eval_unsigned( + expr : string; length : natural; session : python_session_t := default_session + ) return unsigned is + variable result : unsigned(length - 1 downto 0); + begin + eval_unsigned( + expr, result, session + ); + return result; + end; + + impure function eval_unsigned( + expr : string; size_res : unsigned; session : python_session_t := default_session + ) return unsigned is + begin + return eval_unsigned( + expr, size_res'length, session + ); + end; + ----------------------------------------------------------------------------- -- call ----------------------------------------------------------------------------- @@ -1142,4 +1254,76 @@ package body python_pkg is to_call_str(identifier, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10), result, session ); end; + + impure function call_std_ulogic_vector( + identifier : string; length : natural; + arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg; + session : python_session_t := default_session + ) return std_ulogic_vector is + variable result : std_ulogic_vector(length - 1 downto 0); + begin + call_std_ulogic_vector( + identifier, result, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session + ); + return result; + end; + + impure function call_std_ulogic_vector( + identifier : string; size_res : std_ulogic_vector; + arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg; + session : python_session_t := default_session + ) return std_ulogic_vector is + begin + return call_std_ulogic_vector( + identifier, size_res'length, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session + ); + end; + + impure function call_signed( + identifier : string; length : natural; + arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg; + session : python_session_t := default_session + ) return signed is + variable result : signed(length - 1 downto 0); + begin + call_signed( + identifier, result, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session + ); + return result; + end; + + impure function call_signed( + identifier : string; size_res : signed; + arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg; + session : python_session_t := default_session + ) return signed is + begin + return call_signed( + identifier, size_res'length, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session + ); + end; + + impure function call_unsigned( + identifier : string; length : natural; + arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg; + session : python_session_t := default_session + ) return unsigned is + variable result : unsigned(length - 1 downto 0); + begin + call_unsigned( + identifier, result, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session + ); + return result; + end; + + impure function call_unsigned( + identifier : string; size_res : unsigned; + arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg; + session : python_session_t := default_session + ) return unsigned is + begin + return call_unsigned( + identifier, size_res'length, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, session + ); + end; end package body; 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 20437d4..cf7c942 100644 --- a/src/vunit_python_bridge/hdl/tools/generate_python_pkg.py +++ b/src/vunit_python_bridge/hdl/tools/generate_python_pkg.py @@ -194,6 +194,49 @@ ] +def sized_declarations(prefix, name, vhdl, extra=""): + """ + Declarations of the sized function forms of a procedure result, like to_unsigned: + the length given, or the length of size_res. + """ + return [ + f" impure function {name}(", + f" {prefix}length : natural{extra}", + f" ) return {vhdl};", + f" impure function {name}(", + f" {prefix}size_res : {vhdl}{extra}", + f" ) return {vhdl};", + ] + + +def sized_bodies(prefix, name, vhdl, procedure_actuals, size_res_actuals, extra=""): + """ + Bodies of the sized function forms of a procedure result. They call the + procedure, so a failure returns the vector of the length with 'U' elements. + """ + return f"""\ + impure function {name}( + {prefix}length : natural{extra} + ) return {vhdl} is + variable result : {vhdl}(length - 1 downto 0); + begin + {name}( + {procedure_actuals} + ); + return result; + end; + + impure function {name}( + {prefix}size_res : {vhdl}{extra} + ) return {vhdl} is + begin + return {name}( + {size_res_actuals} + ); + end; +""" + + def call_name(result): """ Name of the call function of a result type. The integer one is named @@ -275,6 +318,9 @@ def eval_declarations(): lines.append(f" procedure eval_{result['name']}(") lines.append(f" expr : string; result : out {result['vhdl']}; {SESSION}") lines.append(" );") + lines += ["", " -- Like the procedures, the length given or that of size_res, like to_unsigned"] + for result in PROCEDURE_RESULTS: + lines += sized_declarations("expr : string; ", f"eval_{result['name']}", result["vhdl"], f"; {SESSION}") return "\n".join(lines) @@ -312,6 +358,13 @@ def eval_subprograms(): end; """ ) + for result in PROCEDURE_RESULTS: + parts.append( + sized_bodies( + "expr : string; ", f"eval_{result['name']}", result["vhdl"], + "expr, result, session", "expr, size_res'length, session", f"; {SESSION}", + ) + ) return "\n".join(parts).rstrip("\n") @@ -341,6 +394,14 @@ def call_declarations(): lines.append(f" {ARG_PARAMETERS};") lines.append(f" {SESSION}") lines.append(" );") + lines += ["", " -- Like the procedures, the length given or that of size_res, like to_unsigned"] + for result in PROCEDURE_RESULTS: + lines += sized_declarations( + "identifier : string; ", + f"call_{result['name']}", + result["vhdl"], + f";\n {ARG_PARAMETERS};\n {SESSION}", + ) return "\n".join(lines) @@ -382,6 +443,15 @@ def call_subprograms(): end; """ ) + for result in PROCEDURE_RESULTS: + parts.append( + sized_bodies( + "identifier : string; ", f"call_{result['name']}", result["vhdl"], + f"identifier, result, {ARG_ACTUALS}, session", + f"identifier, size_res'length, {ARG_ACTUALS}, session", + f";\n {ARG_PARAMETERS};\n {SESSION}", + ) + ) return "\n".join(parts).rstrip("\n") @@ -430,6 +500,12 @@ def object_declarations(): 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']});") + lines += ["", " -- Like the procedures, the length given or that of size_res, like to_unsigned"] + for result in PROCEDURE_RESULTS: + lines += sized_declarations( + f"{OBJECT}; method : string; ", f"call_{result['name']}", result["vhdl"], f";\n {ARG_PARAMETERS}" + ) + lines += sized_declarations(f"{OBJECT}; expr : string; ", f"eval_{result['name']}", result["vhdl"]) return "\n".join(lines) @@ -526,6 +602,21 @@ def object_subprograms(): end; """ ) + for result in PROCEDURE_RESULTS: + parts.append( + sized_bodies( + f"{OBJECT}; method : string; ", f"call_{result['name']}", result["vhdl"], + f"object, method, result, {ARG_ACTUALS}", + f"object, method, size_res'length, {ARG_ACTUALS}", + f";\n {ARG_PARAMETERS}", + ) + ) + parts.append( + sized_bodies( + f"{OBJECT}; expr : string; ", f"eval_{result['name']}", result["vhdl"], + "object, expr, result", "object, expr, size_res'length", + ) + ) return "\n".join(parts).rstrip("\n") diff --git a/tests/tb_python_objects.vhd b/tests/tb_python_objects.vhd index ec9c6b9..35e874a 100644 --- a/tests/tb_python_objects.vhd +++ b/tests/tb_python_objects.vhd @@ -75,6 +75,14 @@ begin eval_std_ulogic_vector(object, "'0000' + '1000'", bits); check(bits = "00001000", "eval_std_ulogic_vector"); + -- The sized function forms, like to_unsigned + check_equal(call_unsigned(object, "value", 8), to_unsigned(6, 8)); + check_equal(call_unsigned(object, "value", byte), to_unsigned(6, 8)); + check_equal(call_signed(object, "add", 8, arg(-10)), to_signed(-4, 8)); + check_equal(eval_signed(object, "obj.count", to_signed(0, 4)), to_signed(-4, 4)); + check_equal(eval_unsigned(object, "obj.count + 10", 8), to_unsigned(6, 8)); + check_equal(eval_std_ulogic_vector(object, "'0000' + '1000'", 8), std_ulogic_vector'("00001000")); + check_equal(eval_std_ulogic_vector(object, "'00000011'", bits), std_ulogic_vector'("00000011")); 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"); diff --git a/tests/tb_python_pkg_bridge.vhd b/tests/tb_python_pkg_bridge.vhd index ec9aabf..e70de9b 100644 --- a/tests/tb_python_pkg_bridge.vhd +++ b/tests/tb_python_pkg_bridge.vhd @@ -415,6 +415,17 @@ begin eval_unsigned("2 ** 8 - 1", u8); check_equal(u8, to_unsigned(255, 8)); + elsif run("Test the sized function forms of the vector results") then + exec("def identity(x):" + " return x"); + check_equal(call_signed("identity", 8, arg(-128)), to_signed(-128, 8)); + check_equal(call_unsigned("identity", u8, arg(255)), to_unsigned(255, 8)); + check_equal(call_std_ulogic_vector("identity", 4, arg("1010")), std_ulogic_vector'("1010")); + check_equal(call_std_ulogic_vector("identity", slv4, arg("0110")), std_ulogic_vector'("0110")); + check_equal(eval_signed("-1", s8), to_signed(-1, 8)); + check_equal(eval_unsigned("2 ** 8 - 1", 8), to_unsigned(255, 8)); + check_equal(eval_std_ulogic_vector("'01'", 2), std_ulogic_vector'("01")); + check_equal(eval_std_ulogic_vector("'0011'", slv4), std_ulogic_vector'("0011")); + elsif run("Test integer_vector and real_vector results") then exec("def identity(x):" + " return x"); reals := call_real_vector("identity", arg(real_vector'(1.5, -0.25, 0.0))); From cd9b41751ea66bb12db1de6a4e076f1d66c91a6d Mon Sep 17 00:00:00 2001 From: Sebastian Abrahamsson Date: Thu, 15 Oct 2026 16:48:19 +0200 Subject: [PATCH 31/36] Leave the result of a failed sized function out of the docs A failure stops the test before the function returns. Co-Authored-By: Claude Opus 5.5 --- docs/running_code.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/running_code.rst b/docs/running_code.rst index ae266a6..3ae2803 100644 --- a/docs/running_code.rst +++ b/docs/running_code.rst @@ -116,8 +116,7 @@ as the procedures ``eval_std_ulogic_vector``, ``eval_signed`` and ``eval_unsigned``, that take the result as an ``out`` parameter. The width is given by the actual, and a Python value that does not fit that width is an error. Functions of the same names take the width like ``to_unsigned`` does, -as a length or as the length of ``size_res``, and return a vector of -``'U'`` on an error. +as a length or as the length of ``size_res``. .. code-block:: vhdl From 6ffcc52f92f37136c9c5755116aefe21bc223e1d Mon Sep 17 00:00:00 2001 From: Sebastian Abrahamsson Date: Thu, 15 Oct 2026 17:01:00 +0200 Subject: [PATCH 32/36] Align the docs and comments of Python objects with the latest changes - 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 --- docs/python_objects.rst | 14 +++++++------- docs/running_code.rst | 7 ++++--- examples/embedded_python/tb_example.vhd | 4 +++- src/vunit_python_bridge/hdl/src/python_pkg.vhd | 5 +++-- .../hdl/tools/python_pkg.vhd.in | 5 +++-- tests/counter_vc_pkg.vhd | 3 ++- tests/tb_python_objects.vhd | 2 +- 7 files changed, 23 insertions(+), 17 deletions(-) diff --git a/docs/python_objects.rst b/docs/python_objects.rst index d64438d..589abcd 100644 --- a/docs/python_objects.rst +++ b/docs/python_objects.rst @@ -23,7 +23,7 @@ own, and a testbench can create one and give it to a component. check_true(eval_boolean(model, "obj.is_idle()")); exec(model, "obj.trace = True"); -``new_python_object(class_name, args, id)`` names the class and the arguments +``new_python_object(class_name, args, id)``, or ``name`` instead of ``id``, names the class and the arguments of its constructor, given like the arguments of ``call``, including groups made with ``&``. The class is @@ -95,8 +95,8 @@ The ``id`` of an object names its session and its logger, optional and mainly makes the log readable. Without an ``id``, objects are enumerated per class, the way VUnit enumerates verification components, with the class name as the hierarchy: -``new_python_object("models.uart.UartModel")`` gives -``python_bridge:python:object:models:uart:UartModel:1``, then ``…:2`` and so +``new_python_object("my_models.uart.UartModel")`` gives +``python_bridge:python:object:my_models:uart:UartModel:1``, then ``…: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 @@ -104,9 +104,9 @@ the logger of the identity and returns ``null_python_object``. Using ``null_python_object`` is reported as a failure on ``python_logger``. The identity can also be given by its full name, like ``get_logger(name)``: -``new_python_object("models.Uart", name => "tb:uart_model")`` is -``new_python_object("models.Uart", id => get_id("tb:uart_model"))``, and so -for ``create_python_object``. +``new_python_object("my_models.uart.UartModel", name => "tb:uart_model")`` is +``new_python_object("my_models.uart.UartModel", id => get_id("tb:uart_model"))``, +and so for ``create_python_object``. ``get_python_object(id)`` and ``get_python_object(name)`` return the object made with that identity, so a testbench can reach the object of a component @@ -277,7 +277,7 @@ case: check_equal(call_integer(memory, "read", arg(16#100#)), 16#cafe#); In the same way, a component can let its user choose the class, by taking its -name as a ``string`` generic. +name as an argument of the constructor of its handle. Errors and limitations ~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/running_code.rst b/docs/running_code.rst index 3ae2803..75649e0 100644 --- a/docs/running_code.rst +++ b/docs/running_code.rst @@ -207,9 +207,10 @@ On NVC, GHDL and Questa, ``call`` returns the same types as ``eval``: ``call_integer_vector_ptr``, plus the procedures ``call_std_ulogic_vector``, ``call_signed`` and ``call_unsigned`` taking the result as an ``out`` parameter, and the functions of the same names taking a length or -``size_res`` after the identifier, like ``eval``. All of the functions but ``call_std_ulogic_vector`` are aliased -``call``. The ``integer`` one, ``call_integer_w_arg``, is also aliased -``call_integer``, like ``eval_integer``. +``size_res`` after the identifier, like ``eval``. The functions without a +width but ``call_std_ulogic_vector`` are aliased ``call``. The ``integer`` +one, ``call_integer_w_arg``, is also aliased ``call_integer``, like +``eval_integer``. ``to_call_str`` builds the Python call expression itself, as a string, which is useful to embed a call inside a larger ``exec``/``eval`` string: diff --git a/examples/embedded_python/tb_example.vhd b/examples/embedded_python/tb_example.vhd index c0523a8..8983343 100644 --- a/examples/embedded_python/tb_example.vhd +++ b/examples/embedded_python/tb_example.vhd @@ -272,7 +272,7 @@ begin elsif run("Test result types of eval and call") then -- eval and call convert the Python value to the VHDL type the context expects. - -- The result types a function cannot size by itself are procedures instead. + -- The result types a function cannot size by itself are given the width. import_device_model; -- A boolean result can be used directly as a condition @@ -290,6 +290,8 @@ begin -- The procedure form takes the width of the result from the variable it writes to call_unsigned("device_model.status_word", status); check_equal(status, unsigned'(x"BEEF")); + -- The function form takes it like to_unsigned, as a length or as size_res + check_equal(call_unsigned("device_model.status_word", status'length), unsigned'(x"BEEF")); -- Strings, real vectors and dynamically sized integer vectors check_equal(call_string("device_model.status_name"), "running"); diff --git a/src/vunit_python_bridge/hdl/src/python_pkg.vhd b/src/vunit_python_bridge/hdl/src/python_pkg.vhd index a5a8645..78efcbd 100644 --- a/src/vunit_python_bridge/hdl/src/python_pkg.vhd +++ b/src/vunit_python_bridge/hdl/src/python_pkg.vhd @@ -173,8 +173,9 @@ package python_pkg is -- std_ulogic_vector and integer_array_t results are only available under -- their explicit names, not as eval overloads, which keeps -- check_equal(eval("17"), 17) and length(eval("[1, 2]")) unambiguous. - -- signed and unsigned results are only available in the procedure form - -- since a function cannot know the width of the result. + -- signed and unsigned results, like std_ulogic_vector ones of a given width, + -- are procedures writing the result, or functions given the width as a + -- length or as size_res, like to_unsigned. impure function eval_boolean( expr : string; session : python_session_t := default_session ) return boolean; 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 1622a38..c453bd0 100644 --- a/src/vunit_python_bridge/hdl/tools/python_pkg.vhd.in +++ b/src/vunit_python_bridge/hdl/tools/python_pkg.vhd.in @@ -162,8 +162,9 @@ $arg_declarations -- std_ulogic_vector and integer_array_t results are only available under -- their explicit names, not as eval overloads, which keeps -- check_equal(eval("17"), 17) and length(eval("[1, 2]")) unambiguous. - -- signed and unsigned results are only available in the procedure form - -- since a function cannot know the width of the result. + -- signed and unsigned results, like std_ulogic_vector ones of a given width, + -- are procedures writing the result, or functions given the width as a + -- length or as size_res, like to_unsigned. $eval_declarations ----------------------------------------------------------------------------- diff --git a/tests/counter_vc_pkg.vhd b/tests/counter_vc_pkg.vhd index 177701a..26e823f 100644 --- a/tests/counter_vc_pkg.vhd +++ b/tests/counter_vc_pkg.vhd @@ -5,7 +5,8 @@ -- 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. +-- the constructor creates everything, including the Python object, unless +-- the other constructor is given one. library vunit_lib; context vunit_lib.vunit_context; diff --git a/tests/tb_python_objects.vhd b/tests/tb_python_objects.vhd index 35e874a..126587c 100644 --- a/tests/tb_python_objects.vhd +++ b/tests/tb_python_objects.vhd @@ -251,7 +251,7 @@ begin count => count_a ); - -- No id: an enumerated anonymous identity + -- No id: an identity numbered per class vc_b : entity work.counter_vc generic map ( vc => new_counter_vc(step => 2) From ef3139d19e99f83136c19fd17bfefd262ca920bb Mon Sep 17 00:00:00 2001 From: Sebastian Abrahamsson Date: Thu, 15 Oct 2026 17:06:42 +0200 Subject: [PATCH 33/36] Use the model of a handle directly and drop a python_pkg caveat - 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 #16 changes. Co-Authored-By: Claude Opus 5.5 --- docs/python_objects.rst | 2 -- examples/embedded_python/filter_vc.vhd | 2 +- examples/embedded_python/filter_vc_pkg.vhd | 7 ------- tests/counter_vc.vhd | 2 +- tests/counter_vc_pkg.vhd | 7 ------- 5 files changed, 2 insertions(+), 18 deletions(-) diff --git a/docs/python_objects.rst b/docs/python_objects.rst index 589abcd..dfbdd4c 100644 --- a/docs/python_objects.rst +++ b/docs/python_objects.rst @@ -293,8 +293,6 @@ Errors and limitations ``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 diff --git a/examples/embedded_python/filter_vc.vhd b/examples/embedded_python/filter_vc.vhd index 2a7bb7b..3617f7e 100644 --- a/examples/embedded_python/filter_vc.vhd +++ b/examples/embedded_python/filter_vc.vhd @@ -34,7 +34,7 @@ begin process(clk) begin if rising_edge(clk) and valid = '1' then - y <= call(get_model(filter), "push", arg(x)); + y <= call(filter.p_model, "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 index 17b9afb..2a774f0 100644 --- a/examples/embedded_python/filter_vc_pkg.vhd +++ b/examples/embedded_python/filter_vc_pkg.vhd @@ -23,8 +23,6 @@ package filter_vc_pkg is impure function new_filter_vc(window : positive := 2; id : id_t := null_id) return filter_vc_t; -- A model given by the testbench, e.g. shared with it impure function new_filter_vc(model : python_object_t) return filter_vc_t; - - impure function get_model(filter : filter_vc_t) return python_object_t; end package; package body filter_vc_pkg is @@ -37,9 +35,4 @@ package body filter_vc_pkg is begin return (p_model => model); 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/tests/counter_vc.vhd b/tests/counter_vc.vhd index 0fdda31..314714d 100644 --- a/tests/counter_vc.vhd +++ b/tests/counter_vc.vhd @@ -27,6 +27,6 @@ begin process begin wait on tick; - count <= call(get_model(vc), "add", arg(1)); + count <= call(vc.p_model, "add", arg(1)); end process; end architecture; diff --git a/tests/counter_vc_pkg.vhd b/tests/counter_vc_pkg.vhd index 26e823f..9b9b1b8 100644 --- a/tests/counter_vc_pkg.vhd +++ b/tests/counter_vc_pkg.vhd @@ -23,8 +23,6 @@ package counter_vc_pkg is impure function new_counter_vc(step : natural := 1; id : id_t := null_id) return counter_vc_t; -- A given model, e.g. shared with other VCs or the testbench impure function new_counter_vc(model : python_object_t) return counter_vc_t; - - impure function get_model(vc : counter_vc_t) return python_object_t; end package; package body counter_vc_pkg is @@ -37,9 +35,4 @@ package body counter_vc_pkg is begin return (p_model => model); end; - - impure function get_model(vc : counter_vc_t) return python_object_t is - begin - return vc.p_model; - end; end package body; From 65b48cca449ae3f73fa3039ed9b8b7c7b9550d1f Mon Sep 17 00:00:00 2001 From: Sebastian Abrahamsson Date: Thu, 15 Oct 2026 17:10:23 +0200 Subject: [PATCH 34/36] Make the release notes news fragments Co-Authored-By: Claude Opus 5.5 --- release_notes/news.d/15.feature.1.md | 1 + release_notes/news.d/15.feature.2.md | 1 + release_notes/news.d/15.feature.md | 1 + 3 files changed, 3 insertions(+) create mode 100644 release_notes/news.d/15.feature.1.md create mode 100644 release_notes/news.d/15.feature.2.md create mode 100644 release_notes/news.d/15.feature.md diff --git a/release_notes/news.d/15.feature.1.md b/release_notes/news.d/15.feature.1.md new file mode 100644 index 0000000..fa39a6f --- /dev/null +++ b/release_notes/news.d/15.feature.1.md @@ -0,0 +1 @@ +`call_integer` is an alias of `call_integer_w_arg`, the `call` returning `integer`, named like `eval_integer` and `call_real`. diff --git a/release_notes/news.d/15.feature.2.md b/release_notes/news.d/15.feature.2.md new file mode 100644 index 0000000..cbfda5b --- /dev/null +++ b/release_notes/news.d/15.feature.2.md @@ -0,0 +1 @@ +The `std_ulogic_vector`, `signed` and `unsigned` results of `eval` and `call` are also functions taking the width like `to_unsigned`, as a length or as `size_res`: `byte := call_unsigned("model.pop", byte'length)`. diff --git a/release_notes/news.d/15.feature.md b/release_notes/news.d/15.feature.md new file mode 100644 index 0000000..452106c --- /dev/null +++ b/release_notes/news.d/15.feature.md @@ -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. From 9fdcc1e3b8886706c5a1e77f87d5e11f596563b0 Mon Sep 17 00:00:00 2001 From: Sebastian Abrahamsson Date: Thu, 15 Oct 2026 20:26:20 +0200 Subject: [PATCH 35/36] Shorten the news fragment of Python objects, drop the call_integer one 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 --- release_notes/news.d/15.feature.1.md | 1 - release_notes/news.d/15.feature.md | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 release_notes/news.d/15.feature.1.md diff --git a/release_notes/news.d/15.feature.1.md b/release_notes/news.d/15.feature.1.md deleted file mode 100644 index fa39a6f..0000000 --- a/release_notes/news.d/15.feature.1.md +++ /dev/null @@ -1 +0,0 @@ -`call_integer` is an alias of `call_integer_w_arg`, the `call` returning `integer`, named like `eval_integer` and `call_real`. diff --git a/release_notes/news.d/15.feature.md b/release_notes/news.d/15.feature.md index 452106c..bcc93ea 100644 --- a/release_notes/news.d/15.feature.md +++ b/release_notes/news.d/15.feature.md @@ -1 +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. +Python objects: `python_object_t` is an instance of a Python class owned by VHDL, whose methods VHDL calls. See "Python objects" in the user guide. From 526d295669a24ec81d9f3645a1946dcd46e9282d Mon Sep 17 00:00:00 2001 From: Sebastian Abrahamsson Date: Fri, 16 Oct 2026 14:43:13 +0200 Subject: [PATCH 36/36] Name objects without an id python_object:: The default identity was python_bridge:python:object:package:module:Class:. python_object:package.module.Class: 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 --- docs/python_objects.rst | 6 ++---- .../hdl/src/python_object_pkg.vhd | 19 ++++--------------- .../hdl/tools/python_object_pkg.vhd.in | 19 ++++--------------- tests/tb_python_objects.vhd | 8 +++----- 4 files changed, 13 insertions(+), 39 deletions(-) diff --git a/docs/python_objects.rst b/docs/python_objects.rst index dfbdd4c..07756c0 100644 --- a/docs/python_objects.rst +++ b/docs/python_objects.rst @@ -94,10 +94,8 @@ 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 per class, the way VUnit enumerates -verification components, with the class name as the hierarchy: -``new_python_object("my_models.uart.UartModel")`` gives -``python_bridge:python:object:my_models:uart:UartModel:1``, then ``…:2`` and so -on. The session of an object must +verification components: ``new_python_object("my_models.uart.UartModel")`` +gives ``python_object:my_models.uart.UartModel:1``, then ``…: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 diff --git a/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd b/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd index ed6ddba..e2db661 100644 --- a/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd +++ b/src/vunit_python_bridge/hdl/src/python_object_pkg.vhd @@ -39,8 +39,8 @@ package python_object_pkg is -- 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 per class like VUnit's verification components, the class name - -- giving the hierarchy: python_bridge:python:object:package:module:Class:. + -- enumerated per class like VUnit's verification components: + -- python_object:package.module.Class:. -- An identity already used by an object or a session is 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) @@ -246,7 +246,7 @@ package body python_object_pkg is 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); + constant anonymous_id : id_t := get_id("python_object"); -- The defaults of the call results of a failed call constant p_no_integers : integer_vector(0 downto 1) := (others => 0); @@ -265,17 +265,6 @@ package body python_object_pkg is -- The objects by the full names of their identities, as integers of their p_data constant p_objects : dict_t := new_dict; - function p_dots_to_colons(name : string) return string is - variable result : string(1 to name'length) := name; - begin - for idx in result'range loop - if result(idx) = '.' then - result(idx) := ':'; - end if; - 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 @@ -285,7 +274,7 @@ package body python_object_pkg is begin if object_id = null_id then -- Like enumerate of vc_pkg, which is only there with the verification components - class_id := get_id(p_dots_to_colons(class_name), parent => anonymous_id); + class_id := get_id(class_name, parent => anonymous_id); object_id := get_id(to_string(num_children(class_id) + 1), parent => class_id); end if; -- The session of the object is that of its identity, which must be its own 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 index fd68583..06e3d2b 100644 --- a/src/vunit_python_bridge/hdl/tools/python_object_pkg.vhd.in +++ b/src/vunit_python_bridge/hdl/tools/python_object_pkg.vhd.in @@ -39,8 +39,8 @@ package python_object_pkg is -- 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 per class like VUnit's verification components, the class name - -- giving the hierarchy: python_bridge:python:object:package:module:Class:. + -- enumerated per class like VUnit's verification components: + -- python_object:package.module.Class:. -- An identity already used by an object or a session is 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) @@ -82,7 +82,7 @@ package body python_object_pkg is 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); + constant anonymous_id : id_t := get_id("python_object"); -- The defaults of the call results of a failed call constant p_no_integers : integer_vector(0 downto 1) := (others => 0); @@ -101,17 +101,6 @@ package body python_object_pkg is -- The objects by the full names of their identities, as integers of their p_data constant p_objects : dict_t := new_dict; - function p_dots_to_colons(name : string) return string is - variable result : string(1 to name'length) := name; - begin - for idx in result'range loop - if result(idx) = '.' then - result(idx) := ':'; - end if; - 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 @@ -121,7 +110,7 @@ package body python_object_pkg is begin if object_id = null_id then -- Like enumerate of vc_pkg, which is only there with the verification components - class_id := get_id(p_dots_to_colons(class_name), parent => anonymous_id); + class_id := get_id(class_name, parent => anonymous_id); object_id := get_id(to_string(num_children(class_id) + 1), parent => class_id); end if; -- The session of the object is that of its identity, which must be its own diff --git a/tests/tb_python_objects.vhd b/tests/tb_python_objects.vhd index 126587c..7cea389 100644 --- a/tests/tb_python_objects.vhd +++ b/tests/tb_python_objects.vhd @@ -95,7 +95,7 @@ begin object := new_python_object("Doubler"); check_equal(call_integer(object, "apply", arg(21)), 42); -- Without an id, objects are enumerated per class - check_equal(full_name(get_id(object)), "python_bridge:python:object:Doubler:1"); + check_equal(full_name(get_id(object)), "python_object:Doubler:1"); elsif run("Test that an identity used by another object or a session fails") then mock(get_logger(duplicate_id), failure); @@ -193,10 +193,8 @@ begin check_equal(eval_integer(other, "obj.count"), 2); exec(object, "shared = 'object'"); check_false(eval_boolean(other, "'shared' in globals()"), "the sessions are separate"); - -- Without an id, objects are enumerated per class, the class name giving the hierarchy - check_equal( - full_name(get_id(object)), "python_bridge:python:object:models:counter_model:Counter:" & name(get_id(object)) - ); + -- Without an id, objects are enumerated per class + check_equal(full_name(get_id(object)), "python_object:models.counter_model.Counter:" & 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