VHDL to Python interface: python_pkg on a native bridge for NVC, GHDL and Questa - #1220
VHDL to Python interface: python_pkg on a native bridge for NVC, GHDL and Questa#1220ru551n wants to merge 18 commits into
Conversation
run_script_path(runner_cfg) gives a testbench the path of the run script, passed by the test runner through the "run script path" key of runner_cfg. check_result_t gets the accessors is_pass, get_checker, get_msg, get_log_level, get_line_num and get_file_name. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
3c2c338 to
badf7eb
Compare
VHDL testbenches execute Python code and call Python functions through python_pkg (exec, eval, call with arg and kwarg, sessions, files, NumPy arrays for integer_array_t), enabled with add_python() after add_vhdl_builtins() and used through the python_context context. The implementation for NVC, GHDL and Questa/ModelSim is the Python bridge in vunit/python_bridge: a C library embedding CPython, called through VHPIDIRECT or the FLI, compiled on first use on Linux and macOS and cached under the output path, and shipped as prebuilt MSVC DLLs per CPython version on Windows. Riviera-PRO/Active-HDL use a VHPI application built the same way. Python errors are reported through the python_logger logger. NumPy becomes a dependency of VUnit, since integer_array_t values are exchanged as NumPy arrays. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A testbench exercising python_pkg: executing code, evaluating expressions, calling functions with positional and keyword arguments, sessions, files, NumPy arrays, result types, error reporting, and a verification component whose behaviour is a Python function. Tests needing Python packages that VUnit does not depend on, and tests demonstrating error reporting, carry attributes so that they can be left out. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
python_bridge.yml tests the feature with CPython from python.org on Linux (NVC and every GHDL backend, with canaries on NVC built from master and the GHDL nightly builds), macOS and Windows, checks the package content, and builds the Windows DLLs with MSVC through the reusable python_bridge_dlls.yml, which the release job of push.yml also uses so that releases ship the DLLs while the repository holds no binaries. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
LarsAsplund
left a comment
There was a problem hiding this comment.
I'm not done reviewing but this is a start. As a general comment it looks very good with nice additions. I do see that this will be a feature with a lot of development going forward. For that reason, I think most of it should be external to the VUnit core, for example in repo vunit/python_bridge. That way it can have its own life-cycle with more frequent major releases. I have started an embryo of a package system for VUnit that should be used. See how json4vhdl became a VUnit package that can be downloaded from pypi and imported as Python package (https://pypi.org/project/vunit-json-for-vhdl)
| @@ -0,0 +1,13 @@ | |||
| VHDL testbenches can now execute Python code and call Python functions, for | |||
There was a problem hiding this comment.
It feel like this should be re-written to cover the larger picture. It feels like Claude (?) focused on the additions such as std_logic support.
There was a problem hiding this comment.
Fixed in 24035be: rewritten as an announcement of the feature itself.
| -- module device_model.py, next to this testbench | ||
| procedure import_device_model is | ||
| begin | ||
| import_module_from_file(tb_path(runner_cfg) & "device_model.py", "device_model"); |
There was a problem hiding this comment.
VUnit provides join(a, b) to concatenate path segments in a way that works regardless if the path separator is present in a or not
There was a problem hiding this comment.
Fixed in b9430da: join(tb_path(runner_cfg), "...") everywhere in the example.
| check_true(call_boolean("device_model.is_busy")); | ||
|
|
||
| -- A single bit and a vector of bits, std_logic characters on the Python side | ||
| check_equal(call_std_logic("device_model.parity"), '1', result("for the parity of the status")); |
There was a problem hiding this comment.
Other "typed" VUnit functions/procedures use _std_ulogic(_vector)
There was a problem hiding this comment.
Fixed in d7a4009: eval_std_ulogic, eval_std_ulogic_vector, call_std_ulogic, call_std_ulogic_vector and arg/kwarg(std_ulogic), with std_ulogic/std_ulogic_vector parameter and result types.
|
|
||
| -- exec_file executes the file in the namespace we have been using all along, | ||
| -- which puts counter and bump there. The file name is relative to the run script. | ||
| exec_file("bump.py"); |
There was a problem hiding this comment.
I think we should be explicit rather than implicit regarding the path. That follows the pattern we've used before. That way we don't have to decide what the best root is. tb_path is probably equally good as default.
There was a problem hiding this comment.
Fixed in 4ce146e: a relative file name is resolved from tb_path (in VHDL, from runner_state), an absolute path is used as given, and the run-script-relative resolution is gone.
| check_equal(integer'(call("bump")), 1, result("for the counter after re-executing the file")); | ||
|
|
||
| -- import_module_from_file gives us the module object, which keeps its state | ||
| import_module_from_file(tb_path(runner_cfg) & "bump.py", "bumper"); |
| "", arg1), arg2), arg3), arg4), arg5), arg6), arg7), arg8), arg9), arg10) & ")"; | ||
| end; | ||
|
|
||
| procedure call( |
There was a problem hiding this comment.
call with string arguments should be remove. This was my initial approach
There was a problem hiding this comment.
Fixed in 4d5c399: both string forms are removed; the example uses arg(...).
| function arg(value : real_vector) return arg_t is | ||
| constant text : string := p_arg_value(value, "arg"); | ||
| begin | ||
| if text = "" then |
There was a problem hiding this comment.
Should this be an error instead? If the user calls arg and it becomes a null_arg, it will completely disappear from the Python call
There was a problem hiding this comment.
Agreed and fixed in d33a067: after the failure is logged, the argument becomes __vunit__.error("<message>"), so the call raises in Python with the same message instead of proceeding with a missing argument (only observable when the logger is mocked, but that is exactly when it matters).
| end; | ||
|
|
||
| ----------------------------------------------------------------------------- | ||
| -- Keyword argument groups |
There was a problem hiding this comment.
Can positional arguments be grouped in the same way?
There was a problem hiding this comment.
Yes. Done in e4ffa82: arg(1) & arg(2) becomes one argument *(1, 2,), a mixed group arg(1) & kwarg("a", 2) becomes *(1,), **dict(a=2), and appending a positional argument after keyword arguments is an error, as in Python.
|
|
||
| impure function "&"(l, r : arg_t) return arg_t is | ||
| begin | ||
| if l.name = p_ignore_arg then |
There was a problem hiding this comment.
Is grouping will null_arg a real valid use case?
There was a problem hiding this comment.
Yes, it is the identity element of &, and three things rely on it: (1) building a group in a loop, args := null_arg; for i in regs'range loop args := args & kwarg(names(i).all, regs(i)); end loop;, which otherwise needs a special first iteration; (2) optional arguments, since VHDL has no conditional expression the idiom is a helper that returns null_arg when the argument does not apply, and the caller just writes base & optional_kwarg(...); (3) an empty group, call("f", args) with args = null_arg, is a valid call with no arguments, consistent with the unused slots of call. It also costs nothing: after the change in d33a067 a failed conversion no longer degrades to null_arg, so it is only ever produced deliberately. Documented in the comment and the user guide in 9e0cdfd.
| -- constant golden_model : python_session_t := "golden_model"; | ||
| -- | ||
| -- The default session is the __main__ namespace. | ||
| type python_session_t is array (positive range <>) of character; |
There was a problem hiding this comment.
session_t should probably be an object that we can build upon, i.e. a record type with a p_data field of type integer_vector_ptr_t. It is created with new_session function. I would like to see that its name is an identity (id_t) and that log calls are made to a logger with that identity. Going forward, I think we may see message passing into actors within the session namespaces. If so, actors in that namespace will be children of the session identiy. As an initial step, it would be sufficient to place the session name in such an record type but leave everything else as is.
There was a problem hiding this comment.
Done in 91ff756: python_session_t is a record with p_data : integer_vector_ptr_t, created with new_session(name); the name is an identity under vunit_lib:python (the identity of python_logger), name(session) returns it, and the operations of a session log on the logger of that identity, a child of python_logger (the default session logs on python_logger itself). Sessions are compared by identity, so new_session("golden") twice is the same session.
There was a problem hiding this comment.
Now exactly as you describe, in 6f34c38: python_session_t is a record with p_data : integer_vector_ptr_t; new_session(name) creates its identity under vunit_lib:python and, following new_actor, new_session(id : id_t) takes an existing identity so that a session can sit anywhere in the identity tree (and actors can be its children later); get_id(session) and name(session) are the accessors; and every session, the default one included, logs on get_logger(get_id(session)) — the earlier exception where the default session logged on python_logger is gone; python_logger is the logger of the parent identity. The Python namespace is keyed by the full identity name, so sessions with the same leaf name under different parents are distinct.
Review: The release note should tell the larger picture, that VHDL testbenches can execute Python code and call Python functions, rather than list individual type names. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Review: VUnit provides join(a, b) from the path package, already in vunit_context, to concatenate path segments. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Review: The rest of VUnit's typed subprograms use _std_ulogic(_vector), so eval_std_logic and friends should be eval_std_ulogic. The result and parameter types become std_ulogic/std_ulogic_vector as well. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Review: exec_file should not resolve a relative file name against the run script directory. python_pkg resolves it explicitly against tb_path, the directory of the testbench file, like the file names of the other VUnit subprograms, and an absolute name is used as given. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Review: The generated arg and kwarg overloads belong in the arg and kwarg block of the package rather than in sections of their own further down. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Review: The p_arg_value overloads whose conversion cannot fail have no use for the name of the operation. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Review: ieee.std_logic_1164.is_x already has the semantics of the local p_has_metavalue. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Review: to_call_str and call should take the arg_t arguments that arg and kwarg build, not strings the caller has converted itself. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Review: An arg or kwarg whose value cannot be converted returned null_arg, which made the call go ahead without the argument once python_logger was mocked. It now returns an argument whose Python source text raises the message that was reported, through the new __vunit__.error of the bridge runtime. to_call_str builds the call string in VHDL rather than through Python, which keeps the quotes of such a message intact. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Review: & should not be limited to keyword arguments. A group of positional arguments becomes *(1, 2,), a group of both kinds *(1,), **dict(a=1), and appending a positional argument to a group that already has keyword arguments is an error, like in Python. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Review: The comment above & and the user guide should explain what the identity is good for. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Review: A session should be a VUnit object with an identity rather than a string. new_session(name) creates it from an identity under vunit_lib:python, name(session) gives the name back, and the errors of an operation are reported on the logger of its session. Mocking a logger does not capture what its children log, so the default session keeps reporting on python_logger itself. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Regarding the packaging, I have created #1221 as a means to support passing simulator flags (hooks into sim_if) as well as running setup hooks (compiling the C python bridge). I have also created vunit-python-bridge which will be a package similar to vunit-json-for-vhdl, We will merge this later to a repo under the VUnit banner when this feature is completed. For the sake of context I propose that we keep reviewing here, then sync to the vunit-python-bridge repo when the review is done. When we are satisified we can create a repo under the VUnit group and merge my personal repo to there. |
|
As per discussion, as much as possible should reside in the package itself. |
|
As per discussion, move examples to package. |
An operation now reports on the logger of the identity of the session it was performed in, get_logger(get_id(session)), the default session included. The default session used to be a special case reporting on python_logger itself, which made it the only session whose failures were caught without mocking a logger of its own. python_logger is now the logger of the parent identity of the sessions created from a name, so log levels and log handler settings made on it still apply to all of them, and it is used for what has no session: an argument value that cannot be converted and the transfer of an integer_array_t. A session also follows the conventions of the other VUnit objects, as actors do: the private p_id is now the public get_id, and new_session(id) takes an identity that already exists, which puts a session anywhere in the identity tree rather than only under vunit_lib:python. The full name of the identity is the key of the Python namespace, so two sessions with the same name but different identities are namespaces of their own. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Summary
This PR brings the VHDL-to-Python interface of the
python_pkgbranch (Lars Asplund,VUnit/vunit@22af9848) tomaster, with a new native implementation underneath and a few additions to its API.The API of the
python_pkgbranch is the interface.python_pkg.vhd,python_context.vhd, the VHPI package and its C sources,run_script_path, thecheck_result_tgetters and the test cases oftb_python_pkg.vhdare taken over; the testbench's 8 error demonstrations, which failed by design, are now negative tests checking the reported message through the mocked logger, so the whole testbench passes on NVC, GHDL and Questa.Enable with
vu.add_vhdl_builtins()followed byvu.add_python(); in VHDL,context vunit_lib.python_context;.add_python()builds whatever the simulator needs under the output path, so a run script needs nothing else, and the interpreter starts on first use, sopython_setup/python_cleanupare optional.What is underneath
compile_*run-script steps are replaced by the VUnit Python bridge (vunit/python_bridge, C innative/): compiled on first use on Linux/macOS and cached by a fingerprint of its inputs; prebuilt MSVC DLLs per CPython version on Windows (built in CI, shipped in releases, never committed). Loading is automatic (--loadfor NVC, the search path for GHDL, per backend).native/fli.c), built and cached byadd_python()like the others (the system C compiler on Linux, the MinGW compiler bundled with Questa on Windows), so the API behaves identically on NVC, GHDL and Questa. Verified on Questa Altera Starter FPGA Edition 2025.3 (Linux). The branch's FLI application is therefore not included.add_python()as well and starting the interpreter on first use. Untested (no licence available); its run-script helpercompile_vhpi_applicationis not needed and not included.PyConfigas the Python running VUnit (virtual environments and editable installs resolve), the GIL is taken on every entry, only scalars and fixed-size chunks cross the FFI (no length limit, same ABI on all simulators), and Python errors are reported throughpython_logger(vunit_lib:python), so they can be tested withmock/check_only_log.Added to the API, and why
sessionparameter on every operation; a session is a VUnit object (a record around aninteger_vector_ptr_t, created withnew_session(name)or, from an existing identity,new_session(id), withget_id/nameaccessors) whose name is an identity undervunit_lib:python; every session logs on the logger of its identity,python_loggerbeing the parent. Two Python models that both definemodelorconfigcan live in separate namespaces of the same interpreter.arg/kwargvalue types:std_ulogic(→bool),arg_unsigned/arg_signedand theirkwargforms (any width, exact Python integers),real_vector,integer_vector_ptr_t,integer_array_t. The typed names forunsigned/signedexist because anargoverload would make a string literal argument such asarg("Hello")ambiguous. A value that cannot be converted (metavalues) is reported onpython_loggerand becomes an argument that raises in Python, so a call never proceeds with a missing argument. They came out of migrating a real testbench that hands 32-bit CSR counters to a Python checker.kwarg("a", 1) & kwarg("b", 2)becomes one argument (**dict(...)),arg(1) & arg(2)becomes*(1, 2,), and mixed groups follow Python's rules; this lifts the 10-argument limit ofcalland lets arguments be built in loops (null_argis the identity), without touchingarg_torto_call_str.integer_array_tas NumPy arrays, in both directions, with shape and word size preserved (get(a, x, y)isa[y, x]): bulk data such as images and weight tensors are not viable as source text.eval_boolean/call_boolean,eval_std_ulogic, width-checkedstd_ulogic_vector/signed/unsignedresults,call_string,call_real_vector,call_integer_vector_ptr,eval_integer_array/call_integer_array.exec_file: executes a Python file (a relative name is relative to the testbench directory,tb_path) into the session namespace, so a verification component can load its own model from a generic without the testbench knowing that Python is involved.to_call_strandcallare removed; arguments are always built witharg/kwarg.realvalues are not range-checked against single precision on the bridge (VHDLrealis a double on these simulators); any finite value is exchanged exactly and a non-finite one is reported as a failure.The embedded Python example gained cases for each of these, in the style of the existing ones, including a minimal verification component whose behaviour is a Python function.
NumPy as a dependency
numpyis added to the package dependencies.integer_array_tvalues are exchanged as NumPy arrays and a user of the feature should not have to discover that from an error message; the runtime itself only imports NumPy lazily, so nothing changes for users who never calladd_python().Tests and CI
vunit/vhdl/python/run.py: the branch'stb_python_pkg.vhdplustb_python_pkg_bridge.vhd(75 cases for the additions), on NVC, GHDL (mcode, llvm-jit, llvm, gcc) and Questa.python_bridge.yml: Linux with CPython 3.10/3.14 × NVC 1.22.1 and every GHDL backend on the latest release; canaries (allowed to fail) on NVC built from master and the GHDL nightly builds; macOS with NVC and GHDL; the MSVC DLL build (reusablepython_bridge_dlls.yml) and DLL-fed Windows tests on NVC and GHDL with CPython 3.10/3.12/3.14; a packaging check.push.ymlonly gains the DLL download in the release job.docs/python_bridge/user_guide.rst.Review
The first review round is addressed in one commit per comment (24035be … 91ff756, sessions completed in 6f34c38), each answered on its thread.
Disclosure
This work was done with substantial use of an AI assistant (Claude Code, Anthropic): the design, the C, Python and VHDL code, the tests, the workflows and this description were produced with it, under my direction and review, and verified by running the test suites on the simulators listed above. Please review it with that in mind.