From fe720673a99d14eec6c01253325b0716085af267 Mon Sep 17 00:00:00 2001 From: Sebastian Abrahamsson Date: Thu, 8 Oct 2026 19:46:04 +0200 Subject: [PATCH 1/2] Transfer integer_array_t arguments when they are first used arg and kwarg transferred an integer_array_t to Python when the argument was made, which crashed the simulator for an argument made during elaboration, before the bridge can be called. The argument now holds a copy of the array and refers to it as __vunit__.deferred() in its source code; exec and eval transfer the copy when they first send code referring to it, and use the same staged value after that. Fixes #17 Co-Authored-By: Claude Opus 5.5 --- docs/types.rst | 5 +- release_notes/unreleased.md | 1 + .../hdl/src/python_ffi_pkg_bridge.vhd | 72 ++++++++++++++++++- .../hdl/src/python_pkg.vhd | 9 +-- .../hdl/src/python_pkg_vhpi.vhd | 8 +++ .../hdl/tools/python_pkg.vhd.in | 9 +-- tests/tb_python_pkg_bridge.vhd | 31 ++++++++ 7 files changed, 120 insertions(+), 15 deletions(-) diff --git a/docs/types.rst b/docs/types.rst index 165b2ee..5fa9ae7 100644 --- a/docs/types.rst +++ b/docs/types.rst @@ -37,7 +37,10 @@ integer_array_t and NumPy An ``integer_array_t`` argument is transferred to Python and referred to by the expression as a NumPy array of dtype ``int32``, so it can be reused in -several calls. It is indexed like VUnit, so that elements correspond +several calls. The argument holds a copy of the values the array had when it +was made, which is transferred when the argument is first used. An argument +can therefore be made during elaboration, for example as a constant, before +the bridge can be called. It is indexed like VUnit, so that elements correspond directly: ============= ================================ =========================== diff --git a/release_notes/unreleased.md b/release_notes/unreleased.md index 88a2f67..1249a67 100644 --- a/release_notes/unreleased.md +++ b/release_notes/unreleased.md @@ -2,6 +2,7 @@ - **Breaking:** an `integer_array_t` is indexed in NumPy like in VUnit, `get(a, x, y, z)` being `a[x, y, z]` with shape `(width, height, depth)`, instead of `a[y, x, z]` with shape `(height, width, depth)`. This applies to arguments and returned arrays. Code indexing 2D or 3D arrays in Python must swap its first two indices, and an array from an image library, which index rows first, needs `a.swapaxes(0, 1)`. - `import_module_from_file` resolves a relative path against the directory of the testbench file, like `exec_file`, rather than against the working directory of the simulator. It must be called after `test_runner_setup`. Callers that relied on a path relative to the working directory must pass an absolute path instead. See #7. +- An `integer_array_t` argument made during elaboration, for example as a constant, crashed the simulator, since the array was transferred to Python when the argument was made. The argument now holds a copy of the values it was made with, which is transferred when it is first used. See #17. - A relative file name given to `exec_file` or `import_module_from_file` before `test_runner_setup` has completed is reported as a failure, instead of being taken from the working directory of the simulator. The new `base_path` returns a base directory set with `vunit_python_bridge.set_relative_file_base()` or `VUNIT_PYTHON_BRIDGE_FILE_BASE`, for file names that work at any time, for example in verification components. - Python 3.15 is supported, with a prebuilt bridge DLL for Windows. - The bridge library is no longer built in or copied to the VUnit output path, which may not allow executing files, but to `~/.vunit-python-bridge` in the home directory of the user, on every platform. This includes the prebuilt Windows DLLs. `VUNIT_PYTHON_BRIDGE_LIBRARY_DIR` selects another directory on every platform. When the directory cannot be written, the setup fails and asks for the variable to be set. diff --git a/src/vunit_python_bridge/hdl/src/python_ffi_pkg_bridge.vhd b/src/vunit_python_bridge/hdl/src/python_ffi_pkg_bridge.vhd index 3b2e491..d7b10c1 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 @@ -26,6 +26,7 @@ use vunit_lib.id_pkg.all; use vunit_lib.integer_array_pkg.all; use vunit_lib.integer_vector_ptr_pkg.all; use vunit_lib.logger_pkg.all; +use vunit_lib.string_ptr_pkg.all; use work.python_bridge_pkg.all; package python_ffi_pkg is @@ -178,6 +179,11 @@ package python_ffi_pkg is -- under, -1 on failure. Python source code refers to the value as -- __vunit__.staged(). impure function p_stage_array(arr : integer_array_t; operation : string) return integer; + + -- Python source code referring to a copy of arr, which is transferred when the + -- code is first executed or evaluated. An argument can thereby be made during + -- elaboration, before the bridge can be called. + impure function p_defer_array(arr : integer_array_t; operation : string) return string; end package; package body python_ffi_pkg is @@ -293,6 +299,68 @@ package body python_ffi_pkg is and p_succeeded(vpy_begin, operation, logger); end; + -- The deferred arrays of p_defer_array, by the id in their Python source code + constant deferred_array_idx : natural := 0; + constant deferred_staged_id_idx : natural := 1; + constant deferred_operation_idx : natural := 2; + constant deferred_prefix : string := "__vunit__.deferred("; + + impure function p_defer_array(arr : integer_array_t; operation : string) return string is + constant deferred : integer_vector_ptr_t := new_integer_vector_ptr(3); + begin + set(deferred, deferred_array_idx, to_integer(new_string_ptr(encode(copy(arr))))); + set(deferred, deferred_operation_idx, to_integer(new_string_ptr(operation))); + return deferred_prefix & integer'image(to_integer(deferred)) & ")"; + end; + + -- The Python source code of a deferred array, transferring it on first use + impure function p_staged_source(deferred : integer_vector_ptr_t) return string is + constant operation : string := to_string(to_string_ptr(get(deferred, deferred_operation_idx))); + variable arr : integer_array_t; + variable staged_id : integer := get(deferred, deferred_staged_id_idx); + begin + if staged_id < 1 then + arr := decode(to_string(to_string_ptr(get(deferred, deferred_array_idx)))); + staged_id := p_stage_array(arr, operation); + if staged_id < 1 then + -- p_stage_array reported the error of the transfer + return "__vunit__.error(""" & operation & " failed"")"; + end if; + set(deferred, deferred_staged_id_idx, staged_id); + deallocate(arr); + end if; + return "__vunit__.staged(" & integer'image(staged_id) & ")"; + end; + + -- text with the deferred arrays it refers to transferred + impure function p_with_staged_arrays(text : string) return string is + alias normalized : string(1 to text'length) is text; + -- textio by name, since its width would hide that of integer_array_pkg + variable result : std.textio.line := new string'(""); + variable start, stop : natural := 1; + begin + while start <= normalized'length loop + stop := start; + while stop + deferred_prefix'length - 1 <= normalized'length loop + exit when normalized(stop to stop + deferred_prefix'length - 1) = deferred_prefix; + stop := stop + 1; + end loop; + if stop + deferred_prefix'length - 1 > normalized'length then + std.textio.write(result, normalized(start to normalized'length)); + exit; + end if; + std.textio.write(result, normalized(start to stop - 1)); + start := stop + deferred_prefix'length; + stop := start; + while normalized(stop) /= ')' loop + stop := stop + 1; + end loop; + std.textio.write(result, p_staged_source(to_integer_vector_ptr(integer'value(normalized(start to stop - 1))))); + start := stop + 1; + end loop; + return result.all; + end; + impure function p_exec( text : string; is_file : integer; @@ -302,7 +370,7 @@ package body python_ffi_pkg is constant logger : logger_t := get_logger(get_id(session)); begin return p_begin(session, operation) - and p_succeeded(p_send(text), operation, logger) + and p_succeeded(p_send(p_with_staged_arrays(text)), operation, logger) and p_succeeded(vpy_execute(is_file), operation, logger); end; @@ -323,7 +391,7 @@ package body python_ffi_pkg is constant logger : logger_t := get_logger(get_id(session)); begin return p_begin(session, operation) - and p_succeeded(p_send(expr), operation, logger) + and p_succeeded(p_send(p_with_staged_arrays(expr)), operation, logger) and p_succeeded(vpy_eval(kind, width), operation, logger); end; diff --git a/src/vunit_python_bridge/hdl/src/python_pkg.vhd b/src/vunit_python_bridge/hdl/src/python_pkg.vhd index 449e4b1..3b8ed23 100644 --- a/src/vunit_python_bridge/hdl/src/python_pkg.vhd +++ b/src/vunit_python_bridge/hdl/src/python_pkg.vhd @@ -612,14 +612,11 @@ package body python_pkg is return p_to_hex_literal(std_ulogic_vector(magnitude), true); end; + -- The array is transferred when the argument is first used, which lets the + -- argument be made during elaboration impure function p_arg_value(value : integer_array_t; operation : string) return string is - constant staged_id : integer := p_stage_array(value, operation); begin - if staged_id < 1 then - -- p_stage_array reported the error of the transfer - return "__vunit__.error(" & p_quoted(operation & " failed") & ")"; - end if; - return "__vunit__.staged(" & integer'image(staged_id) & ")"; + return p_defer_array(value, operation); end; function arg(value : real_vector) return arg_t is diff --git a/src/vunit_python_bridge/hdl/src/python_pkg_vhpi.vhd b/src/vunit_python_bridge/hdl/src/python_pkg_vhpi.vhd index 0512c4a..f8be78a 100644 --- a/src/vunit_python_bridge/hdl/src/python_pkg_vhpi.vhd +++ b/src/vunit_python_bridge/hdl/src/python_pkg_vhpi.vhd @@ -136,6 +136,7 @@ package python_ffi_pkg is -- Transfer an integer_array_t to Python and return the id it is staged -- under, -1 on failure. impure function p_stage_array(arr : integer_array_t; operation : string) return integer; + impure function p_defer_array(arr : integer_array_t; operation : string) return string; end package; package body python_ffi_pkg is @@ -275,6 +276,13 @@ package body python_ffi_pkg is return null_integer_array; end; + -- integer_array_t values are not supported: the failure of p_stage_array + impure function p_defer_array(arr : integer_array_t; operation : string) return string is + constant staged_id : integer := p_stage_array(arr, operation); + begin + return "__vunit__.error(""" & operation & " failed"")"; + end; + impure function p_stage_array(arr : integer_array_t; operation : string) return integer is begin p_unsupported(operation); 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..75866d9 100644 --- a/src/vunit_python_bridge/hdl/tools/python_pkg.vhd.in +++ b/src/vunit_python_bridge/hdl/tools/python_pkg.vhd.in @@ -514,14 +514,11 @@ package body python_pkg is return p_to_hex_literal(std_ulogic_vector(magnitude), true); end; + -- The array is transferred when the argument is first used, which lets the + -- argument be made during elaboration impure function p_arg_value(value : integer_array_t; operation : string) return string is - constant staged_id : integer := p_stage_array(value, operation); begin - if staged_id < 1 then - -- p_stage_array reported the error of the transfer - return "__vunit__.error(" & p_quoted(operation & " failed") & ")"; - end if; - return "__vunit__.staged(" & integer'image(staged_id) & ")"; + return p_defer_array(value, operation); end; $arg_functions diff --git a/tests/tb_python_pkg_bridge.vhd b/tests/tb_python_pkg_bridge.vhd index 2842016..980d154 100644 --- a/tests/tb_python_pkg_bridge.vhd +++ b/tests/tb_python_pkg_bridge.vhd @@ -37,6 +37,18 @@ architecture tb of tb_python_pkg_bridge is constant unsigned_arg_error : string := "kwarg_unsigned cannot convert ""1010X010""; the value has metavalues"; constant unsigned_arg_call : string := "describe(v=__vunit__.error(""kwarg_unsigned cannot convert \""1010X010\""; the value has metavalues""))"; + + impure function one_to_four return integer_array_t is + variable values : integer_array_t := new_1d(4); + begin + for idx in 0 to 3 loop + set(values, idx, idx + 1); + end loop; + return values; + end; + + -- An integer_array_t argument made during elaboration, before the bridge can be called + constant elaborated_values : arg_t := kwarg("data", one_to_four); begin main : process constant golden : python_session_t := new_session("golden"); @@ -137,6 +149,13 @@ begin -- The same staged array used in two calls. The function modifies its -- argument in place but every use gets its own copy. + -- The argument is transferred when it is used, but has the values it was made with + procedure call_after_changing_arr(data : arg_t) is + begin + set(arr, 0, 100); + check_equal(integer'(call("total", data)), 3); + end; + procedure bump_twice(data : arg_t) is begin check_equal(integer'(call("bump", data)), 5); @@ -588,6 +607,18 @@ begin set(arr_b, 1, 20); check_equal(integer'(call("total", arg(arr), kwarg("data", arr_b))), 40); + elsif run("Test an integer_array_t argument made during elaboration") then + exec("def total(*args, **kwargs):" + " return int(sum(a.sum() for a in list(args) + list(kwargs.values())))"); + check_equal(integer'(call("total", elaborated_values)), 10); + check_equal(integer'(call("total", elaborated_values)), 10, "used again"); + + elsif run("Test that an integer_array_t argument has the values it was made with") then + exec("def total(data):" + " return int(data.sum())"); + arr := new_1d(2); + set(arr, 0, 1); + set(arr, 1, 2); + call_after_changing_arr(arg(arr)); + elsif run("Test positional and keyword arguments together") then exec("def scale(x, gain=1, offset=0):" + " return x * gain + offset"); check_equal(integer'(call("scale", arg(5), kwarg("gain", 3), kwarg("offset", 1))), 16); From bbe2ad45a70e9d9d57e4f1f36946d945a64e5fb9 Mon Sep 17 00:00:00 2001 From: Sebastian Abrahamsson Date: Thu, 15 Oct 2026 17:09:34 +0200 Subject: [PATCH 2/2] Make the release note a news fragment Co-Authored-By: Claude Opus 5.5 --- release_notes/news.d/18.bugfix.md | 1 + release_notes/unreleased.md | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 release_notes/news.d/18.bugfix.md diff --git a/release_notes/news.d/18.bugfix.md b/release_notes/news.d/18.bugfix.md new file mode 100644 index 0000000..525cc84 --- /dev/null +++ b/release_notes/news.d/18.bugfix.md @@ -0,0 +1 @@ +An `integer_array_t` argument made during elaboration, for example as a constant, crashed the simulator, since the array was transferred to Python when the argument was made. The argument now holds a copy of the values it was made with, which is transferred when it is first used. See #17. diff --git a/release_notes/unreleased.md b/release_notes/unreleased.md index 1249a67..88a2f67 100644 --- a/release_notes/unreleased.md +++ b/release_notes/unreleased.md @@ -2,7 +2,6 @@ - **Breaking:** an `integer_array_t` is indexed in NumPy like in VUnit, `get(a, x, y, z)` being `a[x, y, z]` with shape `(width, height, depth)`, instead of `a[y, x, z]` with shape `(height, width, depth)`. This applies to arguments and returned arrays. Code indexing 2D or 3D arrays in Python must swap its first two indices, and an array from an image library, which index rows first, needs `a.swapaxes(0, 1)`. - `import_module_from_file` resolves a relative path against the directory of the testbench file, like `exec_file`, rather than against the working directory of the simulator. It must be called after `test_runner_setup`. Callers that relied on a path relative to the working directory must pass an absolute path instead. See #7. -- An `integer_array_t` argument made during elaboration, for example as a constant, crashed the simulator, since the array was transferred to Python when the argument was made. The argument now holds a copy of the values it was made with, which is transferred when it is first used. See #17. - A relative file name given to `exec_file` or `import_module_from_file` before `test_runner_setup` has completed is reported as a failure, instead of being taken from the working directory of the simulator. The new `base_path` returns a base directory set with `vunit_python_bridge.set_relative_file_base()` or `VUNIT_PYTHON_BRIDGE_FILE_BASE`, for file names that work at any time, for example in verification components. - Python 3.15 is supported, with a prebuilt bridge DLL for Windows. - The bridge library is no longer built in or copied to the VUnit output path, which may not allow executing files, but to `~/.vunit-python-bridge` in the home directory of the user, on every platform. This includes the prebuilt Windows DLLs. `VUNIT_PYTHON_BRIDGE_LIBRARY_DIR` selects another directory on every platform. When the directory cannot be written, the setup fails and asks for the variable to be set.