diff --git a/examples/vhdl/embedded_python/data/default/input_stimuli.csv b/examples/vhdl/embedded_python/data/default/input_stimuli.csv new file mode 100644 index 000000000..2739d724d --- /dev/null +++ b/examples/vhdl/embedded_python/data/default/input_stimuli.csv @@ -0,0 +1 @@ +1, 2, 3 diff --git a/examples/vhdl/embedded_python/data/extras/input_stimuli2.csv b/examples/vhdl/embedded_python/data/extras/input_stimuli2.csv new file mode 100644 index 000000000..1ddc48e13 --- /dev/null +++ b/examples/vhdl/embedded_python/data/extras/input_stimuli2.csv @@ -0,0 +1 @@ +10, 20, 30 diff --git a/examples/vhdl/embedded_python/run.py b/examples/vhdl/embedded_python/run.py new file mode 100644 index 000000000..d72094724 --- /dev/null +++ b/examples/vhdl/embedded_python/run.py @@ -0,0 +1,100 @@ +# 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-2024, Lars Asplund lars.anders.asplund@gmail.com + +from pathlib import Path +from vunit import VUnit +from vunit.python_pkg import ( + compile_vhpi_application, + compile_fli_application, + compile_vhpidirect_nvc_application, + compile_vhpidirect_ghdl_application, +) + + +def hello_world(): + print("Hello World") + + +class Plot: + + def __init__(self, x_points, y_limits, title, x_label, y_label): + from matplotlib import pyplot as plt + + # Create plot with a line based on x and y vectors before they have been calculated + # Starting with an uncalculated line and updating it as we calculate more points + # is a trick to make the rendering of the plot quicker. This is not a bottleneck + # created by the VHDL package but inherent to the Python matplotlib package. + fig = plt.figure() + plt.xlabel(x_label) + plt.ylabel(y_label) + plt.title(title) + plt.xlim(x_points[0], x_points[-1]) + plt.ylim(*y_limits) + x_vector = [x_points[0]] * len(x_points) + y_vector = [(y_limits[0] + y_limits[1]) / 2] * len(x_points) + (line,) = plt.plot(x_vector, y_vector, "r-") + fig.canvas.draw() + fig.canvas.flush_events() + plt.show(block=False) + + self.plt = plt + self.fig = fig + self.x_vector = x_vector + self.y_vector = y_vector + self.line = line + + def update(self, x, y): + self.x_vector[x] = x + self.y_vector[x] = y + self.line.set_xdata(self.x_vector) + self.line.set_ydata(self.y_vector) + self.fig.canvas.draw() + self.fig.canvas.flush_events() + + def close(self): + # Some extra code to allow showing the plot without blocking + # the test indefinitely if window isn't closed. + timer = self.fig.canvas.new_timer(interval=5000) + timer.add_callback(self.plt.close) + timer.start() + self.plt.show() + + +def main(): + root = Path(__file__).parent + + vu = VUnit.from_argv() + vu.add_vhdl_builtins() + vu.add_python() + vu.add_random() + simulator_name = vu.get_simulator_name() + + if simulator_name in ["rivierapro", "activehdl"]: + # TODO: Include VHPI application compilation in VUnit + # NOTE: A clean build will delete the output after it was created so another no clean build has to be performed. + compile_vhpi_application(root, vu) + elif simulator_name == "modelsim": + compile_fli_application(root, vu) + elif simulator_name == "nvc": + compile_vhpidirect_nvc_application(root, vu) + elif simulator_name == "ghdl": + compile_vhpidirect_ghdl_application(root, vu) + + lib = vu.add_library("lib") + lib.add_source_files(root / "*.vhd") + + vu.set_compile_option("rivierapro.vcom_flags", ["-dbg"]) + vu.set_sim_option("rivierapro.vsim_flags", ["-interceptcoutput"]) + # Crashes RPRO for some reason. TODO: Fix when the C code is properly + # integrated into the project. Must be able to debug the C code. + # vu.set_sim_option("rivierapro.vsim_flags" , ["-cdebug"]) + vu.set_sim_option("nvc.sim_flags", ["--load", str(root / "vunit_out" / "nvc" / "libraries" / "python.so")]) + + vu.main() + + +if __name__ == "__main__": + main() diff --git a/examples/vhdl/embedded_python/tb_example.vhd b/examples/vhdl/embedded_python/tb_example.vhd new file mode 100644 index 000000000..739b96a1d --- /dev/null +++ b/examples/vhdl/embedded_python/tb_example.vhd @@ -0,0 +1,530 @@ +-- 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-2024, Lars Asplund lars.anders.asplund@gmail.com + +library vunit_lib; +context vunit_lib.vunit_context; +context vunit_lib.python_context; +use vunit_lib.random_pkg.all; + +library ieee; +use ieee.std_logic_1164.all; +use ieee.math_real.all; +use ieee.numeric_std_unsigned.all; + +entity tb_example is + generic( + runner_cfg : string; + debug_mode : boolean := true + ); +end entity; + +architecture tb of tb_example is + constant clk_period : time := 10 ns; + signal clk : std_logic := '0'; + signal in_tvalid, in_tlast : std_logic := '0'; + signal in_tdata : std_logic_vector(7 downto 0); + signal out_tvalid, out_tlast : std_logic := '0'; + signal out_tdata : std_logic_vector(7 downto 0); + signal crc_error : std_logic; +begin + test_runner : process + constant pi : real := 3.141592653589793; + constant test_real_vector : real_vector := (-3.4028234664e38, -1.9, 0.0, 1.1, -3.4028234664e38); + constant crc : std_logic_vector(7 downto 0) := x"17"; + constant expected : std_logic_vector(7 downto 0) := x"21"; + + variable gcd, vhdl_integer : integer; + variable test_input, packet : integer_vector_ptr_t; + variable y : real; + variable coordinate : real_vector(1 to 3); + variable input_stimuli : integer_array_t; + variable seed : integer; + variable test_lengths : integer_vector_ptr_t; + variable vhdl_real : real; + + procedure set_tcl_installation is + begin + exec("from os import environ"); + exec("from sys import prefix"); + exec("from pathlib import Path"); + exec("old_environ = environ"); + exec( + "if (Path(prefix) / 'lib' / 'tcl8.6').exists():" + + " environ['TCL_LIBRARY'] = str(Path(prefix) / 'lib' / 'tcl8.6')" + + "else:" + + " environ['TCL_LIBRARY'] = str(Path(prefix) / 'tcl' / 'tcl8.6')" + ); + exec( + "if (Path(prefix) / 'lib' / 'tk8.6').exists():" + + " environ['TK_LIBRARY'] = str(Path(prefix) / 'lib' / 'tk8.6')" + + "else:" + + " environ['TK_LIBRARY'] = str(Path(prefix) / 'tcl' / 'tk8.6')" + ); + end; + + procedure unset_tcl_installation is + begin + exec("environ = old_environ"); + end; + + procedure query_if(expr : boolean; check_result : check_result_t) is + variable logger : logger_t; + variable log_level : log_level_t; + variable line_num : natural; + variable continue : boolean; + alias log_check_result is log[check_result_t]; -- TODO: Fix that location preprocessing is too friendly with this + begin + if not is_pass(check_result) then + if expr then + log_level := get_log_level(check_result); + logger := get_logger(get_checker(check_result)); + line_num := get_line_num(check_result); + + -- Make the query + -- @formatter:off + if line_num = 0 then + continue := eval("psg.popup_yes_no('" & get_msg(check_result) & "\nDo you want to Continue?', title = '" & + title(to_string(get_log_level(check_result))) & "')") = string'("Yes"); + else + continue := eval("psg.popup_yes_no('" & get_msg(check_result) & "\nDo you want to Continue?', title = '" & + title(to_string(get_log_level(check_result))) & " at " & get_file_name(check_result) & " : " & + to_string(line_num) & "')") = string'("Yes"); + end if; + -- @formatter:on + + if continue then + -- If needed, increase stop count to prevent simulation stop + if not has_stop_count(logger, log_level) then + set_stop_count(logger, log_level, 2); + elsif get_log_count(logger, log_level) = get_stop_count(logger, log_level) - 1 then + set_stop_count(logger, log_level, get_log_count(logger, log_level) + 2); + end if; + end if; + end if; + + log_check_result(check_result); + end if; + end; + + -- TODO: Time to make this part of the VUnit release even though it won't be supported for VHDL-93 + impure function new_integer_vector_ptr( + vec : integer_vector; + mode : storage_mode_t := internal; + eid : index_t := -1 + ) return integer_vector_ptr_t is + variable result : integer_vector_ptr_t; + constant vec_normalized : integer_vector(0 to vec'length - 1) := vec; + begin + result := new_integer_vector_ptr(vec'length, mode => mode, eid => eid); + + for idx in vec_normalized'range loop + set(result, idx, vec_normalized(idx)); + end loop; + + return result; + end; + + begin + test_runner_setup(runner, runner_cfg); + python_setup; + + -- To avoid mixup with the Riviera-PRO TCL installation I had to + -- set the TCL_LIBRARY and TK_LIBRARY environment variables + -- to the Python installation. TODO: Find a better way if possible + set_tcl_installation; + + show(display_handler, debug); + + -- @formatter:off + while test_suite loop + ------------------------------------------------------------------------------------- + -- Basic eval and exec examples + ------------------------------------------------------------------------------------- + if run("Test basic exec and eval") then + -- python_pkg provides the Python functions exec and eval. + -- exec executes a string of Python code while eval evaluates + -- a string containing a Python expression. Only eval returns + -- a value. + exec("a = -17"); + check_equal(eval("abs(a)"), 17); + + elsif run("Test multiline code") then + -- Multiline code can be expressed in several ways. At least + + -- 1. Using multiple eval/exec calls: + exec("a = 5"); + exec("b = 2 * a"); + check_equal(eval("b"), 10); + + -- 2. Using semicolon: + exec("from math import factorial; a = factorial(5)"); + check_equal(eval("a"), 120); + + -- 3. Using LF: + exec( + "l = ['a', 'b', 'c']" & LF & + "s = ', '.join(l)" + ); + check_equal(eval("s"), string'("a, b, c")); + + -- 4. Using + as a shorthand for & LF &: + exec( + "def fibonacci(n):" + + " if n == 0:" + + " return [0]" + + " elif n == 1:" + + " return [0, 1]" + + " elif n > 1:" + + " result = fibonacci(n - 1)" + + " result.append(sum(result[-2:]))" + + " return result" + + " else:" + + " return []"); + + exec("print(f'fibonacci(7) = {fibonacci(7)}')"); + + elsif run("Test working with Python types") then + -- Since exec and eval work with strings, VHDL types + -- must be converted to a suitable string representation + -- before being used. + -- + -- Sometimes to_string is enough + exec("an_integer = " & to_string(17)); + check_equal(eval("an_integer"), 17); + + -- Sometimes a helper function is needed + exec("a_list = " & to_py_list_str(integer_vector'(1, 2, 3, 4))); + check_equal(eval("sum(a_list)"), 10); + + -- For eval it is simply a matter of using a suitable + -- overloaded variant + coordinate := eval("[1.5, -3.6, 9.1]"); -- coordinate is real_vector(1 to 3) + check_equal(coordinate(1), 1.5); + check_equal(coordinate(2), -3.6); + check_equal(coordinate(3), 9.1); + + -- Every eval has a more explicit alias that may be needed to help the compiler + exec("from math import pi"); + vhdl_real := eval_real("pi"); + info("pi = " & to_string(vhdl_real)); + + -- Since Python is more dynamically typed than VHDL it is sometimes useful to use + -- dynamic VUnit types + exec( + "from random import randint" + + "test_input = [randint(0, 255) for i in range(randint(1, 100))]"); + test_input := eval("test_input"); -- test_input is a variable of integer_vector_ptr_t type + check(length(test_input) >= 1); + check(length(test_input) <= 100); + + elsif run("Test run script functions") then + -- As we've seen we can define Python functions with exec (fibonacci) and we can import functions from + -- Python packages. Writing large functions in exec strings is not optimal since we don't + -- have the support of a Python-aware editor (syntax highlighting etc). Instead we can put our functions + -- in a Python module, make sure that the module is on the Python search path, and then use a regular import. + -- However, sometimes it is convenient to simply import a function defined in the run script without + -- doing any extra steps. That approach is shown below. + + -- Without a parameter to the import_run_script procedure, the imported module will be named after the + -- run script. In this case the run script is run.py and the module is named run. + import_run_script; + + -- Now we can call functions defined in the run script + exec("run.hello_world()"); + + -- We can also give a name to the imported module + import_run_script("my_run_script"); + exec("my_run_script.hello_world()"); + + -- Regardless of module name, direct access is also possible + exec("from my_run_script import hello_world"); + exec("hello_world()"); + + elsif run("Test function call helpers") then + exec("from math import gcd"); -- gcd = greatest common divisor + + -- Function calls are fundamental to using the eval and exec subprograms. To simplify + -- calls with many arguments, there are a number of helper subprograms. Rather than: + gcd := eval("gcd(" & to_string(35) & ", " & to_string(77) & ", " & to_string(119) & ")"); + check_equal(gcd, 7); + + -- we can use the call function: + gcd := call("gcd", to_string(35), to_string(77), to_string(119)); + check_equal(gcd, 7); + + -- Calls within an exec string can also be simplified + exec("gcd = " & to_call_str("gcd", to_string(35), to_string(77), to_string(119))); + check_equal(eval("gcd"), 7); + + -- Calls to functions without a return value can be simplified with the call procedure + call("print", to_string(35), to_string(77), to_string(119)); + + ------------------------------------------------------------------------------------- + -- Examples related to error management + -- + -- Errors can obviously occur in the executed Python code. They will all cause the + -- test to fail with information about what was the cause + ------------------------------------------------------------------------------------- + elsif run("Test syntax error") then + exec( + "def hello_word():" + + " print('Hello World)"); -- Missing a ' after Hello World + exec("hello_world()"); + + elsif run("Test type error") then + vhdl_integer := eval("1 / 2"); -- Result is a float (0.5) and should be assigned to a real variable + + elsif run("Test Python exception") then + vhdl_integer := eval("1 / 0"); -- Division by zero exception + + ------------------------------------------------------------------------------------- + -- Examples related to the simulation environment + -- + -- VHDL comes with some functionality to manage the simulation environment such + -- as working with files, directories, time, and dates. While this feature set was + -- extended in VHDL-2019, it is still limited compared to what Python offers and the + -- VHDL-2019 extensions are yet to be widely supported by simulators. Below are some + -- examples of what we can do with Python. + ------------------------------------------------------------------------------------- + elsif run("Test logging simulation platform") then + -- Logging the simulation platform can be done directly from the run script but + -- here we get the information included in the same log used by the VHDL code + exec("from datetime import datetime"); + exec("import platform"); + exec("from vunit import VUnit"); + + info( + colorize("System name: ", cyan) & eval("platform.node()") + + colorize("Operating system: ", cyan) & eval("platform.platform()") + + colorize("Processor: ", cyan) & eval("platform.processor()") + + colorize("Simulator: ", cyan) & eval("VUnit.from_argv().get_simulator_name()") + + colorize("Simulation started: ", cyan) & eval("datetime.now().strftime('%Y-%m-%d %H:%M:%S')") + ); + + elsif run("Test globbing for files") then + -- glob finds all files matching a pattern, in this case all CSV in the directory tree rooted + -- in the testbench directory + exec("from glob import glob"); + exec("stimuli_files = glob('" & join(tb_path(runner_cfg), "**", "*.csv") & "', recursive=True)"); + + for idx in 0 to eval("len(stimuli_files)") - 1 loop + -- Be aware that you may receive backslashes that should be replaced with forward slashes to be + -- VHDL compatible + input_stimuli := load_csv(replace(eval("stimuli_files[" & to_string(idx) & "]"), "\", "/")); + + -- Test with stimuli file... + end loop; + + ------------------------------------------------------------------------------------- + -- Examples of user interactions + -- + -- The end goal should always be fully automated testing but during an exploratory + -- phase and while debugging it can be useful to allow for some user control of + -- the simulation + ------------------------------------------------------------------------------------- + elsif run("Test GUI browsing for input stimuli file") then + if debug_mode then + exec("import PySimpleGUI as psg"); -- Install PySimpleGUI with pip install pysimplegui + input_stimuli := load_csv(replace(eval("psg.popup_get_file('Select input stimuli file')"), "\", "/")); + else + input_stimuli := load_csv(join(tb_path(runner_cfg), "data", "default", "input_stimuli.csv")); + end if; + + elsif run("Test querying for randomization seed") then + if debug_mode then + exec("import PySimpleGUI as psg"); + seed := integer'value(eval("psg.popup_get_text('Enter seed', default_text='1234')")); + else + seed := 1234; + end if; + info("Seed = " & to_string(seed)); + + elsif run("Test controlling progress of simulation") then + exec("import PySimpleGUI as psg"); + + -- We can let the answers to yes/no pop-ups (psg.popup_yes_no) decide how to proceed, + -- for example if the simulation shall continue on an error. Putting that into an + -- action procedure to a check function and we can have a query_if wrapper like this: + query_if(debug_mode, check_equal(crc, expected, result("for CRC"))); + + info("Decided to continue"); + + ------------------------------------------------------------------------------------- + -- Example math functions + -- + -- You can find almost anything in the Python ecosystem so here are just a few + -- examples of situations where that ecosystem can be helpful + ------------------------------------------------------------------------------------- + + -- Have you ever wished VHDL had some a of the features that SystemVerilog has? + -- The support for a constraint solver is an example of something SystemVerilog has + -- and VHDL does not. However, there are Python constraint solvers that might be helpful + -- if a randomization constraint is difficult to solve with a procedural VHDL code. + -- This is not such a difficult problem but just an example of using a Python constraint solver. + elsif run("Test constraint solving") then + exec("from constraint import Problem"); -- Install with pip install python-constraint + exec("from random import choice"); + exec( + "problem = Problem()" + + "problem.addVariables(['x', 'y', 'z'], list(range(16)))" + -- Three variables in the 0 - 15 range + + -- Constrain the variable + "problem.addConstraint(lambda x, y, z: x != z)" + + "problem.addConstraint(lambda x, y, z: y <= z)" + + "problem.addConstraint(lambda x, y, z: x + y == 8)" + + + -- Pick a random solution + "solutions = problem.getSolutions()" + + "solution = choice(solutions)" + ); + + -- Check that the constraints were met + check(eval_integer("solution['x']") /= eval_integer("solution['z']")); + check(eval_integer("solution['y']") <= eval_integer("solution['z']")); + check_equal(eval_integer("solution['x']") + eval_integer("solution['y']"), 8); + + elsif run("Test using a Python module as the golden reference") then + -- In this example we want to test that a receiver correctly accepts + -- a packet with a trailing CRC. While we can generate a correct CRC to + -- our test packets in VHDL, it is easier to use a Python package that + -- already provides most standard CRC calculations. By doing so, we + -- also have an independent opinion of how the CRC should be calculated. + + -- crccheck (install with pip install crccheck) supports more than 100 standard CRCs. + -- Using the 8-bit Bluetooth CRC for this example + exec("from crccheck.crc import Crc8Bluetooth"); + + -- Let's say we should support packet payloads between 1 and 128 bytes and want to test + -- a subset of those. + test_lengths := new_integer_vector_ptr(integer_vector'(1, 2, 8, 16, 32, 64, 127, 128)); + for idx in 0 to length(test_lengths) - 1 loop + -- Randomize a payload but add an extra byte to store the CRC + packet := random_integer_vector_ptr(get(test_lengths, idx) + 1, min_value => 0, max_value => 255); + + -- Calculate and insert the correct CRC + exec("packet = " & to_py_list_str(packet)); + set(packet, get(test_lengths, idx), eval("Crc8Bluetooth.calc(packet[:-1])")); + + -- This is where we would apply the generated packet to the tested receiver and verify that the CRC + -- is accepted as correct + end loop; + + elsif run("Test using Python in an behavioral model") then + -- When writing VHDL behavioral models we want the model to be as high-level as possible. + -- Python creates opportunities to make these models even more high-level + + -- In this example we use the crccheck package to create a behavioral model for a receiver. + -- Such a model would normally be in a separate process or component so the code below is more + -- an illustration of the general idea. We have a minimalistic incoming AXI stream (in_tdata/tvalid/tlast) + -- receiving a packet from a transmitter (at the bottom of this file). The model collects the incoming packet + -- in a dynamic vector (packet). in_tlast becomes active along with the last recevied byte which is also the + -- CRC. At that point the packet collected before the CRC is pushed into Python where the expected CRC is + -- calculated. If it differs from the received CRC, crc_error is set + -- + -- The model also pass the incoming AXI stream to the output (out_tdata/tvalid/tlast) + exec("from crccheck.crc import Crc8Bluetooth"); + packet := new_integer_vector_ptr; + out_tvalid <= '0'; + crc_error <= '0'; + loop + wait until rising_edge(clk); + exit when out_tlast; + out_tvalid <= in_tvalid; + if in_tvalid then + out_tdata <= in_tdata; + out_tlast <= in_tlast; + if in_tlast then + exec("packet = " & to_py_list_str(packet)); + crc_error <= '1' when eval_integer("Crc8Bluetooth.calc(packet)") /= to_integer(in_tdata) else '0'; + else + resize(packet, length(packet) + 1, value => to_integer(in_tdata)); + end if; + end if; + end loop; + check_equal(crc_error, '0'); + out_tvalid <= '0'; + out_tlast <= '0'; + crc_error <= '0'; + + + --------------------------------------------------------------------- + -- Examples of string manipulation + -- + -- Python has tons of functionality for working with string. One of the + -- features most commonly missed in VHDL is the support for format + -- specifiers. Here are two ways of dealing with that + --------------------------------------------------------------------- + elsif run("Test C-style format specifiers") then + exec("from math import pi"); + info(eval("'pi is about %.2f' % pi")); + + elsif run("Test Python f-strings") then + exec("from math import pi"); + info(eval("f'pi is about {pi:.2f}'")); + + --------------------------------------------------------------------- + -- Examples of plotting + -- + -- Simulators have a limited set of capabilities when it comes to + -- Visualize simulation output beyond signal waveforms. Python has + -- almost endless capabilities + --------------------------------------------------------------------- + elsif run("Test simple plot") then + exec("from matplotlib import pyplot as plt"); -- Matplotlib is installed with pip install matplotlib + exec("fig = plt.figure()"); + exec("plt.plot([1,2,3,4,5], [1,2,3,4,5])"); + exec("plt.show()"); + + elsif run("Test advanced plot") then + import_run_script("my_run_script"); + + exec( + "from my_run_script import Plot" + + "plot = Plot(x_points=list(range(360)), y_limits=(-1.1, 1.1), title='Matplotlib Demo'," + + " x_label='x [degree]', y_label='y = sin(x)')"); + + -- A use case for plotting from VHDL is to monitor the progress of slow simulations where we want to update + -- the plot as more data points become available. This is not a slow simulation but the updating of the plot + -- is a bit slow. This is not due to the VHDL code but inherent to Matplotlib. For the use case where we + -- actually have a slow VHDL simulation, the overhead on the Matplotlib is of no significance. + for x in 0 to 359 loop + y := sin(pi * real(x) / 180.0); + + call("plot.update", to_string(x), to_string(y)); + end loop; + + exec("plot.close()"); + + end if; + end loop; + -- @formatter:on + + -- Revert to old environment variables + unset_tcl_installation; + python_cleanup; + test_runner_cleanup(runner); + end process; + + clk <= not clk after clk_period / 2; + + packet_transmitter : process is + -- Packet with valid CRC + constant packet : integer_vector := (48, 43, 157, 58, 110, 67, 192, 76, 119, 97, 235, 143, 131, 216, 60, 121, 111); + begin + -- Transmit the packet once + in_tvalid <= '0'; + for idx in packet'range loop + wait until rising_edge(clk); + in_tdata <= to_slv(packet(idx), in_tdata); + in_tlast <= '1' when idx = packet'right else '0'; + in_tvalid <= '1'; + end loop; + wait until rising_edge(clk); + in_tvalid <= '0'; + in_tlast <= '0'; + wait; + end process; +end; diff --git a/tests/unit/test_test_runner.py b/tests/unit/test_test_runner.py index d29988fa2..2d5678369 100644 --- a/tests/unit/test_test_runner.py +++ b/tests/unit/test_test_runner.py @@ -11,6 +11,7 @@ from pathlib import Path import unittest from unittest import mock +from tempfile import TemporaryFile from tests.common import with_tempdir from vunit.hashing import hash_string from vunit.test.runner import TestRunner @@ -23,10 +24,13 @@ class TestTestRunner(unittest.TestCase): Test the test runner """ + def setUp(self): + self._run_script = TemporaryFile() + @with_tempdir def test_runs_testcases_in_order(self, tempdir): report = TestReport() - runner = TestRunner(report, tempdir) + runner = TestRunner(report, tempdir, self._run_script) order = [] test_case1 = self.create_test("test1", True, order=order) @@ -48,7 +52,7 @@ def test_runs_testcases_in_order(self, tempdir): @with_tempdir def test_fail_fast(self, tempdir): report = TestReport() - runner = TestRunner(report, tempdir, fail_fast=True) + runner = TestRunner(report, tempdir, self._run_script, fail_fast=True) order = [] test_case1 = self.create_test("test1", True, order=order) @@ -72,7 +76,7 @@ def test_fail_fast(self, tempdir): @with_tempdir def test_handles_python_exeception(self, tempdir): report = TestReport() - runner = TestRunner(report, tempdir) + runner = TestRunner(report, tempdir, self._run_script) test_case = self.create_test("test", True) test_list = TestList() @@ -88,7 +92,7 @@ def side_effect(*args, **kwargs): # pylint: disable=unused-argument @with_tempdir def test_collects_output(self, tempdir): report = TestReport() - runner = TestRunner(report, tempdir) + runner = TestRunner(report, tempdir, self._run_script) test_case = self.create_test("test", True) test_list = TestList() @@ -111,7 +115,7 @@ def side_effect(*args, **kwargs): # pylint: disable=unused-argument @with_tempdir def test_can_read_output(self, tempdir): report = TestReport() - runner = TestRunner(report, tempdir) + runner = TestRunner(report, tempdir, self._run_script) test_case = self.create_test("test", True) test_list = TestList() @@ -138,7 +142,7 @@ def side_effect(read_output, **kwargs): # pylint: disable=unused-argument def test_get_output_path_on_linux(self): output_path = "output_path" report = TestReport() - runner = TestRunner(report, output_path) + runner = TestRunner(report, output_path, self._run_script) with mock.patch("sys.platform", new="linux"): with mock.patch("os.environ", new={}): @@ -169,7 +173,7 @@ def test_get_output_path_on_linux(self): def test_get_output_path_on_windows(self): output_path = "output_path" report = TestReport() - runner = TestRunner(report, output_path) + runner = TestRunner(report, output_path, self._run_script) with mock.patch("sys.platform", new="win32"): with mock.patch("os.environ", new={}): @@ -213,6 +217,9 @@ def run_side_effect(*args, **kwargs): # pylint: disable=unused-argument test_case = TestCaseMock(name=name, run_side_effect=run_side_effect) return test_case + def tearDown(self): + self._run_script.close() + class TestCaseMock(object): """ @@ -226,7 +233,7 @@ def __init__(self, name, run_side_effect): self.called = False self.run_side_effect = run_side_effect - def run(self, output_path, read_output): + def run(self, output_path, read_output, run_script_path): """ Mock run method that just records the arguments """ diff --git a/vunit/builtins.py b/vunit/builtins.py index 567f36e37..c825e6d4e 100755 --- a/vunit/builtins.py +++ b/vunit/builtins.py @@ -43,6 +43,7 @@ def add(name, deps=tuple()): add("osvvm") add("random", ["osvvm"]) add("json4vhdl") + add("python") def add(self, name, args=None): self._builtins_adder.add(name, args) @@ -211,6 +212,29 @@ def _add_json4vhdl(self): library.add_source_files(VHDL_PATH / "JSON-for-VHDL" / "src" / "*.vhdl") + def _add_python(self): + """ + Add python package + """ + if not self._vhdl_standard >= VHDL.STD_2008: + raise RuntimeError("Python package only supports vhdl 2008 and later") + + python_package_supported_flis = set(["VHPI", "FLI", "VHPIDIRECT_NVC", "VHPIDIRECT_GHDL"]) + simulator_supported_flis = self._simulator_class.supported_foreign_language_interfaces() + if not python_package_supported_flis & simulator_supported_flis: + raise RuntimeError(f"Python package requires support for one of {', '.join(python_package_supported_flis)}") + + self._vunit_lib.add_source_files(VHDL_PATH / "python" / "src" / "python_context.vhd") + self._vunit_lib.add_source_files(VHDL_PATH / "python" / "src" / "python_pkg.vhd") + if "VHPI" in simulator_supported_flis: + self._vunit_lib.add_source_files(VHDL_PATH / "python" / "src" / "python_pkg_vhpi.vhd") + elif "FLI" in simulator_supported_flis: + self._vunit_lib.add_source_files(VHDL_PATH / "python" / "src" / "python_pkg_fli.vhd") + elif "VHPIDIRECT_NVC" in simulator_supported_flis: + self._vunit_lib.add_source_files(VHDL_PATH / "python" / "src" / "python_pkg_vhpidirect_nvc.vhd") + elif "VHPIDIRECT_GHDL" in simulator_supported_flis: + self._vunit_lib.add_source_files(VHDL_PATH / "python" / "src" / "python_pkg_vhpidirect_ghdl.vhd") + def _add_vhdl_logging(self): """ Add logging functionality diff --git a/vunit/python_pkg.py b/vunit/python_pkg.py new file mode 100644 index 000000000..7ccd3359d --- /dev/null +++ b/vunit/python_pkg.py @@ -0,0 +1,270 @@ +# 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-2024, Lars Asplund lars.anders.asplund@gmail.com + +""" +Temporary helper module to compile C-code used by python_pkg. +""" +from pathlib import Path +from glob import glob +import subprocess +import sys + + +def compile_vhpi_application(run_script_root, vu): + """ + Compile VHPI application used by Aldec's simulators. + """ + path_to_shared_lib = (run_script_root / "vunit_out" / vu.get_simulator_name() / "libraries").resolve() + if not path_to_shared_lib.exists(): + path_to_shared_lib.mkdir(parents=True, exist_ok=True) + shared_lib = path_to_shared_lib / "python.dll" + path_to_python_include = Path(sys.executable).parent.resolve() / "include" + path_to_python_libs = Path(sys.executable).parent.resolve() / "libs" + python_shared_lib = f"python{sys.version_info[0]}{sys.version_info[1]}" + path_to_python_pkg = Path(__file__).parent.resolve() / "vhdl" / "python" / "src" + c_file_paths = [path_to_python_pkg / "python_pkg_vhpi.c", path_to_python_pkg / "python_pkg.c"] + path_to_simulator = Path(vu._simulator_class.find_prefix()).resolve() # pylint: disable=protected-access + ccomp_executable = path_to_simulator / "ccomp.exe" + + proc = subprocess.run( + [ + str(ccomp_executable), + "-vhpi", + "-dbg", + "-verbose", + "-o", + '"' + str(shared_lib) + '"', + "-l", + python_shared_lib, + "-l", + "python3", + "-l", + "_tkinter", + "-I", + '"' + str(path_to_python_include) + '"', + "-I", + '"' + str(path_to_python_pkg) + '"', + "-L", + '"' + str(path_to_python_libs) + '"', + " ".join(['"' + str(path) + '"' for path in c_file_paths]), + ], + capture_output=True, + text=True, + check=False, + ) + + if proc.returncode != 0: + print(proc.stdout) + print(proc.stderr) + raise RuntimeError("Failed to compile VHPI application") + + +def compile_fli_application(run_script_root, vu): # pylint: disable=too-many-locals + """ + Compile FLI application used by Questa. + """ + path_to_simulator = Path(vu._simulator_class.find_prefix()).resolve() # pylint: disable=protected-access + path_to_simulator_include = (path_to_simulator / ".." / "include").resolve() + + # 32 or 64 bit installation? + vsim_executable = path_to_simulator / "vsim.exe" + proc = subprocess.run( + [vsim_executable, "-version"], + capture_output=True, + text=True, + check=False, + ) + if proc.returncode != 0: + print(proc.stderr) + raise RuntimeError("Failed to get vsim version") + + is_64_bit = "64 vsim" in proc.stdout + + # Find GCC executable + matches = glob(str((path_to_simulator / ".." / f"gcc*mingw{'64' if is_64_bit else '32'}*").resolve())) + if len(matches) != 1: + raise RuntimeError("Failed to find GCC executable") + gcc_executable = (Path(matches[0]) / "bin" / "gcc.exe").resolve() + + path_to_python_include = Path(sys.executable).parent.resolve() / "include" + path_to_python_pkg = Path(__file__).parent.resolve() / "vhdl" / "python" / "src" + c_file_paths = [path_to_python_pkg / "python_pkg_fli.c", path_to_python_pkg / "python_pkg.c"] + + for c_file_path in c_file_paths: + args = [ + str(gcc_executable), + "-g", + "-c", + "-m64" if is_64_bit else "-m32", + "-Wall", + "-D__USE_MINGW_ANSI_STDIO=1", + ] + + if not is_64_bit: + args += ["-ansi", "-pedantic"] + + args += [ + "-I" + str(path_to_simulator_include), + "-I" + str(path_to_python_include), + "-freg-struct-return", + str(c_file_path), + ] + + proc = subprocess.run( + args, + capture_output=True, + text=True, + check=False, + ) + if proc.returncode != 0: + print(proc.stdout) + print(proc.stderr) + raise RuntimeError("Failed to compile FLI application") + + path_to_python_libs = Path(sys.executable).parent.resolve() / "libs" + + path_to_shared_lib = run_script_root / "vunit_out" / vu.get_simulator_name() / "libraries" / "python" + if not path_to_shared_lib.exists(): + path_to_shared_lib.mkdir(parents=True, exist_ok=True) + python_shared_lib = f"python{sys.version_info[0]}{sys.version_info[1]}" + + args = [ + str(gcc_executable), + "-shared", + "-lm", + "-m64" if is_64_bit else "-m32", + "-Wl,-Bsymbolic", + "-Wl,-export-all-symbols", + "-o", + str(path_to_shared_lib / "python_fli.so"), + "python_pkg.o", + "python_pkg_fli.o", + "-l" + python_shared_lib, + "-l_tkinter", + "-L" + str(path_to_simulator), + "-L" + str(path_to_python_libs), + "-lmtipli", + ] + + proc = subprocess.run( + args, + capture_output=True, + text=True, + check=False, + ) + if proc.returncode != 0: + print(proc.stdout) + print(proc.stderr) + raise RuntimeError("Failed to link FLI application") + + +def compile_vhpidirect_nvc_application(run_script_root, vu): + """ + Compile VHPIDIRECT application for NVC. + """ + path_to_shared_lib = (run_script_root / "vunit_out" / vu.get_simulator_name() / "libraries").resolve() + if not path_to_shared_lib.exists(): + path_to_shared_lib.mkdir(parents=True, exist_ok=True) + shared_lib = path_to_shared_lib / "python.so" + path_to_python_include = ( + Path(sys.executable).parent.parent.resolve() / "include" / f"python{sys.version_info[0]}.{sys.version_info[1]}" + ) + path_to_python_libs = Path(sys.executable).parent.parent.resolve() / "bin" + python_shared_lib = f"libpython{sys.version_info[0]}.{sys.version_info[1]}" + path_to_python_pkg = Path(__file__).parent.resolve() / "vhdl" / "python" / "src" + + c_file_paths = [path_to_python_pkg / "python_pkg_vhpidirect_nvc.c", path_to_python_pkg / "python_pkg.c"] + + for c_file_path in c_file_paths: + args = [ + "gcc", + "-c", + "-I", + str(path_to_python_include), + str(c_file_path), + ] + + proc = subprocess.run(args, capture_output=True, text=True, check=False, cwd=str(path_to_shared_lib / "..")) + if proc.returncode != 0: + print(proc.stdout) + print(proc.stderr) + raise RuntimeError("Failed to compile NVC VHPIDIRECT application") + + args = [ + "gcc", + "-shared", + "-fPIC", + "-o", + str(shared_lib), + "python_pkg.o", + "python_pkg_vhpidirect_nvc.o", + "-l", + python_shared_lib, + "-L", + str(path_to_python_libs), + ] + + proc = subprocess.run(args, capture_output=True, text=True, check=False, cwd=str(path_to_shared_lib / "..")) + if proc.returncode != 0: + print(proc.stdout) + print(proc.stderr) + raise RuntimeError("Failed to link NVC VHPIDIRECT application") + + +def compile_vhpidirect_ghdl_application(run_script_root, vu): # pylint: disable=unused-argument + """ + Compile VHPIDIRECT application for GHDL. + """ + # TODO: Avoid putting in root # pylint: disable=fixme + path_to_shared_lib = (run_script_root).resolve() + if not path_to_shared_lib.exists(): + path_to_shared_lib.mkdir(parents=True, exist_ok=True) + shared_lib = path_to_shared_lib / "python.so" + path_to_python_include = ( + Path(sys.executable).parent.parent.resolve() / "include" / f"python{sys.version_info[0]}.{sys.version_info[1]}" + ) + path_to_python_libs = Path(sys.executable).parent.parent.resolve() / "bin" + python_shared_lib = f"libpython{sys.version_info[0]}.{sys.version_info[1]}" + path_to_python_pkg = Path(__file__).parent.resolve() / "vhdl" / "python" / "src" + + c_file_names = ["python_pkg_vhpidirect_ghdl.c", "python_pkg.c"] + + for c_file_name in c_file_names: + args = [ + "gcc", + "-c", + "-I", + str(path_to_python_include), + str(path_to_python_pkg / c_file_name), + "-o", + str(path_to_shared_lib / (c_file_name[:-1] + "o")), + ] + + proc = subprocess.run(args, capture_output=True, text=True, check=False, cwd=str(path_to_shared_lib / "..")) + if proc.returncode != 0: + print(proc.stdout) + print(proc.stderr) + raise RuntimeError("Failed to compile GHDL VHPIDIRECT application") + + args = [ + "gcc", + "-shared", + "-fPIC", + "-o", + str(shared_lib), + str(path_to_shared_lib / "python_pkg.o"), + str(path_to_shared_lib / "python_pkg_vhpidirect_ghdl.o"), + "-l", + python_shared_lib, + "-L", + str(path_to_python_libs), + ] + + proc = subprocess.run(args, capture_output=True, text=True, check=False, cwd=str(path_to_shared_lib / "..")) + if proc.returncode != 0: + print(proc.stdout) + print(proc.stderr) + raise RuntimeError("Failed to link GHDL VHPIDIRECT application") diff --git a/vunit/sim_if/__init__.py b/vunit/sim_if/__init__.py index 5596dbd3c..1a1e1f7df 100644 --- a/vunit/sim_if/__init__.py +++ b/vunit/sim_if/__init__.py @@ -81,6 +81,13 @@ def supports_vhdl_call_paths(cls): """ return False + @classmethod + def supported_foreign_language_interfaces(cls): + """ + Returns set of supported foreign interfaces + """ + return set() + @staticmethod def find_executable(executable): """ diff --git a/vunit/sim_if/activehdl.py b/vunit/sim_if/activehdl.py index 18e16738d..581cdbcbc 100644 --- a/vunit/sim_if/activehdl.py +++ b/vunit/sim_if/activehdl.py @@ -59,6 +59,13 @@ def supports_vhdl_call_paths(cls): """ return True + @classmethod + def supported_foreign_language_interfaces(cls): + """ + Returns set of supported foreign interfaces + """ + return set(["VHPI"]) + @classmethod def supports_vhdl_package_generics(cls): """ diff --git a/vunit/sim_if/ghdl.py b/vunit/sim_if/ghdl.py index 6fd33dee0..3ead06f7a 100644 --- a/vunit/sim_if/ghdl.py +++ b/vunit/sim_if/ghdl.py @@ -177,6 +177,13 @@ def supports_vhdl_package_generics(cls): """ return True + @classmethod + def supported_foreign_language_interfaces(cls): + """ + Returns set of supported foreign interfaces + """ + return set(["VHPIDIRECT_GHDL"]) + @classmethod def supports_vhpi(cls): """ diff --git a/vunit/sim_if/modelsim.py b/vunit/sim_if/modelsim.py index dc353211d..28e4d6cc2 100644 --- a/vunit/sim_if/modelsim.py +++ b/vunit/sim_if/modelsim.py @@ -71,6 +71,13 @@ def has_modelsim_ini(path): return cls.find_toolchain(["vsim"], constraints=[has_modelsim_ini]) + @classmethod + def supported_foreign_language_interfaces(cls): + """ + Returns set of supported foreign interfaces + """ + return set(["FLI"]) + @classmethod def supports_vhdl_package_generics(cls): """ diff --git a/vunit/sim_if/nvc.py b/vunit/sim_if/nvc.py index 4081944dc..4ac4173a4 100644 --- a/vunit/sim_if/nvc.py +++ b/vunit/sim_if/nvc.py @@ -144,6 +144,13 @@ def supports_vhdl_package_generics(cls): """ return True + @classmethod + def supported_foreign_language_interfaces(cls): + """ + Returns set of supported foreign interfaces + """ + return set(["VHPIDIRECT_NVC"]) + def setup_library_mapping(self, project): """ Setup library mapping diff --git a/vunit/sim_if/rivierapro.py b/vunit/sim_if/rivierapro.py index c794e9c0b..30b126352 100644 --- a/vunit/sim_if/rivierapro.py +++ b/vunit/sim_if/rivierapro.py @@ -100,6 +100,13 @@ def supports_vhdl_call_paths(cls): """ return True + @classmethod + def supported_foreign_language_interfaces(cls): + """ + Returns set of supported foreign interfaces + """ + return set(["VHPI"]) + @classmethod def supports_vhdl_package_generics(cls): """ diff --git a/vunit/test/runner.py b/vunit/test/runner.py index 536e18611..8ba59c7c9 100644 --- a/vunit/test/runner.py +++ b/vunit/test/runner.py @@ -37,6 +37,7 @@ def __init__( # pylint: disable=too-many-arguments self, report, output_path, + run_script_path, verbosity=VERBOSITY_NORMAL, num_threads=1, fail_fast=False, @@ -49,6 +50,7 @@ def __init__( # pylint: disable=too-many-arguments self._local = threading.local() self._report = report self._output_path = output_path + self._run_script_path = run_script_path assert verbosity in ( self.VERBOSITY_QUIET, self.VERBOSITY_NORMAL, @@ -241,7 +243,9 @@ def read_output(): output_file.seek(prev) return contents - results = test_suite.run(output_path=output_path, read_output=read_output) + results = test_suite.run( + output_path=output_path, read_output=read_output, run_script_path=self._run_script_path + ) except KeyboardInterrupt as exk: self._add_skipped_tests(test_suite, results, start_time, num_tests, output_file_name) raise KeyboardInterrupt from exk diff --git a/vunit/test/suites.py b/vunit/test/suites.py index 9f63ac4c2..60fd3bc9f 100644 --- a/vunit/test/suites.py +++ b/vunit/test/suites.py @@ -159,7 +159,7 @@ def __init__(self, simulator_if, config, elaborate_only, test_suite_name, test_c def set_test_cases(self, test_cases): self._test_cases = test_cases - def run(self, output_path, read_output): + def run(self, output_path, read_output, run_script_path): """ Run selected test cases within the test suite @@ -175,7 +175,7 @@ def run(self, output_path, read_output): # Ensure result file exists ostools.write_file(get_result_file_name(output_path), "") - sim_ok = self._simulate(output_path) + sim_ok = self._simulate(output_path, run_script_path) if self._elaborate_only: status = PASSED if sim_ok else FAILED @@ -211,7 +211,7 @@ def _check_results(self, results, sim_ok): return False, results - def _simulate(self, output_path): + def _simulate(self, output_path, run_script_path): """ Add runner_cfg generic values and run simulation """ @@ -229,6 +229,7 @@ def _simulate(self, output_path): "output path": output_path.replace("\\", "/") + "/", "active python runner": True, "tb path": config.tb_path.replace("\\", "/") + "/", + "run script path": str(run_script_path).replace("\\", "/"), } # @TODO Warn if runner cfg already set? diff --git a/vunit/ui/__init__.py b/vunit/ui/__init__.py index 3ddc531e3..125c2d63f 100644 --- a/vunit/ui/__init__.py +++ b/vunit/ui/__init__.py @@ -19,6 +19,7 @@ from typing import Optional, Set, Union from pathlib import Path from fnmatch import fnmatch +from inspect import stack from ..database import PickledDataBase, DataBase from .. import ostools @@ -116,10 +117,19 @@ def __init__( args, vhdl_standard: Optional[str] = None, ): + self._args = args self._configure_logging(args.log_level) self._output_path = str(Path(args.output_path).resolve()) + # The run script is defined as the external file making the call that created the VUnit object. + # That file is not necessarily the caller of the __init__ function nor the root of the stack. + stack_frame = 0 + this_file_path = Path(__file__).resolve() + while Path(stack()[stack_frame][1]).resolve() == this_file_path: + stack_frame += 1 + self._run_script_path = Path(stack()[stack_frame][1]).resolve() + if args.no_color: self._printer = NO_COLOR_PRINTER else: @@ -953,6 +963,7 @@ def _run_test(self, test_cases, report): runner = TestRunner( report, str(Path(self._output_path) / TEST_OUTPUT_PATH), + self._run_script_path, verbosity=verbosity, num_threads=self._args.num_threads, fail_fast=self._args.fail_fast, @@ -1032,6 +1043,12 @@ def add_json4vhdl(self): """ self._builtins.add("json4vhdl") + def add_python(self): + """ + Add python package + """ + self._builtins.add("python") + def get_compile_order(self, source_files=None): """ Get the compile order of all or specific source files and diff --git a/vunit/vhdl/check/src/check.vhd b/vunit/vhdl/check/src/check.vhd index b2c9fd44b..fd883cfc2 100644 --- a/vunit/vhdl/check/src/check.vhd +++ b/vunit/vhdl/check/src/check.vhd @@ -131,6 +131,38 @@ package body check_pkg is log(check_result); end; + impure function is_pass(check_result : check_result_t) return boolean is + begin + return check_result.p_is_pass; + end; + + impure function get_checker(check_result : check_result_t) return checker_t is + begin + return check_result.p_checker; + end; + + impure function get_msg(check_result : check_result_t) return string is + begin + return to_string(check_result.p_msg); + end; + + impure function get_log_level(check_result : check_result_t) return log_level_t is + begin + return check_result.p_level; + end; + + impure function get_line_num(check_result : check_result_t) return natural is + begin + return check_result.p_line_num; + end; + + impure function get_file_name(check_result : check_result_t) return string is + begin + return to_string(check_result.p_file_name); + end; + + + ----------------------------------------------------------------------------- -- check ----------------------------------------------------------------------------- diff --git a/vunit/vhdl/check/src/check_api.vhd b/vunit/vhdl/check/src/check_api.vhd index bedf49768..e8636fe2e 100644 --- a/vunit/vhdl/check/src/check_api.vhd +++ b/vunit/vhdl/check/src/check_api.vhd @@ -43,8 +43,13 @@ package check_pkg is type trigger_event_t is (first_pipe, first_no_pipe, penultimate); procedure log(check_result : check_result_t); - procedure notify_if_fail(check_result : check_result_t; signal event : inout any_event_t); + impure function is_pass(check_result : check_result_t) return boolean; + impure function get_checker(check_result : check_result_t) return checker_t; + impure function get_msg(check_result : check_result_t) return string; + impure function get_log_level(check_result : check_result_t) return log_level_t; + impure function get_line_num(check_result : check_result_t) return natural; + impure function get_file_name(check_result : check_result_t) return string; ----------------------------------------------------------------------------- -- check diff --git a/vunit/vhdl/python/run.py b/vunit/vhdl/python/run.py new file mode 100644 index 000000000..1d6577825 --- /dev/null +++ b/vunit/vhdl/python/run.py @@ -0,0 +1,55 @@ +# 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-2024, Lars Asplund lars.anders.asplund@gmail.com + +import sys +from os import environ +from pathlib import Path +from vunit import VUnit +from vunit.python_pkg import ( + compile_vhpi_application, + compile_fli_application, + compile_vhpidirect_nvc_application, + compile_vhpidirect_ghdl_application, +) + + +def remote_test(): + return 2 + + +def main(): + root = Path(__file__).parent + vu = VUnit.from_argv() + vu.add_vhdl_builtins() + vu.add_python() + simulator_name = vu.get_simulator_name() + + if simulator_name in ["rivierapro", "activehdl"]: + # TODO: Include VHPI application compilation in VUnit + # NOTE: A clean build will delete the output after it was created so another no clean build has to be performed. + compile_vhpi_application(root, vu) + elif simulator_name == "modelsim": + compile_fli_application(root, vu) + elif simulator_name == "nvc": + compile_vhpidirect_nvc_application(root, vu) + elif simulator_name == "ghdl": + compile_vhpidirect_ghdl_application(root, vu) + + lib = vu.add_library("lib") + lib.add_source_files(root / "test" / "*.vhd") + + vu.set_compile_option("rivierapro.vcom_flags", ["-dbg"]) + vu.set_sim_option("rivierapro.vsim_flags", ["-interceptcoutput"]) + # Crashes RPRO for some reason. TODO: Fix when the C code is properly + # integrated into the project. Must be able to debug the C code. + # vu.set_sim_option("rivierapro.vsim_flags" , ["-cdebug"]) + vu.set_sim_option("nvc.sim_flags", ["--load", str(root / "vunit_out" / "nvc" / "libraries" / "python.so")]) + + vu.main() + + +if __name__ == "__main__": + main() diff --git a/vunit/vhdl/python/src/python_context.vhd b/vunit/vhdl/python/src/python_context.vhd new file mode 100644 index 000000000..0b7b79b87 --- /dev/null +++ b/vunit/vhdl/python/src/python_context.vhd @@ -0,0 +1,13 @@ +-- This package provides a dictionary types and operations +-- +-- 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-2024, Lars Asplund lars.anders.asplund@gmail.com + +context python_context is + library vunit_lib; + use vunit_lib.python_pkg.all; + use vunit_lib.python_ffi_pkg.all; +end context; diff --git a/vunit/vhdl/python/src/python_pkg.c b/vunit/vhdl/python/src/python_pkg.c new file mode 100644 index 000000000..577018848 --- /dev/null +++ b/vunit/vhdl/python/src/python_pkg.c @@ -0,0 +1,121 @@ +// This package provides a dictionary types and operations +// +// 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-2023, Lars Asplund lars.anders.asplund@gmail.com + +#include "python_pkg.h" + +static py_error_handler_callback py_error_handler = NULL; +static ffi_error_handler_callback ffi_error_handler = NULL; + +void register_py_error_handler(py_error_handler_callback callback) { + py_error_handler = callback; +} + +void register_ffi_error_handler(ffi_error_handler_callback callback) { + ffi_error_handler = callback; +} + +char* get_string(PyObject* pyobj) { + PyObject* str = PyObject_Str(pyobj); + if (str == NULL) { + return NULL; + } + + PyObject* str_utf_8 = PyUnicode_AsEncodedString(str, "utf-8", NULL); + Py_DECREF(str); + if (str_utf_8 == NULL) { + return NULL; + } + + char* result = PyBytes_AS_STRING(str_utf_8); + Py_DECREF(str_utf_8); + + return result; +} + +void check_conversion_error(const char* expr) { + PyObject* exc = PyErr_Occurred(); + if (exc != NULL) { + Py_DECREF(exc); + py_error_handler("parsing evaluation result of", expr, NULL, true); + } +} + +void handle_type_check_error(PyObject* pyobj, const char* context, + const char* expr) { + PyTypeObject* type = pyobj->ob_type; + if (type == NULL) { + py_error_handler(context, expr, "Expression evaluates to an unknown type.", + true); + } + const char* type_name_str = type->tp_name; + Py_DECREF(type); + + if (type_name_str == NULL) { + py_error_handler(context, expr, "Expression evaluates to an unknown type.", + true); + } + + char error_message[100] = "Expression evaluates to "; + strncat(error_message, type_name_str, 75); + py_error_handler(context, expr, error_message, true); +} + +PyObject* eval(const char* expr) { + PyObject* pyobj = PyRun_String(expr, Py_eval_input, globals, locals); + if (pyobj == NULL) { + py_error_handler("evaluating", expr, NULL, true); + } + + return pyobj; +} + +int get_integer(PyObject* pyobj, const char* expr, bool dec_ref_count) { + // Check that the Python object has the correct type + if (!PyLong_Check(pyobj)) { + handle_type_check_error(pyobj, "evaluating to integer", expr); + } + + // Convert from Python-typed to C-typed value and check for any errors such as + // overflow/underflow + long value = PyLong_AsLong(pyobj); + if (dec_ref_count) { + Py_DECREF(pyobj); + } + check_conversion_error(expr); + + // TODO: Assume that the simulator is limited to 32-bits for now + if ((value > pow(2, 31) - 1) || (value < -pow(2, 31))) { + py_error_handler("parsing evaluation result of", expr, + "Result out of VHDL integer range.", true); + } + + return (int)value; +} + +double get_real(PyObject* pyobj, const char* expr, bool dec_ref_count) { + // Check that the Python object has the correct type + if (!PyFloat_Check(pyobj)) { + handle_type_check_error(pyobj, "evaluating to real", expr); + } + + // Convert from Python-typed to C-typed value and check for any errors such as + // overflow/underflow + double value = PyFloat_AsDouble(pyobj); + if (dec_ref_count) { + Py_DECREF(pyobj); + } + check_conversion_error(expr); + + // TODO: Assume that the simulator is limited to 32-bits for now + if ((value > 3.4028234664e38) || (value < -3.4028234664e38)) { + py_error_handler("parsing evaluation result of", expr, + "Result out of VHDL real range.", true); + } + + return value; +} diff --git a/vunit/vhdl/python/src/python_pkg.h b/vunit/vhdl/python/src/python_pkg.h new file mode 100644 index 000000000..950a8d555 --- /dev/null +++ b/vunit/vhdl/python/src/python_pkg.h @@ -0,0 +1,31 @@ +// This package provides a dictionary types and operations +// +// 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-2023, Lars Asplund lars.anders.asplund@gmail.com + +#include +#include + +#define PY_SSIZE_T_CLEAN +#include "Python.h" + +extern PyObject* globals; +extern PyObject* locals; + +typedef void (*py_error_handler_callback)(const char*, const char*, const char*, + bool); +void register_py_error_handler(py_error_handler_callback callback); + +typedef void (*ffi_error_handler_callback)(const char*, bool); +void register_ffi_error_handler(ffi_error_handler_callback callback); + +char* get_string(PyObject* pyobj); +void check_conversion_error(const char* expr); +void handle_type_check_error(PyObject* pyobj, const char* context, + const char* expr); +PyObject* eval(const char* expr); +int get_integer(PyObject* pyobj, const char* expr, bool dec_ref_count); +double get_real(PyObject* pyobj, const char* expr, bool dec_ref_count); diff --git a/vunit/vhdl/python/src/python_pkg.vhd b/vunit/vhdl/python/src/python_pkg.vhd new file mode 100644 index 000000000..25438d727 --- /dev/null +++ b/vunit/vhdl/python/src/python_pkg.vhd @@ -0,0 +1,186 @@ +-- This package provides a dictionary types and operations +-- +-- 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-2024, Lars Asplund lars.anders.asplund@gmail.com + +use work.python_ffi_pkg.all; +use work.path.all; +use work.run_pkg.all; +use work.runner_pkg.all; +use work.integer_vector_ptr_pkg.all; +use work.string_ops.all; + +use std.textio.all; + +package python_pkg is + -- TODO: Consider detecting and handling the case where the imported file + -- has no __name__ = "__main__" guard. This is typical for run scripts and + -- people not used to it will make mistakes. + procedure import_module_from_file(module_path, as_module_name : string); + procedure import_run_script(module_name : string := ""); + + function to_py_list_str(vec : integer_vector) return string; + impure function to_py_list_str(vec : integer_vector_ptr_t) return string; + function to_py_list_str(vec : real_vector) return string; + + function "+"(l, r : string) return string; + + impure function eval_integer_vector_ptr(expr : string) return integer_vector_ptr_t; + alias eval is eval_integer_vector_ptr[string return integer_vector_ptr_t]; + + -- TODO: Questa crashes unless this function is impure + impure function to_call_str( + identifier : string; arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : string := "" + ) return string; + + procedure call( + identifier : string; arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : string := "" + ); + + impure function call_integer( + identifier : string; arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : string := "" + ) return integer; + alias call is call_integer[string, string, string, string, string, + string, string, string, string, string, string return integer]; + +end package; + +package body python_pkg is + -- @formatter:off + procedure import_module_from_file(module_path, as_module_name : string) is + constant spec_name : string := "__" & as_module_name & "_spec"; + constant code : string := + "from importlib.util import spec_from_file_location, module_from_spec" & LF & + "from pathlib import Path" & LF & + "import sys" & LF & + spec_name & " = spec_from_file_location('" & as_module_name & "', str(Path('" & module_path & "')))" & LF & + as_module_name & " = module_from_spec(" & spec_name & ")" & LF & + "sys.modules['" & as_module_name & "'] = " & as_module_name & LF & + spec_name & ".loader.exec_module(" & as_module_name & ")"; + begin + exec(code); + end; + -- @formatter:on + + procedure import_run_script(module_name : string := "") is + constant script_path : string := run_script_path(get_cfg(runner_state)); + variable path_items : lines_t; + variable script_name : line; + begin + if module_name = "" then + -- Extract the last item in the full path + path_items := split(script_path, "/"); + for idx in path_items'range loop + if idx = path_items'right then + script_name := path_items(idx); + else + deallocate(path_items(idx)); + end if; + end loop; + deallocate(path_items); + + -- Set module name to script name minus its extension + path_items := split(script_name.all, "."); + deallocate(script_name); + for idx in path_items'range loop + if idx = path_items'left then + import_module_from_file(script_path, path_items(idx).all); + end if; + deallocate(path_items(idx)); + end loop; + deallocate(path_items); + else + import_module_from_file(script_path, module_name); + end if; + end; + + function to_py_list_str(vec : integer_vector) return string is + variable l : line; + begin + swrite(l, "["); + for idx in vec'range loop + swrite(l, to_string(vec(idx))); + if idx /= vec'right then + swrite(l, ","); + end if; + end loop; + swrite(l, "]"); + + return l.all; + end; + + impure function to_py_list_str(vec : integer_vector_ptr_t) return string is + variable l : line; + begin + swrite(l, "["); + for idx in 0 to length(vec) - 1 loop + swrite(l, to_string(get(vec, idx))); + if idx /= length(vec) - 1 then + swrite(l, ","); + end if; + end loop; + swrite(l, "]"); + + return l.all; + end; + + function to_py_list_str(vec : real_vector) return string is + variable l : line; + begin + swrite(l, "["); + for idx in vec'range loop + -- Inconsistency between simulators if to_string and/or real'image of 1.0 returns "1" or "1.0" + -- Enforce type with float() + swrite(l, "float(" & to_string(vec(idx)) & ")"); + if idx /= vec'right then + swrite(l, ","); + end if; + end loop; + swrite(l, "]"); + + return l.all; + end; + + function "+"(l, r : string) return string is + begin + return l & LF & r; + end; + + impure function eval_integer_vector_ptr(expr : string) return integer_vector_ptr_t is + constant result_integer_vector : integer_vector := eval(expr); + constant len : natural := result_integer_vector'length; + constant result_integer_vector_normalized : integer_vector(0 to len - 1) := result_integer_vector; + constant result : integer_vector_ptr_t := new_integer_vector_ptr(len); + begin + for idx in 0 to len - 1 loop + set(result, idx, result_integer_vector_normalized(idx)); + end loop; + + return result; + end; + + impure function to_call_str( + identifier : string; arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : string := "" + ) return string is + constant args : string := "('" & arg1 & "','" & arg2 & "','" & arg3 & "','" & arg4 & "','" & arg5 & "','" & arg6 & "','" & arg7 & "','" & arg8 & "','" & arg9 & "','" & arg10 & "')"; + begin + return eval_string("'" & identifier & "(' + ', '.join((arg for arg in " & args & " if arg)) + ')'"); + end; + + procedure call( + identifier : string; arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : string := "" + ) is + begin + exec(to_call_str(identifier, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10)); + end; + + impure function call_integer( + identifier : string; arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : string := "" + ) return integer is + begin + return eval(to_call_str(identifier, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10)); + end; +end package body; diff --git a/vunit/vhdl/python/src/python_pkg_fli.c b/vunit/vhdl/python/src/python_pkg_fli.c new file mode 100644 index 000000000..0b7b7d6ac --- /dev/null +++ b/vunit/vhdl/python/src/python_pkg_fli.c @@ -0,0 +1,215 @@ +// This package provides a dictionary types and operations +// +// 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-2023, Lars Asplund lars.anders.asplund@gmail.com + +#include +#include + +#include "mti.h" +#include "python_pkg.h" + +#define MAX_VHDL_PARAMETER_STRING_LENGTH 100000 + +PyObject* globals = NULL; +PyObject* locals = NULL; + +void python_cleanup(void); + +static void py_error_handler(const char* context, const char* code_or_expr, + const char* reason, bool cleanup) { + const char* unknown_error = "Unknown error"; + + // Use provided error reason or try extracting the reason from the Python + // exception + if (reason == NULL) { + PyObject *ptype, *pvalue, *ptraceback; + PyErr_Fetch(&ptype, &pvalue, &ptraceback); + if (ptype != NULL) { + reason = get_string(pvalue); + } + PyErr_Restore(ptype, pvalue, ptraceback); + } + + // Clean-up Python session first in case vhpi_assert stops the simulation + if (cleanup) { + python_cleanup(); + } + + // Output error message + reason = reason == NULL ? unknown_error : reason; + if (code_or_expr == NULL) { + printf("ERROR %s:\n\n%s\n\n", context, reason); + } else { + printf("ERROR %s:\n\n%s\n\n%s\n\n", context, code_or_expr, reason); + } + mti_FatalError(); +} + +static void ffi_error_handler(const char* context, bool cleanup) { + // Clean-up Python session first in case vhpi_assert stops the simulation + if (cleanup) { + python_cleanup(); + } + + printf("ERROR %s\n\n", context); + mti_FatalError(); +} + +void python_setup(void) { + Py_Initialize(); + if (!Py_IsInitialized()) { + ffi_error_handler("Failed to initialize Python", false); + } + + PyObject* main_module = PyImport_AddModule("__main__"); + if (main_module == NULL) { + ffi_error_handler("Failed to get the main module", true); + } + + globals = PyModule_GetDict(main_module); + if (globals == NULL) { + ffi_error_handler("Failed to get the global dictionary", true); + } + + // globals and locals are the same at the top-level + locals = globals; + + register_py_error_handler(py_error_handler); + register_ffi_error_handler(ffi_error_handler); + + // This class allow us to evaluate an expression and get the length of the + // result before getting the result. The length is used to allocate a VHDL + // array before getting the result. This saves us from passing and evaluating + // the expression twice (both when getting its length and its value). When + // only supporting Python 3.8+, this can be solved with the walrus operator: + // len(__eval_result__ := expr) + char* code = + "\ +class __EvalResult__():\n\ + def __init__(self):\n\ + self._result = None\n\ + def set(self, expr):\n\ + self._result = expr\n\ + return len(self._result)\n\ + def get(self):\n\ + return self._result\n\ +__eval_result__=__EvalResult__()\n"; + + if (PyRun_String(code, Py_file_input, globals, locals) == NULL) { + ffi_error_handler("Failed to initialize predefined Python objects", true); + } +} + +void python_cleanup(void) { + if (locals != NULL) { + Py_DECREF(locals); + } + + if (Py_FinalizeEx()) { + printf("WARNING: Failed to finalize Python\n"); + } +} + +static const char* get_parameter(mtiVariableIdT id) { + mtiTypeIdT type; + int len; + static char vhdl_parameter_string[MAX_VHDL_PARAMETER_STRING_LENGTH]; + + type = mti_GetVarType(id); + len = mti_TickLength(type); + mti_GetArrayVarValue(id, vhdl_parameter_string); + vhdl_parameter_string[len] = 0; + + return vhdl_parameter_string; +} + +int eval_integer(mtiVariableIdT id) { + const char* expr = get_parameter(id); + + // Eval(uate) expression in Python + PyObject* eval_result = eval(expr); + + // Return result to VHDL + return get_integer(eval_result, expr, true); +} + +mtiRealT eval_real(mtiVariableIdT id) { + const char* expr = get_parameter(id); + mtiRealT result; + + // Eval(uate) expression in Python + PyObject* eval_result = eval(expr); + + // Return result to VHDL + MTI_ASSIGN_TO_REAL(result, get_real(eval_result, expr, true)); + return result; +} + +void p_get_integer_vector(mtiVariableIdT vec) { + // Get evaluation result from Python + PyObject* eval_result = eval("__eval_result__.get()"); + + // Check that the eval results in a list. TODO: tuple and sets of integers + // should also work + if (!PyList_Check(eval_result)) { + handle_type_check_error(eval_result, "evaluating to integer_vector", + "__eval_result__.get()"); + } + + const int list_size = PyList_GET_SIZE(eval_result); + const int vec_len = mti_TickLength(mti_GetVarType(vec)); + int* arr = (int*)mti_GetArrayVarValue(vec, NULL); + + for (int idx = 0; idx < vec_len; idx++) { + arr[idx] = get_integer(PyList_GetItem(eval_result, idx), + "__eval_result__.get()", false); + } + Py_DECREF(eval_result); +} + +void p_get_real_vector(mtiVariableIdT vec) { + // Get evaluation result from Python + PyObject* eval_result = eval("__eval_result__.get()"); + + // Check that the eval results in a list. TODO: tuple and sets of integers + // should also work + if (!PyList_Check(eval_result)) { + handle_type_check_error(eval_result, "evaluating to real_vector", + "__eval_result__.get()"); + } + + const int list_size = PyList_GET_SIZE(eval_result); + const int vec_len = mti_TickLength(mti_GetVarType(vec)); + double* arr = (double*)mti_GetArrayVarValue(vec, NULL); + + for (int idx = 0; idx < vec_len; idx++) { + arr[idx] = get_real(PyList_GetItem(eval_result, idx), + "__eval_result__.get()", false); + } + Py_DECREF(eval_result); +} + +void p_get_string(mtiVariableIdT vec) { + // Get evaluation result from Python + PyObject* eval_result = eval("__eval_result__.get()"); + + const char* py_str = get_string(eval_result); + char* vhdl_str = (char*)mti_GetArrayVarValue(vec, NULL); + strcpy(vhdl_str, py_str); + + Py_DECREF(eval_result); +} + +void exec(mtiVariableIdT id) { + // Get code parameter from VHDL procedure call + const char* code = get_parameter(id); + + // Exec(ute) Python code + if (PyRun_String(code, Py_file_input, globals, locals) == NULL) { + py_error_handler("executing", code, NULL, true); + } +} diff --git a/vunit/vhdl/python/src/python_pkg_fli.vhd b/vunit/vhdl/python/src/python_pkg_fli.vhd new file mode 100644 index 000000000..3c9fc1501 --- /dev/null +++ b/vunit/vhdl/python/src/python_pkg_fli.vhd @@ -0,0 +1,120 @@ +-- This package provides a dictionary types and operations +-- +-- 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-2024, Lars Asplund lars.anders.asplund@gmail.com + +use std.textio.all; + +package python_ffi_pkg is + procedure python_setup; + attribute foreign of python_setup : procedure is "python_setup libraries/python/python_fli.so"; + procedure python_cleanup; + attribute foreign of python_cleanup : procedure is "python_cleanup libraries/python/python_fli.so"; + + function eval_integer(expr : string) return integer; + attribute foreign of eval_integer : function is "eval_integer libraries/python/python_fli.so"; + alias eval is eval_integer[string return integer]; + + function eval_real(expr : string) return real; + attribute foreign of eval_real : function is "eval_real libraries/python/python_fli.so"; + alias eval is eval_real[string return real]; + + function eval_integer_vector(expr : string) return integer_vector; + alias eval is eval_integer_vector[string return integer_vector]; + + procedure p_get_integer_vector(vec : out integer_vector); + attribute foreign of p_get_integer_vector : procedure is "p_get_integer_vector libraries/python/python_fli.so"; + + function eval_real_vector(expr : string) return real_vector; + alias eval is eval_real_vector[string return real_vector]; + + procedure p_get_real_vector(vec : out real_vector); + attribute foreign of p_get_real_vector : procedure is "p_get_real_vector libraries/python/python_fli.so"; + + function eval_string(expr : string) return string; + alias eval is eval_string[string return string]; + + procedure p_get_string(vec : out string); + attribute foreign of p_get_string : procedure is "p_get_string libraries/python/python_fli.so"; + + procedure exec(code : string); + attribute foreign of exec : procedure is "exec libraries/python/python_fli.so"; + +end package; + +package body python_ffi_pkg is + procedure python_setup is + begin + report "ERROR: Failed to call foreign subprogram" severity failure; + end; + + procedure python_cleanup is + begin + report "ERROR: Failed to call foreign subprogram" severity failure; + end; + + function eval_integer(expr : string) return integer is + begin + report "ERROR: Failed to call foreign subprogram" severity failure; + return -1; + end; + + function eval_real(expr : string) return real is + begin + report "ERROR: Failed to call foreign subprogram" severity failure; + return -1.0; + end; + + procedure exec(code : string) is + begin + report "ERROR: Failed to call foreign subprogram" severity failure; + end; + + function eval_integer_vector(expr : string) return integer_vector is + constant result_length : natural := eval_integer("__eval_result__.set(" & expr & ")"); + variable result : integer_vector(0 to result_length - 1); + begin + p_get_integer_vector(result); + + return result; + end; + + procedure p_get_integer_vector(vec : out integer_vector) is + begin + report "ERROR: Failed to call foreign subprogram" severity failure; + end; + + function eval_real_vector(expr : string) return real_vector is + constant result_length : natural := eval_integer("__eval_result__.set(" & expr & ")"); + variable result : real_vector(0 to result_length - 1); + begin + p_get_real_vector(result); + + return result; + end; + + procedure p_get_real_vector(vec : out real_vector) is + begin + report "ERROR: Failed to call foreign subprogram" severity failure; + end; + + function eval_string(expr : string) return string is + constant result_length : natural := eval_integer("__eval_result__.set(" & expr & ")"); + -- Add one character for the C null termination such that strcpy can be used. Do not return this + -- character + variable result : string(1 to result_length + 1); + begin + p_get_string(result); + + return result(1 to result_length); + end; + + procedure p_get_string(vec : out string) is + begin + report "ERROR: Failed to call foreign subprogram" severity failure; + end; + +end package body; diff --git a/vunit/vhdl/python/src/python_pkg_vhpi.c b/vunit/vhdl/python/src/python_pkg_vhpi.c new file mode 100644 index 000000000..e9860a014 --- /dev/null +++ b/vunit/vhdl/python/src/python_pkg_vhpi.c @@ -0,0 +1,335 @@ +// This package provides a dictionary types and operations +// +// 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-2023, Lars Asplund lars.anders.asplund@gmail.com + +#include +#include +#include + +#include "python_pkg.h" + +PyObject* globals = NULL; +PyObject* locals = NULL; + +#define MAX_VHDL_PARAMETER_STRING_LENGTH 100000 + +PLI_VOID python_cleanup(const struct vhpiCbDataS* cb_p); + +static void py_error_handler(const char* context, const char* code_or_expr, + const char* reason, bool cleanup) { + const char* unknown_error = "Unknown error"; + + // Use provided error reason or try extracting the reason from the Python + // exception + if (reason == NULL) { + PyObject *ptype, *pvalue, *ptraceback; + PyErr_Fetch(&ptype, &pvalue, &ptraceback); + if (ptype != NULL) { + reason = get_string(pvalue); + } + PyErr_Restore(ptype, pvalue, ptraceback); + } + + // Clean-up Python session first in case vhpi_assert stops the simulation + if (cleanup) { + python_cleanup(NULL); + } + + // Output error message + reason = reason == NULL ? unknown_error : reason; + if (code_or_expr == NULL) { + vhpi_assert(vhpiError, "ERROR %s:\n\n%s\n\n", context, reason); + } else { + vhpi_assert(vhpiError, "ERROR %s:\n\n%s\n\n%s\n\n", context, code_or_expr, + reason); + } + + // Stop the simulation if vhpi_assert didn't. + vhpi_control(vhpiStop); +} + +static void ffi_error_handler(const char* context, bool cleanup) { + vhpiErrorInfoT err; + + // Clean-up Python session first in case vhpi_assert stops the simulation + if (cleanup) { + python_cleanup(NULL); + } + + if (vhpi_check_error(&err)) { + vhpi_assert(err.severity, "ERROR %s: \n\n%s (%d): %s\n\n", context, + err.file, err.line, err.message); + + } else { + vhpi_assert(vhpiError, "ERROR %s\n\n", context); + } + + // Stop the simulation if vhpi_assert didn't. + vhpi_control(vhpiStop); +} + +PLI_VOID python_setup(const struct vhpiCbDataS* cb_p) { + Py_Initialize(); + if (!Py_IsInitialized()) { + ffi_error_handler("Failed to initialize Python", false); + } + + PyObject* main_module = PyImport_AddModule("__main__"); + if (main_module == NULL) { + ffi_error_handler("Failed to get the main module", true); + } + + globals = PyModule_GetDict(main_module); + if (globals == NULL) { + ffi_error_handler("Failed to get the global dictionary", true); + } + + // globals and locals are the same at the top-level + locals = globals; + + register_py_error_handler(py_error_handler); + register_ffi_error_handler(ffi_error_handler); +} + +PLI_VOID python_cleanup(const struct vhpiCbDataS* cb_p) { + if (locals != NULL) { + Py_DECREF(locals); + } + + if (Py_FinalizeEx()) { + vhpi_assert(vhpiWarning, "WARNING: Failed to finalize Python"); + } +} + +static const char* get_parameter(const struct vhpiCbDataS* cb_p) { + // Get parameter from VHDL function call + vhpiHandleT parameter_handle = + vhpi_handle_by_index(vhpiParamDecls, cb_p->obj, 0); + if (parameter_handle == NULL) { + ffi_error_handler("getting VHDL parameter handle", true); + } + + vhpiValueT parameter; + static char vhdl_parameter_string[MAX_VHDL_PARAMETER_STRING_LENGTH]; + + parameter.bufSize = MAX_VHDL_PARAMETER_STRING_LENGTH; + parameter.value.str = vhdl_parameter_string; + parameter.format = vhpiStrVal; + + if (vhpi_get_value(parameter_handle, ¶meter)) { + ffi_error_handler("getting VHDL parameter value", true); + } + + return vhdl_parameter_string; +} + +PLI_VOID eval_integer(const struct vhpiCbDataS* cb_p) { + // Get expression parameter from VHDL function call + const char* expr = get_parameter(cb_p); + + // Eval(uate) expression in Python + PyObject* eval_result = eval(expr); + + // Return result to VHDL + vhpiValueT vhdl_result; + vhdl_result.format = vhpiIntVal; + vhdl_result.bufSize = 0; + vhdl_result.value.intg = get_integer(eval_result, expr, true); + if (vhpi_put_value(cb_p->obj, &vhdl_result, vhpiDeposit)) { + py_error_handler("returning result for evaluation of", expr, NULL, true); + } +} + +PLI_VOID eval_real(const struct vhpiCbDataS* cb_p) { + // Get expression parameter from VHDL function call + const char* expr = get_parameter(cb_p); + + // Eval(uate) expression in Python + PyObject* eval_result = eval(expr); + + // Return result to VHDL + vhpiValueT vhdl_result; + vhdl_result.format = vhpiRealVal; + vhdl_result.bufSize = 0; + vhdl_result.value.real = get_real(eval_result, expr, true); + if (vhpi_put_value(cb_p->obj, &vhdl_result, vhpiDeposit)) { + py_error_handler("returning result for evaluation of", expr, NULL, true); + } +} + +PLI_VOID eval_integer_vector(const struct vhpiCbDataS* cb_p) { + // Get expression parameter from VHDL function call + const char* expr = get_parameter(cb_p); + + // Eval(uate) expression in Python + PyObject* pyobj = eval(expr); + + // Check that the eval results in a list. TODO: tuple and sets of integers + // should also work + if (!PyList_Check(pyobj)) { + handle_type_check_error(pyobj, "evaluating to integer_vector", expr); + } + + const int list_size = PyList_GET_SIZE(pyobj); + const int n_bytes = list_size * sizeof(int); + int* int_array = (int*)malloc(n_bytes); + + for (int idx = 0; idx < list_size; idx++) { + int_array[idx] = get_integer(PyList_GetItem(pyobj, idx), expr, false); + } + Py_DECREF(pyobj); + + // Return result to VHDL + vhpiValueT vhdl_result; + vhdl_result.format = vhpiIntVecVal; + vhdl_result.bufSize = n_bytes; + vhdl_result.numElems = list_size; + vhdl_result.value.intgs = (vhpiIntT*)int_array; + + if (vhpi_put_value(cb_p->obj, &vhdl_result, vhpiSizeConstraint)) { + free(int_array); + py_error_handler( + "setting size constraints when returning result for evaluation of", + expr, NULL, true); + } + + if (vhpi_put_value(cb_p->obj, &vhdl_result, vhpiDeposit)) { + free(int_array); + py_error_handler("returning result for evaluation of", expr, NULL, true); + } + + free(int_array); +} + +PLI_VOID eval_real_vector(const struct vhpiCbDataS* cb_p) { + // Get expression parameter from VHDL function call + const char* expr = get_parameter(cb_p); + + // Eval(uate) expression in Python + PyObject* pyobj = eval(expr); + + // Check that the eval results in a list. TODO: tuple and sets of integers + // should also work + if (!PyList_Check(pyobj)) { + handle_type_check_error(pyobj, "evaluating to real_vector", expr); + } + + const int list_size = PyList_GET_SIZE(pyobj); + const int n_bytes = list_size * sizeof(double); + double* double_array = (double*)malloc(n_bytes); + + for (int idx = 0; idx < list_size; idx++) { + double_array[idx] = get_real(PyList_GetItem(pyobj, idx), expr, false); + } + Py_DECREF(pyobj); + + // Return result to VHDL + vhpiValueT vhdl_result; + vhdl_result.format = vhpiRealVecVal; + vhdl_result.bufSize = n_bytes; + vhdl_result.numElems = list_size; + vhdl_result.value.reals = double_array; + + if (vhpi_put_value(cb_p->obj, &vhdl_result, vhpiSizeConstraint)) { + free(double_array); + py_error_handler( + "setting size constraints when returning result for evaluation of", + expr, NULL, true); + } + + if (vhpi_put_value(cb_p->obj, &vhdl_result, vhpiDeposit)) { + free(double_array); + py_error_handler("returning result for evaluation of", expr, NULL, true); + } + + free(double_array); +} + +PLI_VOID eval_string(const struct vhpiCbDataS* cb_p) { + // Get expression parameter from VHDL function call + const char* expr = get_parameter(cb_p); + + // Eval(uate) expression in Python + PyObject* pyobj = eval(expr); + + char* str = get_string(pyobj); + Py_DECREF(pyobj); + + // Return result to VHDL + vhpiValueT vhdl_result; + vhdl_result.format = vhpiStrVal; + vhdl_result.bufSize = strlen(str) + 1; // null termination included + vhdl_result.numElems = strlen(str); + vhdl_result.value.str = str; + + if (vhpi_put_value(cb_p->obj, &vhdl_result, vhpiSizeConstraint)) { + py_error_handler( + "setting size constraints when returning result for evaluation of", + expr, NULL, true); + } + + if (vhpi_put_value(cb_p->obj, &vhdl_result, vhpiDeposit)) { + py_error_handler("returning result for evaluation of", expr, NULL, true); + } +} + +PLI_VOID exec(const struct vhpiCbDataS* cb_p) { + // Get code parameter from VHDL procedure call + const char* code = get_parameter(cb_p); + + // Exec(ute) Python code + if (PyRun_String(code, Py_file_input, globals, locals) == NULL) { + py_error_handler("executing", code, NULL, true); + } +} + +PLI_VOID register_foreign_subprograms() { + char library_name[] = "python"; + + char python_setup_name[] = "python_setup"; + vhpiForeignDataT python_setup_data = {vhpiProcF, library_name, + python_setup_name, NULL, python_setup}; + // vhpi_assert doesn't seem to work at this point + assert(vhpi_register_foreignf(&python_setup_data) != NULL); + + char python_cleanup_name[] = "python_cleanup"; + vhpiForeignDataT python_cleanup_data = { + vhpiProcF, library_name, python_cleanup_name, NULL, python_cleanup}; + assert(vhpi_register_foreignf(&python_cleanup_data) != NULL); + + char eval_integer_name[] = "eval_integer"; + vhpiForeignDataT eval_integer_data = {vhpiProcF, library_name, + eval_integer_name, NULL, eval_integer}; + assert(vhpi_register_foreignf(&eval_integer_data) != NULL); + + char eval_real_name[] = "eval_real"; + vhpiForeignDataT eval_real_data = {vhpiProcF, library_name, eval_real_name, + NULL, eval_real}; + assert(vhpi_register_foreignf(&eval_real_data) != NULL); + + char eval_integer_vector_name[] = "eval_integer_vector"; + vhpiForeignDataT eval_integer_vector_data = {vhpiProcF, library_name, + eval_integer_vector_name, NULL, + eval_integer_vector}; + assert(vhpi_register_foreignf(&eval_integer_vector_data) != NULL); + + char eval_real_vector_name[] = "eval_real_vector"; + vhpiForeignDataT eval_real_vector_data = { + vhpiProcF, library_name, eval_real_vector_name, NULL, eval_real_vector}; + assert(vhpi_register_foreignf(&eval_real_vector_data) != NULL); + + char eval_string_name[] = "eval_string"; + vhpiForeignDataT eval_string_data = {vhpiProcF, library_name, + eval_string_name, NULL, eval_string}; + assert(vhpi_register_foreignf(&eval_string_data) != NULL); + + char exec_name[] = "exec"; + vhpiForeignDataT exec_data = {vhpiProcF, library_name, exec_name, NULL, exec}; + assert(vhpi_register_foreignf(&exec_data) != NULL); +} + +PLI_VOID (*vhpi_startup_routines[])() = {register_foreign_subprograms, NULL}; diff --git a/vunit/vhdl/python/src/python_pkg_vhpi.vhd b/vunit/vhdl/python/src/python_pkg_vhpi.vhd new file mode 100644 index 000000000..a675eb37c --- /dev/null +++ b/vunit/vhdl/python/src/python_pkg_vhpi.vhd @@ -0,0 +1,44 @@ +-- This package provides a dictionary types and operations +-- +-- 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-2024, Lars Asplund lars.anders.asplund@gmail.com + +use std.textio.all; + +package python_ffi_pkg is + procedure python_setup; + -- TODO: Looks like Riviera-PRO requires the path to the shared library to be fixed at compile time + -- and that may become a bit limited. VHDL standard allow for expressions. + attribute foreign of python_setup : procedure is "VHPI libraries/python; python_setup"; + procedure python_cleanup; + attribute foreign of python_cleanup : procedure is "VHPI libraries/python; python_cleanup"; + + function eval_integer(expr : string) return integer; + attribute foreign of eval_integer : function is "VHPI libraries/python; eval_integer"; + alias eval is eval_integer[string return integer]; + + function eval_real(expr : string) return real; + attribute foreign of eval_real : function is "VHPI libraries/python; eval_real"; + alias eval is eval_real[string return real]; + + function eval_integer_vector(expr : string) return integer_vector; + attribute foreign of eval_integer_vector : function is "VHPI libraries/python; eval_integer_vector"; + alias eval is eval_integer_vector[string return integer_vector]; + + function eval_real_vector(expr : string) return real_vector; + attribute foreign of eval_real_vector : function is "VHPI libraries/python; eval_real_vector"; + alias eval is eval_real_vector[string return real_vector]; + + function eval_string(expr : string) return string; + attribute foreign of eval_string : function is "VHPI libraries/python; eval_string"; + alias eval is eval_string[string return string]; + + procedure exec(code : string); + attribute foreign of exec : procedure is "VHPI libraries/python; exec"; +end package; + +package body python_ffi_pkg is +end package body; diff --git a/vunit/vhdl/python/src/python_pkg_vhpidirect_ghdl.c b/vunit/vhdl/python/src/python_pkg_vhpidirect_ghdl.c new file mode 100644 index 000000000..563f4a81c --- /dev/null +++ b/vunit/vhdl/python/src/python_pkg_vhpidirect_ghdl.c @@ -0,0 +1,220 @@ +// This package provides a dictionary types and operations +// +// 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-2023, Lars Asplund lars.anders.asplund@gmail.com + +#include "python_pkg.h" + +PyObject* globals = NULL; +PyObject* locals = NULL; + +#define MAX_VHDL_PARAMETER_STRING_LENGTH 100000 + +void python_cleanup(void); + +static void py_error_handler(const char* context, const char* code_or_expr, + const char* reason, bool cleanup) { + const char* unknown_error = "Unknown error"; + + // Use provided error reason or try extracting the reason from the Python + // exception + if (reason == NULL) { + PyObject *ptype, *pvalue, *ptraceback; + PyErr_Fetch(&ptype, &pvalue, &ptraceback); + if (ptype != NULL) { + reason = get_string(pvalue); + } + PyErr_Restore(ptype, pvalue, ptraceback); + } + + // Clean-up Python session first in case vhpi_assert stops the simulation + if (cleanup) { + python_cleanup(); + } + + // Output error message + reason = reason == NULL ? unknown_error : reason; + if (code_or_expr == NULL) { + printf("ERROR %s:\n\n%s\n\n", context, reason); + } else { + printf("ERROR %s:\n\n%s\n\n%s\n\n", context, code_or_expr, reason); + } + assert(0); +} + +static void ffi_error_handler(const char* context, bool cleanup) { + // Clean-up Python session first in case vhpi_assert stops the simulation + if (cleanup) { + python_cleanup(); + } + + printf("ERROR %s\n\n", context); + assert(0); +} + +void python_setup(void) { + // See https://github.com/msys2/MINGW-packages/issues/18984 + putenv("PYTHONLEGACYWINDOWSDLLLOADING=1"); + Py_SetPythonHome(L"c:\\msys64\\mingw64"); + Py_Initialize(); + if (!Py_IsInitialized()) { + ffi_error_handler("Failed to initialize Python", false); + } + + PyObject* main_module = PyImport_AddModule("__main__"); + if (main_module == NULL) { + ffi_error_handler("Failed to get the main module", true); + } + + globals = PyModule_GetDict(main_module); + if (globals == NULL) { + ffi_error_handler("Failed to get the global dictionary", true); + } + + // globals and locals are the same at the top-level + locals = globals; + + register_py_error_handler(py_error_handler); + register_ffi_error_handler(ffi_error_handler); + + // This class allow us to evaluate an expression and get the length of the + // result before getting the result. The length is used to allocate a VHDL + // array before getting the result. This saves us from passing and evaluating + // the expression twice (both when getting its length and its value). When + // only supporting Python 3.8+, this can be solved with the walrus operator: + // len(__eval_result__ := expr) + char* code = + "\ +class __EvalResult__():\n\ + def __init__(self):\n\ + self._result = None\n\ + def set(self, expr):\n\ + self._result = expr\n\ + return len(self._result)\n\ + def get(self):\n\ + return self._result\n\ +__eval_result__=__EvalResult__()\n"; + + if (PyRun_String(code, Py_file_input, globals, locals) == NULL) { + ffi_error_handler("Failed to initialize predefined Python objects", true); + } +} + +void python_cleanup(void) { + + if (locals != NULL) { + Py_DECREF(locals); + } + + if (Py_FinalizeEx()) { + printf("WARNING: Failed to finalize Python\n"); + } +} + +typedef struct { + int32_t left; + int32_t right; + int32_t dir; + int32_t len; +} range_t; + +typedef struct { + range_t dim_1; +} bounds_t; + +typedef struct { + void* arr; + bounds_t* bounds; +} ghdl_arr_t; + +static const char* get_parameter(ghdl_arr_t* expr) { + static char vhdl_parameter_string[MAX_VHDL_PARAMETER_STRING_LENGTH]; + int length = expr->bounds->dim_1.len; + + strncpy(vhdl_parameter_string, expr->arr, length); + vhdl_parameter_string[length] = '\0'; + + return vhdl_parameter_string; +} + +int eval_integer(ghdl_arr_t* expr) { + // Get null-terminated expression parameter from VHDL function call + const char *param = get_parameter(expr); + + // Eval(uate) expression in Python + PyObject* eval_result = eval(param); + + // Return result to VHDL + return get_integer(eval_result, param, true); +} + +double eval_real(ghdl_arr_t* expr) { + // Get null-terminated expression parameter from VHDL function call + const char *param = get_parameter(expr); + + // Eval(uate) expression in Python + PyObject* eval_result = eval(param); + + // Return result to VHDL + return get_real(eval_result, param, true); +} + +void get_integer_vector(ghdl_arr_t* vec) { + // Get evaluation result from Python + PyObject* eval_result = eval("__eval_result__.get()"); + + // Check that the eval results in a list. TODO: tuple and sets of integers + // should also work + if (!PyList_Check(eval_result)) { + handle_type_check_error(eval_result, "evaluating to integer_vector", + "__eval_result__.get()"); + } + + for (int idx = 0; idx < PyList_Size(eval_result); idx++) { + ((int *)vec->arr)[idx] = get_integer(PyList_GetItem(eval_result, idx), + "__eval_result__.get()", false); + } + + Py_DECREF(eval_result); +} + +void get_real_vector(ghdl_arr_t* vec) { + // Get evaluation result from Python + PyObject* eval_result = eval("__eval_result__.get()"); + + // Check that the eval results in a list. TODO: tuple and sets of integers + // should also work + if (!PyList_Check(eval_result)) { + handle_type_check_error(eval_result, "evaluating to real_vector", + "__eval_result__.get()"); + } + + for (int idx = 0; idx < PyList_Size(eval_result); idx++) { + ((double *)vec->arr)[idx] = get_real(PyList_GetItem(eval_result, idx), + "__eval_result__.get()", false); + } + Py_DECREF(eval_result); +} + +void get_py_string(ghdl_arr_t* vec) { + // Get evaluation result from Python + PyObject* eval_result = eval("__eval_result__.get()"); + + const char* py_str = get_string(eval_result); + strcpy((char *)vec->arr, py_str); + + Py_DECREF(eval_result); +} + +void exec(ghdl_arr_t* code) { + // Get null-terminated code parameter from VHDL function call + const char *param = get_parameter(code); + + // Exec(ute) Python code + if (PyRun_String(param, Py_file_input, globals, locals) == NULL) { + py_error_handler("executing", param, NULL, true); + } +} diff --git a/vunit/vhdl/python/src/python_pkg_vhpidirect_ghdl.vhd b/vunit/vhdl/python/src/python_pkg_vhpidirect_ghdl.vhd new file mode 100644 index 000000000..3bffbb8e6 --- /dev/null +++ b/vunit/vhdl/python/src/python_pkg_vhpidirect_ghdl.vhd @@ -0,0 +1,118 @@ +-- This package provides a dictionary types and operations +-- +-- 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-2024, Lars Asplund lars.anders.asplund@gmail.com + +use std.textio.all; + +package python_ffi_pkg is + procedure python_setup; + attribute foreign of python_setup: procedure is "VHPIDIRECT python.so python_setup"; + procedure python_cleanup; + attribute foreign of python_cleanup: procedure is "VHPIDIRECT python.so python_cleanup"; + + function eval_integer(expr : string) return integer; + attribute foreign of eval_integer : function is "VHPIDIRECT python.so eval_integer"; + alias eval is eval_integer[string return integer]; + + function eval_real(expr : string) return real; + attribute foreign of eval_real : function is "VHPIDIRECT python.so eval_real"; + alias eval is eval_real[string return real]; + + function eval_integer_vector(expr : string) return integer_vector; + alias eval is eval_integer_vector[string return integer_vector]; + + function eval_real_vector(expr : string) return real_vector; + alias eval is eval_real_vector[string return real_vector]; + + function eval_string(expr : string) return string; + alias eval is eval_string[string return string]; + + procedure exec(code : string); + attribute foreign of exec : procedure is "VHPIDIRECT python.so exec"; + + procedure get_integer_vector(vec : out integer_vector); + attribute foreign of get_integer_vector : procedure is "VHPIDIRECT python.so get_integer_vector"; + + procedure get_real_vector(vec : out real_vector); + attribute foreign of get_real_vector : procedure is "VHPIDIRECT python.so get_real_vector"; + + procedure get_py_string(vec : out string); + attribute foreign of get_py_string : procedure is "VHPIDIRECT python.so get_py_string"; + +end package; + +package body python_ffi_pkg is + procedure python_setup is + begin + report "VHPIDIRECT python_setup" severity failure; + end; + + procedure python_cleanup is + begin + report "VHPIDIRECT python_cleanup" severity failure; + end; + + function eval_integer(expr : string) return integer is + begin + report "VHPIDIRECT eval_integer" severity failure; + end; + + function eval_real(expr : string) return real is + begin + report "VHPIDIRECT eval_real" severity failure; + end; + + procedure get_integer_vector(vec : out integer_vector) is + begin + report "ERROR: Failed to call foreign subprogram" severity failure; + end; + + function eval_integer_vector(expr : string) return integer_vector is + constant result_length : natural := eval_integer("__eval_result__.set(" & expr & ")"); + variable result : integer_vector(0 to result_length - 1); + begin + get_integer_vector(result); + + return result; + end; + + procedure get_real_vector(vec : out real_vector) is + begin + report "ERROR: Failed to call foreign subprogram" severity failure; + end; + + function eval_real_vector(expr : string) return real_vector is + constant result_length : natural := eval_integer("__eval_result__.set(" & expr & ")"); + variable result : real_vector(0 to result_length - 1); + begin + get_real_vector(result); + + return result; + end; + + procedure get_py_string(vec : out string) is + begin + report "ERROR: Failed to call foreign subprogram" severity failure; + end; + + function eval_string(expr : string) return string is + constant result_length : natural := eval_integer("__eval_result__.set(" & expr & ")"); + -- Add one character for the C null termination such that strcpy can be used. Do not return this + -- character + variable result : string(1 to result_length + 1); + begin + get_py_string(result); + + return result(1 to result_length); + end; + + procedure exec(code : string) is + begin + report "ERROR: Failed to call foreign subprogram" severity failure; + end; + +end package body; diff --git a/vunit/vhdl/python/src/python_pkg_vhpidirect_nvc.c b/vunit/vhdl/python/src/python_pkg_vhpidirect_nvc.c new file mode 100644 index 000000000..45d403901 --- /dev/null +++ b/vunit/vhdl/python/src/python_pkg_vhpidirect_nvc.c @@ -0,0 +1,204 @@ +// This package provides a dictionary types and operations +// +// 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-2023, Lars Asplund lars.anders.asplund@gmail.com + +#include "python_pkg.h" + +PyObject* globals = NULL; +PyObject* locals = NULL; + +#define MAX_VHDL_PARAMETER_STRING_LENGTH 100000 + +void python_cleanup(void); + +static void py_error_handler(const char* context, const char* code_or_expr, + const char* reason, bool cleanup) { + const char* unknown_error = "Unknown error"; + + // Use provided error reason or try extracting the reason from the Python + // exception + if (reason == NULL) { + PyObject *ptype, *pvalue, *ptraceback; + PyErr_Fetch(&ptype, &pvalue, &ptraceback); + if (ptype != NULL) { + reason = get_string(pvalue); + } + PyErr_Restore(ptype, pvalue, ptraceback); + } + + // Clean-up Python session first in case vhpi_assert stops the simulation + if (cleanup) { + python_cleanup(); + } + + // Output error message + reason = reason == NULL ? unknown_error : reason; + if (code_or_expr == NULL) { + printf("ERROR %s:\n\n%s\n\n", context, reason); + } else { + printf("ERROR %s:\n\n%s\n\n%s\n\n", context, code_or_expr, reason); + } + assert(0); +} + +static void ffi_error_handler(const char* context, bool cleanup) { + // Clean-up Python session first in case vhpi_assert stops the simulation + if (cleanup) { + python_cleanup(); + } + + printf("ERROR %s\n\n", context); + assert(0); +} + +void python_setup(void) { + Py_Initialize(); + if (!Py_IsInitialized()) { + ffi_error_handler("Failed to initialize Python", false); + } + + PyObject* main_module = PyImport_AddModule("__main__"); + if (main_module == NULL) { + ffi_error_handler("Failed to get the main module", true); + } + + globals = PyModule_GetDict(main_module); + if (globals == NULL) { + ffi_error_handler("Failed to get the global dictionary", true); + } + + // globals and locals are the same at the top-level + locals = globals; + + register_py_error_handler(py_error_handler); + register_ffi_error_handler(ffi_error_handler); + + // This class allow us to evaluate an expression and get the length of the + // result before getting the result. The length is used to allocate a VHDL + // array before getting the result. This saves us from passing and evaluating + // the expression twice (both when getting its length and its value). When + // only supporting Python 3.8+, this can be solved with the walrus operator: + // len(__eval_result__ := expr) + char* code = + "\ +class __EvalResult__():\n\ + def __init__(self):\n\ + self._result = None\n\ + def set(self, expr):\n\ + self._result = expr\n\ + return len(self._result)\n\ + def get(self):\n\ + return self._result\n\ +__eval_result__=__EvalResult__()\n"; + + + if (PyRun_String(code, Py_file_input, globals, locals) == NULL) { + ffi_error_handler("Failed to initialize predefined Python objects", true); + } +} + +void python_cleanup(void) { + if (locals != NULL) { + Py_DECREF(locals); + } + + + if (Py_FinalizeEx()) { + printf("WARNING: Failed to finalize Python\n"); + } +} + +static const char* get_parameter(const char* expr, int64_t length) { + static char vhdl_parameter_string[MAX_VHDL_PARAMETER_STRING_LENGTH]; + + memcpy(vhdl_parameter_string, expr, sizeof(char) * length); + vhdl_parameter_string[length] = '\0'; + + return vhdl_parameter_string; +} + +int eval_integer(const char* expr, int64_t length) { + // Get null-terminated expression parameter from VHDL function call + const char *param = get_parameter(expr, length); + + // Eval(uate) expression in Python + PyObject* eval_result = eval(param); + + // Return result to VHDL + return get_integer(eval_result, param, true); +} + +double eval_real(const char* expr, int64_t length) { + // Get null-terminated expression parameter from VHDL function call + const char *param = get_parameter(expr, length); + + // Eval(uate) expression in Python + PyObject* eval_result = eval(param); + + // Return result to VHDL + return get_real(eval_result, param, true); +} + +void get_integer_vector(int* vec, int64_t length) { + // Get evaluation result from Python + PyObject* eval_result = eval("__eval_result__.get()"); + + // Check that the eval results in a list. TODO: tuple and sets of integers + // should also work + if (!PyList_Check(eval_result)) { + handle_type_check_error(eval_result, "evaluating to integer_vector", + "__eval_result__.get()"); + } + + for (int idx = 0; idx < length; idx++) { + vec[idx] = get_integer(PyList_GetItem(eval_result, idx), + "__eval_result__.get()", false); + } + Py_DECREF(eval_result); +} + +void get_real_vector(double* vec, int64_t length) { + // Get evaluation result from Python + PyObject* eval_result = eval("__eval_result__.get()"); + + // Check that the eval results in a list. TODO: tuple and sets of integers + // should also work + if (!PyList_Check(eval_result)) { + handle_type_check_error(eval_result, "evaluating to real_vector", + "__eval_result__.get()"); + } + + for (int idx = 0; idx < length; idx++) { + vec[idx] = get_real(PyList_GetItem(eval_result, idx), + "__eval_result__.get()", false); + } + Py_DECREF(eval_result); +} + +void get_py_string(char* vec, int64_t length) { + // Get evaluation result from Python + PyObject* eval_result = eval("__eval_result__.get()"); + + const char* py_str = get_string(eval_result); + strcpy(vec, py_str); + + Py_DECREF(eval_result); +} + +void exec(const char* code, int64_t length) { + // Get null-terminated code parameter from VHDL function call + const char *param = get_parameter(code, length); + + // Exec(ute) Python code + if (PyRun_String(param, Py_file_input, globals, locals) == NULL) { + py_error_handler("executing", param, NULL, true); + } +} + +void (*vhpi_startup_routines[])() = { + NULL +}; diff --git a/vunit/vhdl/python/src/python_pkg_vhpidirect_nvc.vhd b/vunit/vhdl/python/src/python_pkg_vhpidirect_nvc.vhd new file mode 100644 index 000000000..609da8dcd --- /dev/null +++ b/vunit/vhdl/python/src/python_pkg_vhpidirect_nvc.vhd @@ -0,0 +1,119 @@ +-- This package provides a dictionary types and operations +-- +-- 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-2024, Lars Asplund lars.anders.asplund@gmail.com + +use std.textio.all; + +package python_ffi_pkg is + procedure python_setup; + attribute foreign of python_setup: procedure is "VHPIDIRECT python_setup"; + procedure python_cleanup; + attribute foreign of python_cleanup: procedure is "VHPIDIRECT python_cleanup"; + + function eval_integer(expr : string) return integer; + attribute foreign of eval_integer : function is "VHPIDIRECT eval_integer"; + alias eval is eval_integer[string return integer]; + + function eval_real(expr : string) return real; + attribute foreign of eval_real : function is "VHPIDIRECT eval_real"; + alias eval is eval_real[string return real]; + + function eval_integer_vector(expr : string) return integer_vector; + alias eval is eval_integer_vector[string return integer_vector]; + + function eval_real_vector(expr : string) return real_vector; + alias eval is eval_real_vector[string return real_vector]; + + function eval_string(expr : string) return string; + alias eval is eval_string[string return string]; + + -- TODO: Impure all functions + procedure exec(code : string); + attribute foreign of exec : procedure is "VHPIDIRECT exec"; + + procedure get_integer_vector(vec : out integer_vector); + attribute foreign of get_integer_vector : procedure is "VHPIDIRECT get_integer_vector"; + + procedure get_real_vector(vec : out real_vector); + attribute foreign of get_real_vector : procedure is "VHPIDIRECT get_real_vector"; + + procedure get_py_string(vec : out string); + attribute foreign of get_py_string : procedure is "VHPIDIRECT get_py_string"; + +end package; + +package body python_ffi_pkg is + procedure python_setup is + begin + report "VHPIDIRECT python_setup" severity failure; + end; + + procedure python_cleanup is + begin + report "VHPIDIRECT python_cleanup" severity failure; + end; + + function eval_integer(expr : string) return integer is + begin + report "VHPIDIRECT eval_integer" severity failure; + end; + + function eval_real(expr : string) return real is + begin + report "VHPIDIRECT eval_real" severity failure; + end; + + procedure get_integer_vector(vec : out integer_vector) is + begin + report "ERROR: Failed to call foreign subprogram" severity failure; + end; + + function eval_integer_vector(expr : string) return integer_vector is + constant result_length : natural := eval_integer("__eval_result__.set(" & expr & ")"); + variable result : integer_vector(0 to result_length - 1); + begin + get_integer_vector(result); + + return result; + end; + + procedure get_real_vector(vec : out real_vector) is + begin + report "ERROR: Failed to call foreign subprogram" severity failure; + end; + + function eval_real_vector(expr : string) return real_vector is + constant result_length : natural := eval_integer("__eval_result__.set(" & expr & ")"); + variable result : real_vector(0 to result_length - 1); + begin + get_real_vector(result); + + return result; + end; + + procedure get_py_string(vec : out string) is + begin + report "ERROR: Failed to call foreign subprogram" severity failure; + end; + + function eval_string(expr : string) return string is + constant result_length : natural := eval_integer("__eval_result__.set(" & expr & ")"); + -- Add one character for the C null termination such that strcpy can be used. Do not return this + -- character + variable result : string(1 to result_length + 1); + begin + get_py_string(result); + + return result(1 to result_length); + end; + + procedure exec(code : string) is + begin + report "ERROR: Failed to call foreign subprogram" severity failure; + end; + +end package body; diff --git a/vunit/vhdl/python/test/tb_python_pkg.vhd b/vunit/vhdl/python/test/tb_python_pkg.vhd new file mode 100644 index 000000000..07f60916b --- /dev/null +++ b/vunit/vhdl/python/test/tb_python_pkg.vhd @@ -0,0 +1,240 @@ +-- 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-2024, Lars Asplund lars.anders.asplund@gmail.com + +library vunit_lib; +context vunit_lib.vunit_context; +-- Keep this as an add-on while developing +-- but eventually I think it should be included +-- by default for supporting simulators +context vunit_lib.python_context; +use vunit_lib.runner_pkg.all; + +library ieee; +use ieee.math_real.all; + +entity tb_python_pkg is + generic(runner_cfg : string); +end entity; + +architecture tb of tb_python_pkg is +begin + test_runner : process + constant empty_integer_vector : integer_vector(0 downto 1) := (others => 0); + constant empty_real_vector : real_vector(0 downto 1) := (others => 0.0); + constant test_real_vector : real_vector := (-3.4028234664e38, -1.9, 0.0, 1.1, -3.4028234664e38); + constant max_int : integer := 2 ** 30 - 1 + 2 ** 30; + constant min_int : integer := -2 ** 30 - 2 ** 30; + + variable vhdl_int : integer; + variable vhdl_real : real; + variable vhdl_real_vector : real_vector(test_real_vector'range); + variable vhdl_integer_vector_ptr : integer_vector_ptr_t; + variable vhdl_integer_vector : integer_vector(0 to 3); + + begin + test_runner_setup(runner, runner_cfg); + -- While developing, python_setup and cleanup are separate + -- procedures. Eventually they will probably be part of test_runner_setup + -- and cleanup + python_setup; + + show(display_handler, debug); + + -- @formatter:off + while test_suite loop + --------------------------------------------------------------------- + -- Test eval of different types + --------------------------------------------------------------------- + if run("Test eval of integer expression") then + check_equal(eval("17"), 17); + check_equal(eval("2**31 - 1"), max_int); + check_equal(eval("-2**31"), min_int); + + elsif run("Test eval of integer with overflow from Python to C") then + vhdl_int := eval("2**63"); + + elsif run("Test eval of integer with underflow from Python to C") then + vhdl_int := eval("-2**63 - 1"); + + elsif run("Test eval of integer with overflow from C to VHDL") then + vhdl_int := eval("2**31"); + + elsif run("Test eval of integer with underflow from C to VHDL") then + vhdl_int := eval("-2**31 - 1"); + + elsif run("Test eval of real expression") then + check_equal(eval("3.40282346e38"), 3.40282346e38); + check_equal(eval("1.1754943508e-38"), 1.1754943508e-38); + check_equal(eval("-3.4028e38"), -3.4028e38); + check_equal(eval("-1.1754943508e-38"), -1.1754943508e-38); + + elsif run("Test eval of real with overflow from C to VHDL") then + vhdl_real := eval("3.4028234665e38"); + + elsif run("Test eval of real with underflow from C to VHDL") then + vhdl_real := eval("-3.4028234665e38"); + + elsif run("Test converting integer_vector to Python list string") then + check_equal(to_py_list_str(empty_integer_vector), "[]"); + check_equal(to_py_list_str(integer_vector'(0 => 1)), "[1]"); + check_equal(to_py_list_str(integer_vector'(-1, 0, 1)), "[-1,0,1]"); + + elsif run("Test eval of integer_vector expression") then + check(eval(to_py_list_str(empty_integer_vector)) = empty_integer_vector); + check(eval(to_py_list_str(integer_vector'(0 => 17))) = integer_vector'(0 => 17)); + check(eval(to_py_list_str(integer_vector'(min_int, -1, 0, 1, max_int))) = + integer_vector'(min_int, -1, 0, 1, max_int) + ); + + elsif run("Test converting integer_vector_ptr to Python list string") then + vhdl_integer_vector_ptr := new_integer_vector_ptr; + check_equal(to_py_list_str(vhdl_integer_vector_ptr), "[]"); + + vhdl_integer_vector_ptr := new_integer_vector_ptr(1); + set(vhdl_integer_vector_ptr, 0, 1); + check_equal(to_py_list_str(vhdl_integer_vector_ptr), "[1]"); + + vhdl_integer_vector_ptr := new_integer_vector_ptr(3); + for idx in 0 to 2 loop + set(vhdl_integer_vector_ptr, idx, idx - 1); + end loop; + check_equal(to_py_list_str(vhdl_integer_vector_ptr), "[-1,0,1]"); + + elsif run("Test eval of integer_vector_ptr expression") then + check_equal(length(eval(to_py_list_str(new_integer_vector_ptr))), 0); + + vhdl_integer_vector_ptr := eval(to_py_list_str(integer_vector'(0 => 17))); + check_equal(get(vhdl_integer_vector_ptr, 0), 17); + + vhdl_integer_vector_ptr := eval(to_py_list_str(integer_vector'(min_int, -1, 0, 1, max_int))); + check_equal(get(vhdl_integer_vector_ptr, 0), min_int); + check_equal(get(vhdl_integer_vector_ptr, 1), -1); + check_equal(get(vhdl_integer_vector_ptr, 2), 0); + check_equal(get(vhdl_integer_vector_ptr, 3), 1); + check_equal(get(vhdl_integer_vector_ptr, 4), max_int); + + elsif run("Test eval of string expression") then + check_equal(eval("''"), string'("")); + check_equal(eval("'\\'"), string'("\")); + check_equal(eval_string("'Hello from VUnit'"), "Hello from VUnit"); + + -- TODO: We could use a helper function converting newlines to VHDL linefeeds + check_equal(eval_string("'Hello\\nWorld'"), "Hello\nWorld"); + + elsif run("Test converting real_vector to Python list string") then + check_equal(to_py_list_str(empty_real_vector), "[]"); + -- TODO: real'image creates a scientific notation with an arbitrary number of + -- digits that makes the string representation hard to predict/verify. + -- check_equal(to_py_list_str(real_vector'(0 => 1.1)), "[1.1]"); + -- check_equal(to_py_list_str(real_vector'(-1.1, 0.0, 1.3)), "[-1.1,0.0,1.3]"); + + elsif run("Test eval of real_vector expression") then + check(eval(to_py_list_str(empty_real_vector)) = empty_real_vector); + check(eval(to_py_list_str(real_vector'(0 => 17.0))) = real_vector'(0 => 17.0)); + vhdl_real_vector := eval(to_py_list_str(test_real_vector)); + for idx in vhdl_real_vector'range loop + check_equal(vhdl_real_vector(idx), vhdl_real_vector(idx)); + end loop; + + --------------------------------------------------------------------- + -- Test exec + --------------------------------------------------------------------- + elsif run("Test basic exec") then + exec("py_int = 21"); + check_equal(eval("py_int"), 21); + + elsif run("Test exec with multiple code snippets separated by a semicolon") then + exec("a = 1; b = 2"); + check_equal(eval("a"), 1); + check_equal(eval("b"), 2); + + elsif run("Test exec with multiple code snippets separated by a newline") then + exec( + "a = 1" & LF & + "b = 2" + ); + check_equal(eval("a"), 1); + check_equal(eval("b"), 2); + + elsif run("Test exec with code construct with indentation") then + exec( + "a = [None] * 2" & LF & + "for idx in range(len(a)):" & LF & + " a[idx] = idx" + ); + + check_equal(eval("a[0]"), 0); + check_equal(eval("a[1]"), 1); + + elsif run("Test a simpler multiline syntax") then + exec( + "a = [None] * 2" + + "for idx in range(len(a)):" + + " a[idx] = idx" + ); + + check_equal(eval("a[0]"), 0); + check_equal(eval("a[1]"), 1); + + elsif run("Test exec of locally defined function") then + exec( + "def local_test():" & LF & + " return 1" + ); + + check_equal(eval("local_test()"), 1); + + elsif run("Test exec of function defined in run script") then + import_run_script; + check_equal(eval("run.remote_test()"), 2); + + import_run_script("my_run_script"); + check_equal(eval("my_run_script.remote_test()"), 2); + + exec("from my_run_script import remote_test"); + check_equal(eval("remote_test()"), 2); + + --------------------------------------------------------------------- + -- Test error handling + --------------------------------------------------------------------- + elsif run("Test exceptions in exec") then + exec( + "doing_something_right = 17" & LF & + "doing_something_wrong = doing_something_right_misspelled" + ); + + elsif run("Test exceptions in eval") then + vhdl_int := eval("1 / 0"); + + elsif run("Test eval with type error") then + vhdl_int := eval("10 / 2"); + + elsif run("Test raising exception") then + -- TODO: It fails as expected but the feedback is a bit strange + exec("raise RuntimeError('An exception')"); + + --------------------------------------------------------------------- + -- Misc tests + --------------------------------------------------------------------- + elsif run("Test globals and locals") then + exec("assert(globals() == locals())"); + + elsif run("Test print flushing") then + -- TODO: Observing that for some simulators the buffer isn't flushed + -- until the end of simulation + exec("from time import sleep"); + exec("print('Before sleep', flush=True)"); + exec("sleep(5)"); + exec("print('After sleep')"); + + end if; + end loop; + -- @formatter:on + + python_cleanup; + test_runner_cleanup(runner); + end process; +end; diff --git a/vunit/vhdl/run/src/run.vhd b/vunit/vhdl/run/src/run.vhd index 2c09df760..ff6ec1ba6 100644 --- a/vunit/vhdl/run/src/run.vhd +++ b/vunit/vhdl/run/src/run.vhd @@ -519,4 +519,14 @@ package body run_pkg is end if; end; + impure function run_script_path( + constant runner_cfg : string) + return string is + begin + if has_key(runner_cfg, "run script path") then + return get(runner_cfg, "run script path"); + else + return ""; + end if; + end; end package body run_pkg; diff --git a/vunit/vhdl/run/src/run_api.vhd b/vunit/vhdl/run/src/run_api.vhd index f20215315..2866e1646 100644 --- a/vunit/vhdl/run/src/run_api.vhd +++ b/vunit/vhdl/run/src/run_api.vhd @@ -163,6 +163,10 @@ package run_pkg is constant runner_cfg : string) return string; + impure function run_script_path( + constant runner_cfg : string) + return string; + alias test_runner_setup_entry_gate is entry_gate[runner_sync_t]; alias test_runner_setup_exit_gate is exit_gate[runner_sync_t]; alias test_suite_setup_entry_gate is entry_gate[runner_sync_t];