From 1e3068eb39bb88883364d5e80ea2278851d1c82b Mon Sep 17 00:00:00 2001 From: Lars Asplund Date: Sat, 6 Jan 2024 22:38:29 +0100 Subject: [PATCH 1/8] Initial prototype for VHPI. --- .../data/default/input_stimuli.csv | 1 + .../data/extras/input_stimuli2.csv | 1 + examples/vhdl/embedded_python/run.py | 155 ++++++ examples/vhdl/embedded_python/tb_example.vhd | 519 ++++++++++++++++++ vunit/builtins.py | 19 + vunit/sim_if/__init__.py | 7 + vunit/sim_if/activehdl.py | 11 +- vunit/sim_if/rivierapro.py | 7 + vunit/test/runner.py | 18 +- vunit/test/suites.py | 9 +- vunit/ui/__init__.py | 69 ++- vunit/vhdl/check/src/check.vhd | 32 ++ vunit/vhdl/check/src/check_api.vhd | 7 +- vunit/vhdl/python/run.py | 81 +++ vunit/vhdl/python/src/python.cpp | 401 ++++++++++++++ vunit/vhdl/python/src/python_context.vhd | 13 + vunit/vhdl/python/src/python_fli_pkg_vhpi.vhd | 44 ++ vunit/vhdl/python/src/python_pkg.vhd | 183 ++++++ vunit/vhdl/python/test/tb_python_pkg.vhd | 233 ++++++++ vunit/vhdl/run/src/run.vhd | 10 + vunit/vhdl/run/src/run_api.vhd | 4 + 21 files changed, 1785 insertions(+), 39 deletions(-) create mode 100644 examples/vhdl/embedded_python/data/default/input_stimuli.csv create mode 100644 examples/vhdl/embedded_python/data/extras/input_stimuli2.csv create mode 100644 examples/vhdl/embedded_python/run.py create mode 100644 examples/vhdl/embedded_python/tb_example.vhd create mode 100644 vunit/vhdl/python/run.py create mode 100644 vunit/vhdl/python/src/python.cpp create mode 100644 vunit/vhdl/python/src/python_context.vhd create mode 100644 vunit/vhdl/python/src/python_fli_pkg_vhpi.vhd create mode 100644 vunit/vhdl/python/src/python_pkg.vhd create mode 100644 vunit/vhdl/python/test/tb_python_pkg.vhd 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..292e67c60 --- /dev/null +++ b/examples/vhdl/embedded_python/run.py @@ -0,0 +1,155 @@ +# 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 + +from pathlib import Path +import subprocess +import sys +from multiprocessing import Manager, Process, get_context, Queue +from vunit import VUnit + + +def ccomp(simulator_name): + root = Path(__file__).parent + path_to_shared_lib = root / "vunit_out" / simulator_name / "libraries" / "python" + if not path_to_shared_lib.exists(): + path_to_shared_lib.mkdir(parents=True, exist_ok=True) + path_to_python_include = Path(sys.executable).parent / "include" + path_to_python_libs = Path(sys.executable).parent / "libs" + python_shared_lib = f"python{sys.version_info[0]}{sys.version_info[1]}" + path_to_cpp_file = root / ".." / ".." / ".." / "vunit" / "vhdl" / "python" / "src" / "python.cpp" + + proc = subprocess.run( + [ + "ccomp", + "-vhpi", + "-dbg", + "-verbose", + "-o", + '"' + str(path_to_shared_lib) + '"', + "-l", + python_shared_lib, + "-l", + "_tkinter", + "-I", + '"' + str(path_to_python_include) + '"', + "-L", + '"' + str(path_to_python_libs) + '"', + '"' + str(path_to_cpp_file) + '"', + ], + 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 remote_test(): + return 2 + + +def remote_test2(): + return 3 + + +def remote_test3(): + return 4 + + +def actor(): + print("Actor started") + + +def init_actor(): + print("Init actor") + + ctx = get_context('spawn') + proc = ctx.Process(target=actor) + try: + proc.start() + except Exception as exc: + print(exc) + + +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() + vu.enable_location_preprocessing() + + # 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. + ccomp(vu.get_simulator_name()) + + 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.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..fa9521f85 --- /dev/null +++ b/examples/vhdl/embedded_python/tb_example.vhd @@ -0,0 +1,519 @@ +-- 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 + +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; + + 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("environ['TCL_LIBRARY'] = str(Path(prefix) / 'tcl' / 'tcl8.6')"); + exec("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 + 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))) & "')") = "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) & "')") = "Yes"; + end if; + + 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"), "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((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"); + info("pi = " & to_string(eval_real("pi"))); + + -- 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. + -- The example is taken directly from the examples provided with the Python package. + elsif run("Test constraint solving") then + exec("from cocotb_coverage import crv"); -- Install with pip install cocotb-coverage + exec( + "class RandExample(crv.Randomized):" + + " def __init__(self, z):" + + " crv.Randomized.__init__(self)" + + " self.x = 0" + + " self.y = 0" + + " self.z = z" + + " self.x_c = lambda x, z: x > z" + + " self.add_rand('x', list(range(16)))" + + " self.add_rand('y', list(range(16)))" + + " self.add_constraint(lambda x, z : x != z)" + + " self.add_constraint(lambda y, z : y <= z)" + + " self.add_constraint(lambda x, y : x + y == 8)" + ); + exec("foo = RandExample(5)"); + exec("foo.randomize()"); + + -- Check that the constraints were met + check(eval_integer("foo.x") /= eval_integer("foo.z")); + check(eval_integer("foo.y") <= eval_integer("foo.z")); + check_equal(eval_integer("foo.x") + eval_integer("foo.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((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/vunit/builtins.py b/vunit/builtins.py index 567f36e37..b704e6deb 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,24 @@ 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") + + # TODO: Create enums for FLIs + python_package_supported_flis = set(["VHPI"]) + 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_fli_pkg_vhpi.vhd") + def _add_vhdl_logging(self): """ Add logging functionality 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..1c9dc9908 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): """ @@ -128,8 +135,8 @@ def compile_vhdl_file_command(self, source_file): "-j", str(Path(self._library_cfg).parent), ] - + source_file.compile_options.get("activehdl.vcom_flags", []) - + [ + +source_file.compile_options.get("activehdl.vcom_flags", []) + +[ self._std_str(source_file.get_vhdl_standard()), "-work", source_file.library.name, 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..436091593 100644 --- a/vunit/test/runner.py +++ b/vunit/test/runner.py @@ -33,10 +33,11 @@ class TestRunner(object): # pylint: disable=too-many-instance-attributes VERBOSITY_NORMAL = 1 VERBOSITY_VERBOSE = 2 - def __init__( # pylint: disable=too-many-arguments + 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, @@ -198,7 +200,7 @@ def _add_skipped_tests(self, test_suite, results, start_time, num_tests, output_ results[name] = SKIPPED self._add_results(test_suite, results, start_time, num_tests, output_file_name) - def _run_test_suite( # pylint: disable=too-many-locals + def _run_test_suite(# pylint: disable=too-many-locals self, test_suite, write_stdout, num_tests, output_path, output_file_name ): """ @@ -224,7 +226,7 @@ def _run_test_suite( # pylint: disable=too-many-locals if write_stdout: output_from = self._stdout_ansi else: - color_output_file = Path(color_output_file_name).open( # pylint: disable=consider-using-with + color_output_file = Path(color_output_file_name).open(# pylint: disable=consider-using-with "w", encoding="utf-8" ) output_from = color_output_file @@ -241,7 +243,11 @@ 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 @@ -296,7 +302,7 @@ def _create_test_mapping_file(self, test_suites): mapping.add(f"{Path(test_output).name!s} {test_suite.name!s}") # Sort by everything except hash - mapping = sorted(mapping, key=lambda value: value[value.index(" ") :]) + mapping = sorted(mapping, key=lambda value: value[value.index(" "):]) with mapping_file_name.open("w", encoding="utf-8") as fptr: for value in mapping: @@ -451,7 +457,7 @@ def wrap(file_obj, use_color=True): NOTE: imports colorama here to avoid dependency from setup.py importing VUnit before colorama is installed """ - from colorama import ( # type: ignore # pylint: disable=import-outside-toplevel + from colorama import (# type: ignore # pylint: disable=import-outside-toplevel AnsiToWin32, ) diff --git a/vunit/test/suites.py b/vunit/test/suites.py index 9f63ac4c2..a33844baf 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? @@ -259,7 +260,7 @@ def _read_test_results(self, file_name): # pylint: disable=too-many-branches for line in test_results.splitlines(): if line.startswith("test_start:"): - test_name = line[len("test_start:") :] + test_name = line[len("test_start:"):] if test_name not in test_starts: test_starts.append(test_name) diff --git a/vunit/ui/__init__.py b/vunit/ui/__init__.py index 3ddc531e3..915656e41 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 @@ -59,7 +60,7 @@ class VUnit(object): # pylint: disable=too-many-instance-attributes, too-many-p def from_argv( cls, argv=None, - vhdl_standard: Optional[str] = None, + vhdl_standard: Optional[str]=None, ): """ Create VUnit instance from command line arguments. @@ -90,7 +91,7 @@ def from_argv( def from_args( cls, args, - vhdl_standard: Optional[str] = None, + vhdl_standard: Optional[str]=None, ): """ Create VUnit instance from args namespace. @@ -114,11 +115,20 @@ def from_args( def __init__( self, args, - vhdl_standard: Optional[str] = None, + 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 @@ -207,7 +217,7 @@ def _which_vhdl_standard(self, vhdl_standard: Optional[str]) -> VHDLStandard: return VHDL.standard(vhdl_standard) - def add_external_library(self, library_name, path: Union[str, Path], vhdl_standard: Optional[str] = None): + def add_external_library(self, library_name, path: Union[str, Path], vhdl_standard: Optional[str]=None): """ Add an externally compiled library as a black-box @@ -233,7 +243,7 @@ def add_external_library(self, library_name, path: Union[str, Path], vhdl_standa ) return self.library(library_name) - def add_source_files_from_csv(self, project_csv_path: Union[str, Path], vhdl_standard: Optional[str] = None): + def add_source_files_from_csv(self, project_csv_path: Union[str, Path], vhdl_standard: Optional[str]=None): """ Add a project configuration, mapping all the libraries and files @@ -268,8 +278,8 @@ def add_source_files_from_csv(self, project_csv_path: Union[str, Path], vhdl_sta def add_library( self, library_name: str, - vhdl_standard: Optional[str] = None, - allow_duplicate: Optional[bool] = False, + vhdl_standard: Optional[str]=None, + allow_duplicate: Optional[bool]=False, ): """ Add a library managed by VUnit. @@ -311,7 +321,7 @@ def library(self, library_name: str): def get_libraries( self, pattern="*", - allow_empty: Optional[bool] = False, + allow_empty: Optional[bool]=False, ): """ Get a list of libraries @@ -332,7 +342,7 @@ def get_libraries( return LibraryList(results) - def set_attribute(self, name: str, value: str, allow_empty: Optional[bool] = False): + def set_attribute(self, name: str, value: str, allow_empty: Optional[bool]=False): """ Set a value of attribute in all |configurations| @@ -353,7 +363,7 @@ def set_attribute(self, name: str, value: str, allow_empty: Optional[bool] = Fal for test_bench in check_not_empty(test_benches, allow_empty, "No test benches found"): test_bench.set_attribute(name, value) - def set_generic(self, name: str, value: str, allow_empty: Optional[bool] = False): + def set_generic(self, name: str, value: str, allow_empty: Optional[bool]=False): """ Set a value of generic in all |configurations| @@ -374,7 +384,7 @@ def set_generic(self, name: str, value: str, allow_empty: Optional[bool] = False for test_bench in check_not_empty(test_benches, allow_empty, "No test benches found"): test_bench.set_generic(name.lower(), value) - def set_parameter(self, name: str, value: str, allow_empty: Optional[bool] = False): + def set_parameter(self, name: str, value: str, allow_empty: Optional[bool]=False): """ Set value of parameter in all |configurations| @@ -399,8 +409,8 @@ def set_sim_option( self, name: str, value: str, - allow_empty: Optional[bool] = False, - overwrite: Optional[bool] = True, + allow_empty: Optional[bool]=False, + overwrite: Optional[bool]=True, ): """ Set simulation option in all |configurations| @@ -423,7 +433,7 @@ def set_sim_option( for test_bench in check_not_empty(test_benches, allow_empty, "No test benches found"): test_bench.set_sim_option(name, value, overwrite) - def set_compile_option(self, name: str, value: str, allow_empty: Optional[bool] = False): + def set_compile_option(self, name: str, value: str, allow_empty: Optional[bool]=False): """ Set compile option of all files @@ -445,7 +455,7 @@ def set_compile_option(self, name: str, value: str, allow_empty: Optional[bool] for source_file in check_not_empty(source_files, allow_empty, "No source files found"): source_file.set_compile_option(name, value) - def add_compile_option(self, name: str, value: str, allow_empty: Optional[bool] = False): + def add_compile_option(self, name: str, value: str, allow_empty: Optional[bool]=False): """ Add compile option to all files @@ -460,7 +470,7 @@ def add_compile_option(self, name: str, value: str, allow_empty: Optional[bool] for source_file in check_not_empty(source_files, allow_empty, "No source files found"): source_file.add_compile_option(name, value) - def get_source_file(self, file_name: Union[str, Path], library_name: Optional[str] = None): + def get_source_file(self, file_name: Union[str, Path], library_name: Optional[str]=None): """ Get a source file @@ -484,8 +494,8 @@ def get_source_file(self, file_name: Union[str, Path], library_name: Optional[st def get_source_files( self, pattern="*", - library_name: Optional[str] = None, - allow_empty: Optional[bool] = False, + library_name: Optional[str]=None, + allow_empty: Optional[bool]=False, ): """ Get a list of source files @@ -513,21 +523,21 @@ def get_source_files( results, allow_empty, f"Pattern {pattern!r} did not match any file" - + (f"within library {library_name!s}" if library_name is not None else ""), + +(f"within library {library_name!s}" if library_name is not None else ""), ) return SourceFileList(results) - def add_source_files( # pylint: disable=too-many-arguments + def add_source_files(# pylint: disable=too-many-arguments self, pattern, library_name: str, preprocessors=None, include_dirs=None, defines=None, - allow_empty: Optional[bool] = False, - vhdl_standard: Optional[str] = None, - no_parse: Optional[bool] = False, + allow_empty: Optional[bool]=False, + vhdl_standard: Optional[str]=None, + no_parse: Optional[bool]=False, file_type=None, ): """ @@ -563,15 +573,15 @@ def add_source_files( # pylint: disable=too-many-arguments file_type=file_type, ) - def add_source_file( # pylint: disable=too-many-arguments + def add_source_file(# pylint: disable=too-many-arguments self, file_name: Union[str, Path], library_name: str, preprocessors=None, include_dirs=None, defines=None, - vhdl_standard: Optional[str] = None, - no_parse: Optional[bool] = False, + vhdl_standard: Optional[str]=None, + no_parse: Optional[bool]=False, file_type=None, ): """ @@ -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..b156f5053 100644 --- a/vunit/vhdl/check/src/check.vhd +++ b/vunit/vhdl/check/src/check.vhd @@ -130,6 +130,38 @@ package body check_pkg is end if; 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..0fe85bae0 --- /dev/null +++ b/vunit/vhdl/python/run.py @@ -0,0 +1,81 @@ +# 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 + +from pathlib import Path +import subprocess +import sys +from multiprocessing import Manager, Process, get_context, Queue +from vunit import VUnit + + +def ccomp(simulator_name): + root = Path(__file__).parent + path_to_shared_lib = root / "vunit_out" / simulator_name / "libraries" / "python" + if not path_to_shared_lib.exists(): + path_to_shared_lib.mkdir(parents=True, exist_ok=True) + path_to_python_include = Path(sys.executable).parent / "include" + path_to_python_libs = Path(sys.executable).parent / "libs" + python_shared_lib = f"python{sys.version_info[0]}{sys.version_info[1]}" + path_to_cpp_file = root / ".." / ".." / ".." / "vunit" / "vhdl" / "python" / "src" / "python.cpp" + + proc = subprocess.run( + [ + "ccomp", + "-vhpi", + "-dbg", + "-verbose", + "-o", + '"' + str(path_to_shared_lib) + '"', + "-l", + python_shared_lib, + "-l", + "_tkinter", + "-I", + '"' + str(path_to_python_include) + '"', + "-L", + '"' + str(path_to_python_libs) + '"', + '"' + str(path_to_cpp_file) + '"', + ], + 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 remote_test(): + return 2 + + +def main(): + root = Path(__file__).parent + + vu = VUnit.from_argv() + vu.add_vhdl_builtins() + vu.add_python() + + # 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. + ccomp(vu.get_simulator_name()) + + 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.main() + + +if __name__ == "__main__": + main() diff --git a/vunit/vhdl/python/src/python.cpp b/vunit/vhdl/python/src/python.cpp new file mode 100644 index 000000000..5df8b0c1c --- /dev/null +++ b/vunit/vhdl/python/src/python.cpp @@ -0,0 +1,401 @@ +#include +#include +#include + +#define PY_SSIZE_T_CLEAN +#include "Python.h" + +#define MAX_VHDL_PARAMETER_STRING_LENGTH 100000 + +static PyObject* globals; +static PyObject* locals; + +using namespace std; + +PLI_VOID python_cleanup(const struct vhpiCbDataS* cb_p); + +static char* get_string(PyObject *pyobj) { + PyObject* str = PyObject_Str(pyobj); + if (str == nullptr) { + return nullptr; + } + + PyObject* str_utf_8 = PyUnicode_AsEncodedString(str, "utf-8", NULL); + Py_DECREF(str); + if (str_utf_8 == nullptr) { + return nullptr; + } + + char* result = PyBytes_AS_STRING(str_utf_8); + Py_DECREF(str_utf_8); + + return result; +} +static void py_error_handler(const char *context, const char *code_or_expr = nullptr, const char *reason = nullptr, bool cleanup = true) { + const char* unknown_error = "Unknown error"; + + // Use provided error reason or try extracting the reason from the Python exception + if (reason == nullptr) { + PyObject *exc; + + exc = PyErr_GetRaisedException(); + if (exc != nullptr) { + reason = get_string(exc); + Py_DECREF(exc); + } + } + + // Clean-up Python session first in case vhpi_assert stops the simulation + if (cleanup) { + python_cleanup(nullptr); + } + + // Output error message + reason = reason == nullptr ? unknown_error : reason; + if (code_or_expr == nullptr) { + 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 vhpi_error_handler(const char* context, bool cleanup = true) { + vhpiErrorInfoT err; + + // Clean-up Python session first in case vhpi_assert stops the simulation + if (cleanup) { + python_cleanup(nullptr); + } + + 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()) { + vhpi_error_handler("Failed to initialize Python", false); + } + + PyObject* main_module = PyImport_AddModule("__main__"); + if (main_module == nullptr) { + vhpi_error_handler("Failed to get the main module"); + } + + globals = PyModule_GetDict(main_module); + if (globals == nullptr) { + vhpi_error_handler("Failed to get the global dictionary"); + } + + // globals and locals are the same at the top-level + locals = globals; +} + +PLI_VOID python_cleanup(const struct vhpiCbDataS* cb_p) { + if (locals != nullptr) { + 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 == nullptr) { + vhpi_error_handler("getting VHDL parameter handle"); + } + + 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)) { + vhpi_error_handler("getting VHDL parameter value"); + } + + return vhdl_parameter_string; +} + +static PyObject* eval(const char *expr) { + PyObject* pyobj = PyRun_String(expr, Py_eval_input, globals, locals); + if (pyobj == nullptr) { + py_error_handler("evaluating", expr); + } + + return pyobj; +} + +static void handle_type_check_error(PyObject* pyobj, const char *context, const char *expr) { + PyObject* type_name = PyType_GetName(Py_TYPE(pyobj)); + if (type_name == nullptr) { + py_error_handler(context, expr, "Expression evaluates to an unknown type."); + } + + const char* type_name_str = get_string(type_name); + Py_DECREF(type_name); + if (type_name_str == nullptr) { + py_error_handler(context, expr, "Expression evaluates to an unknown type."); + } + + string error_message = "Expression evaluates to " + string(type_name_str); + py_error_handler(context, expr, error_message.c_str()); +} + +static void check_conversion_error(const char *expr) { + PyObject* exc = PyErr_Occurred(); + if (exc != nullptr) { + Py_DECREF(exc); + py_error_handler("parsing evaluation result of", expr); + } +} + +static int get_integer(PyObject* pyobj, const char* expr, bool dec_ref_count = true) { + // 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) or (value < -pow(2, 31))) { + py_error_handler("parsing evaluation result of", expr, "Result out of VHDL integer range."); + } + + return int(value); +} + +static double get_real(PyObject* pyobj, const char* expr, bool dec_ref_count = true) { + // 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) or (value < -3.4028234664e38)) { + py_error_handler("parsing evaluation result of", expr, "Result out of VHDL real range."); + } + + return value; +} + + +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); + if (vhpi_put_value(cb_p->obj, &vhdl_result, vhpiDeposit)) { + py_error_handler("returning result for evaluation of", expr); + } +} + +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); + if (vhpi_put_value(cb_p->obj, &vhdl_result, vhpiDeposit)) { + py_error_handler("returning result for evaluation of", expr); + } +} + +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); + } + + if (vhpi_put_value(cb_p->obj, &vhdl_result, vhpiDeposit)) { + free(int_array); + py_error_handler("returning result for evaluation of", expr); + } + + 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); + } + + if (vhpi_put_value(cb_p->obj, &vhdl_result, vhpiDeposit)) { + free(double_array); + py_error_handler("returning result for evaluation of", expr); + } + + 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); + } + + if (vhpi_put_value(cb_p->obj, &vhdl_result, vhpiDeposit)) { + py_error_handler("returning result for evaluation of", expr); + } +} + + +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) == nullptr) { + py_error_handler("executing", code); + } +} + +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, nullptr, python_setup}; + // vhpi_assert doesn't seem to work at this point + assert(vhpi_register_foreignf(&python_setup_data) != nullptr); + + char python_cleanup_name[] = "python_cleanup"; + vhpiForeignDataT python_cleanup_data = {vhpiProcF, library_name, python_cleanup_name, nullptr, python_cleanup}; + assert(vhpi_register_foreignf(&python_cleanup_data) != nullptr); + + char eval_integer_name[] = "eval_integer"; + vhpiForeignDataT eval_integer_data = {vhpiProcF, library_name, eval_integer_name, nullptr, eval_integer}; + assert(vhpi_register_foreignf(&eval_integer_data) != nullptr); + + char eval_real_name[] = "eval_real"; + vhpiForeignDataT eval_real_data = {vhpiProcF, library_name, eval_real_name, nullptr, eval_real}; + assert(vhpi_register_foreignf(&eval_real_data) != nullptr); + + char eval_integer_vector_name[] = "eval_integer_vector"; + vhpiForeignDataT eval_integer_vector_data = {vhpiProcF, library_name, eval_integer_vector_name, nullptr, eval_integer_vector}; + assert(vhpi_register_foreignf(&eval_integer_vector_data) != nullptr); + + char eval_real_vector_name[] = "eval_real_vector"; + vhpiForeignDataT eval_real_vector_data = {vhpiProcF, library_name, eval_real_vector_name, nullptr, eval_real_vector}; + assert(vhpi_register_foreignf(&eval_real_vector_data) != nullptr); + + char eval_string_name[] = "eval_string"; + vhpiForeignDataT eval_string_data = {vhpiProcF, library_name, eval_string_name, nullptr, eval_string}; + assert(vhpi_register_foreignf(&eval_string_data) != nullptr); + + char exec_name[] = "exec"; + vhpiForeignDataT exec_data = {vhpiProcF, library_name, exec_name, nullptr, exec}; + assert(vhpi_register_foreignf(&exec_data) != nullptr); +} + +PLI_VOID (*vhpi_startup_routines[])() = {register_foreign_subprograms, nullptr}; diff --git a/vunit/vhdl/python/src/python_context.vhd b/vunit/vhdl/python/src/python_context.vhd new file mode 100644 index 000000000..a488f693a --- /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-2023, Lars Asplund lars.anders.asplund@gmail.com + +context python_context is + library vunit_lib; + use vunit_lib.python_pkg.all; + use vunit_lib.python_fli_pkg.all; +end context; diff --git a/vunit/vhdl/python/src/python_fli_pkg_vhpi.vhd b/vunit/vhdl/python/src/python_fli_pkg_vhpi.vhd new file mode 100644 index 000000000..393942d81 --- /dev/null +++ b/vunit/vhdl/python/src/python_fli_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-2023, Lars Asplund lars.anders.asplund@gmail.com + +use std.textio.all; + +package python_fli_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_fli_pkg is +end package body; diff --git a/vunit/vhdl/python/src/python_pkg.vhd b/vunit/vhdl/python/src/python_pkg.vhd new file mode 100644 index 000000000..047beddb1 --- /dev/null +++ b/vunit/vhdl/python/src/python_pkg.vhd @@ -0,0 +1,183 @@ +-- 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 + +use work.python_fli_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]; + + 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 := "" + ); + + 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 + 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; + + procedure import_run_script(module_name : string := "") is + constant run_script_path : string := run_script_path(get_cfg(runner_state)); + variable path_items : lines_t; + variable run_script_name : line; + begin + if module_name = "" then + -- Extract the last item in the full path + path_items := split(run_script_path, "/"); + for idx in path_items'range loop + if idx = path_items'right then + run_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(run_script_name.all, "."); + deallocate(run_script_name); + for idx in path_items'range loop + if idx = path_items'left then + import_module_from_file(run_script_path, path_items(idx).all); + end if; + deallocate(path_items(idx)); + end loop; + deallocate(path_items); + else + import_module_from_file(run_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 + -- to_string of real seems to express an integer real as an integer: to_string(1.0) is "1". + -- use real'image instead. + swrite(l, real'image(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; + + 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; + + 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/test/tb_python_pkg.vhd b/vunit/vhdl/python/test/tb_python_pkg.vhd new file mode 100644 index 000000000..e23067fe5 --- /dev/null +++ b/vunit/vhdl/python/test/tb_python_pkg.vhd @@ -0,0 +1,233 @@ +-- 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 + +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); + + 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; + + 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("2**31 - 1"), 2 ** 31 - 1); + check_equal(eval("-2**31"), -2 ** 31); + + 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.4028234664e38"), 3.4028234664e38); + check_equal(eval("1.1754943508e-38"), 1.1754943508e-38); + check_equal(eval("-3.4028234664e38"), -3.4028234664e38); + 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((0 => 17))) = (0 => 17)); + check(eval(to_py_list_str((-2 ** 31, -1, 0, 1, 2 ** 31 - 1))) = (-2 ** 31, -1, 0, 1, 2 ** 31 - 1)); + + 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((0 => 17))); + check_equal(get(vhdl_integer_vector_ptr, 0), 17); + + vhdl_integer_vector_ptr := eval(eval(to_py_list_str((-2 ** 31, -1, 0, 1, 2 ** 31 - 1)))); + check_equal(get(vhdl_integer_vector_ptr, 0), -2 ** 31); + 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), 2 ** 31 - 1); + + elsif run("Test eval of string expression") then + check_equal(eval("''"), string'("")); + check_equal(eval("'\\'"), string'("\")); + check_equal(eval("'Hello from VUnit'"), "Hello from VUnit"); + + -- TODO: We could use a helper function converting newlines to VHDL linefeeds + check_equal(eval("'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((0 => 17.0))) = (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 + -- 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 + -- Observing that buffer isn't flushed until 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..6d04fc4d6 100644 --- a/vunit/vhdl/run/src/run_api.vhd +++ b/vunit/vhdl/run/src/run_api.vhd @@ -162,6 +162,10 @@ package run_pkg is impure function tb_path ( 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]; From 13c4567cdd7215e63f432ed7e9856fe24e56c2cf Mon Sep 17 00:00:00 2001 From: Lars Asplund Date: Sun, 21 Jan 2024 18:45:38 +0100 Subject: [PATCH 2/8] Added support for FLI --- examples/vhdl/embedded_python/run.py | 84 +---- examples/vhdl/embedded_python/tb_example.vhd | 18 +- vunit/builtins.py | 4 +- vunit/python_fli_compile.py | 128 ++++++++ vunit/sim_if/modelsim.py | 7 + vunit/vhdl/python/run.py | 56 +--- vunit/vhdl/python/src/python_fli.c | 297 ++++++++++++++++++ vunit/vhdl/python/src/python_fli_pkg_fli.vhd | 121 +++++++ vunit/vhdl/python/src/python_pkg.vhd | 9 +- .../src/{python.cpp => python_vhpi.cpp} | 0 vunit/vhdl/python/test/tb_python_pkg.vhd | 109 +++---- 11 files changed, 644 insertions(+), 189 deletions(-) create mode 100644 vunit/python_fli_compile.py create mode 100644 vunit/vhdl/python/src/python_fli.c create mode 100644 vunit/vhdl/python/src/python_fli_pkg_fli.vhd rename vunit/vhdl/python/src/{python.cpp => python_vhpi.cpp} (100%) diff --git a/examples/vhdl/embedded_python/run.py b/examples/vhdl/embedded_python/run.py index 292e67c60..127ffce73 100644 --- a/examples/vhdl/embedded_python/run.py +++ b/examples/vhdl/embedded_python/run.py @@ -5,82 +5,12 @@ # Copyright (c) 2014-2023, Lars Asplund lars.anders.asplund@gmail.com from pathlib import Path -import subprocess -import sys -from multiprocessing import Manager, Process, get_context, Queue from vunit import VUnit - - -def ccomp(simulator_name): - root = Path(__file__).parent - path_to_shared_lib = root / "vunit_out" / simulator_name / "libraries" / "python" - if not path_to_shared_lib.exists(): - path_to_shared_lib.mkdir(parents=True, exist_ok=True) - path_to_python_include = Path(sys.executable).parent / "include" - path_to_python_libs = Path(sys.executable).parent / "libs" - python_shared_lib = f"python{sys.version_info[0]}{sys.version_info[1]}" - path_to_cpp_file = root / ".." / ".." / ".." / "vunit" / "vhdl" / "python" / "src" / "python.cpp" - - proc = subprocess.run( - [ - "ccomp", - "-vhpi", - "-dbg", - "-verbose", - "-o", - '"' + str(path_to_shared_lib) + '"', - "-l", - python_shared_lib, - "-l", - "_tkinter", - "-I", - '"' + str(path_to_python_include) + '"', - "-L", - '"' + str(path_to_python_libs) + '"', - '"' + str(path_to_cpp_file) + '"', - ], - 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 remote_test(): - return 2 - - -def remote_test2(): - return 3 - - -def remote_test3(): - return 4 - - -def actor(): - print("Actor started") - - -def init_actor(): - print("Init actor") - - ctx = get_context('spawn') - proc = ctx.Process(target=actor) - try: - proc.start() - except Exception as exc: - print(exc) - +from vunit.python_fli_compile import compile_vhpi_application, compile_fli_application def hello_world(): print("Hello World") - class Plot(): def __init__(self, x_points, y_limits, title, x_label, y_label): @@ -134,10 +64,14 @@ def main(): vu.add_python() vu.add_random() vu.enable_location_preprocessing() - - # 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. - ccomp(vu.get_simulator_name()) + 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, simulator_name) + elif simulator_name == "modelsim": + compile_fli_application(root, vu) lib = vu.add_library("lib") lib.add_source_files(root / "*.vhd") diff --git a/examples/vhdl/embedded_python/tb_example.vhd b/examples/vhdl/embedded_python/tb_example.vhd index fa9521f85..0b712da75 100644 --- a/examples/vhdl/embedded_python/tb_example.vhd +++ b/examples/vhdl/embedded_python/tb_example.vhd @@ -43,6 +43,7 @@ begin 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 @@ -75,11 +76,11 @@ begin -- Make the query 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))) & "')") = "Yes"; + 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) & "')") = "Yes"; + " : " & to_string(line_num) & "')") = string'("Yes"); end if; if continue then @@ -155,7 +156,7 @@ begin "l = ['a', 'b', 'c']" & LF & "s = ', '.join(l)" ); - check_equal(eval("s"), "a, b, c"); + check_equal(eval("s"), string'("a, b, c")); -- 4. Using + as a shorthand for & LF &: exec( @@ -183,7 +184,7 @@ begin check_equal(eval("an_integer"), 17); -- Sometimes a helper function is needed - exec("a_list = " & to_py_list_str((1, 2, 3, 4))); + 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 @@ -195,8 +196,9 @@ begin -- Every eval has a more explicit alias that may be needed to help the compiler exec("from math import pi"); - info("pi = " & to_string(eval_real("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( @@ -205,7 +207,7 @@ begin 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 @@ -388,7 +390,7 @@ begin -- 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((1, 2, 8, 16, 32, 64, 127, 128)); + 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); diff --git a/vunit/builtins.py b/vunit/builtins.py index b704e6deb..c6d4d3cff 100755 --- a/vunit/builtins.py +++ b/vunit/builtins.py @@ -220,7 +220,7 @@ def _add_python(self): raise RuntimeError("Python package only supports vhdl 2008 and later") # TODO: Create enums for FLIs - python_package_supported_flis = set(["VHPI"]) + python_package_supported_flis = set(["VHPI", "FLI"]) 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)}") @@ -229,6 +229,8 @@ def _add_python(self): 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_fli_pkg_vhpi.vhd") + elif "FLI" in simulator_supported_flis: + self._vunit_lib.add_source_files(VHDL_PATH / "python" / "src" / "python_fli_pkg_fli.vhd") def _add_vhdl_logging(self): """ diff --git a/vunit/python_fli_compile.py b/vunit/python_fli_compile.py new file mode 100644 index 000000000..90aa93568 --- /dev/null +++ b/vunit/python_fli_compile.py @@ -0,0 +1,128 @@ +# 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 +from pathlib import Path +from glob import glob +import subprocess +import sys + +def compile_vhpi_application(run_script_root, simulator_name): + path_to_shared_lib = (run_script_root / "vunit_out" / simulator_name / "libraries" / "python").resolve() + if not path_to_shared_lib.exists(): + path_to_shared_lib.mkdir(parents=True, exist_ok=True) + 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]}" + application_path = Path(__file__).parent.resolve() / "vhdl" / "python" / "src" / "python_vhpi.cpp" + + proc = subprocess.run( + [ + "ccomp", + "-vhpi", + "-dbg", + "-verbose", + "-o", + '"' + str(path_to_shared_lib) + '"', + "-l", + python_shared_lib, + "-l", + "_tkinter", + "-I", + '"' + str(path_to_python_include) + '"', + "-L", + '"' + str(path_to_python_libs) + '"', + '"' + str(application_path) + '"', + ], + capture_output=True, + text=True, + ) + + 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): + path_to_simulator = Path(vu._simulator_class.find_prefix()).resolve() + 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, + ) + 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" + application_path = Path(__file__).parent.resolve() / "vhdl" / "python" / "src" / "python_fli.c" + + 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(application_path), + ] + + proc = subprocess.run( + args, + capture_output=True, + text=True, + ) + 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" / "modelsim" / "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_fli.o", + "-l" + python_shared_lib, + "-l_tkinter", + "-L" + str(path_to_simulator), + "-L" + str(path_to_python_libs), + "-lmtipli" + ] + + if proc.returncode != 0: + print(proc.stdout) + print(proc.stderr) + raise RuntimeError("Failed to link FLI application") + 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/vhdl/python/run.py b/vunit/vhdl/python/run.py index 0fe85bae0..1137c6eaa 100644 --- a/vunit/vhdl/python/run.py +++ b/vunit/vhdl/python/run.py @@ -5,50 +5,8 @@ # Copyright (c) 2014-2023, Lars Asplund lars.anders.asplund@gmail.com from pathlib import Path -import subprocess -import sys -from multiprocessing import Manager, Process, get_context, Queue from vunit import VUnit - - -def ccomp(simulator_name): - root = Path(__file__).parent - path_to_shared_lib = root / "vunit_out" / simulator_name / "libraries" / "python" - if not path_to_shared_lib.exists(): - path_to_shared_lib.mkdir(parents=True, exist_ok=True) - path_to_python_include = Path(sys.executable).parent / "include" - path_to_python_libs = Path(sys.executable).parent / "libs" - python_shared_lib = f"python{sys.version_info[0]}{sys.version_info[1]}" - path_to_cpp_file = root / ".." / ".." / ".." / "vunit" / "vhdl" / "python" / "src" / "python.cpp" - - proc = subprocess.run( - [ - "ccomp", - "-vhpi", - "-dbg", - "-verbose", - "-o", - '"' + str(path_to_shared_lib) + '"', - "-l", - python_shared_lib, - "-l", - "_tkinter", - "-I", - '"' + str(path_to_python_include) + '"', - "-L", - '"' + str(path_to_python_libs) + '"', - '"' + str(path_to_cpp_file) + '"', - ], - capture_output=True, - text=True, - check=False, - ) - - if proc.returncode != 0: - print(proc.stdout) - print(proc.stderr) - raise RuntimeError("Failed to compile VHPI application") - +from vunit.python_fli_compile import compile_vhpi_application, compile_fli_application def remote_test(): return 2 @@ -60,10 +18,14 @@ def main(): vu = VUnit.from_argv() vu.add_vhdl_builtins() vu.add_python() - - # 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. - ccomp(vu.get_simulator_name()) + 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, simulator_name) + elif simulator_name == "modelsim": + compile_fli_application(root, vu) lib = vu.add_library("lib") lib.add_source_files(root / "test" / "*.vhd") diff --git a/vunit/vhdl/python/src/python_fli.c b/vunit/vhdl/python/src/python_fli.c new file mode 100644 index 000000000..126611f9e --- /dev/null +++ b/vunit/vhdl/python/src/python_fli.c @@ -0,0 +1,297 @@ +#include +#include +#include "mti.h" +// #include +// #include + +#define PY_SSIZE_T_CLEAN +#include "Python.h" + +#define MAX_VHDL_PARAMETER_STRING_LENGTH 100000 + +static PyObject* globals; +static PyObject* locals; + +void python_cleanup(void); + +static 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; +} + +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 *exc; + + exc = PyErr_GetRaisedException(); + if (exc != NULL) { + reason = get_string(exc); + Py_DECREF(exc); + } + } + + // 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 fli_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()) { + fli_error_handler("Failed to initialize Python", false); + } + + PyObject* main_module = PyImport_AddModule("__main__"); + if (main_module == NULL) { + fli_error_handler("Failed to get the main module", true); + } + + globals = PyModule_GetDict(main_module); + if (globals == NULL) { + fli_error_handler("Failed to get the global dictionary", true); + } + + // globals and locals are the same at the top-level + locals = globals; + + // 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) { + fli_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; +} + +static 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; +} + +static void handle_type_check_error(PyObject* pyobj, const char *context, const char *expr) { + PyObject* type_name = PyType_GetName(Py_TYPE(pyobj)); + if (type_name == NULL) { + py_error_handler(context, expr, "Expression evaluates to an unknown type.", true); + } + + const char* type_name_str = get_string(type_name); + Py_DECREF(type_name); + 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); +} + +static void check_conversion_error(const char *expr) { + PyObject* exc = PyErr_Occurred(); + if (exc != NULL) { + Py_DECREF(exc); + py_error_handler("target type casting evaluation result of", expr, NULL, true); + } +} + +static 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; +} + +static 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; +} + + +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_fli_pkg_fli.vhd b/vunit/vhdl/python/src/python_fli_pkg_fli.vhd new file mode 100644 index 000000000..b3e4344e9 --- /dev/null +++ b/vunit/vhdl/python/src/python_fli_pkg_fli.vhd @@ -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 + +use std.textio.all; + +package python_fli_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_fli_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.vhd b/vunit/vhdl/python/src/python_pkg.vhd index 047beddb1..3008f5c53 100644 --- a/vunit/vhdl/python/src/python_pkg.vhd +++ b/vunit/vhdl/python/src/python_pkg.vhd @@ -31,7 +31,8 @@ package python_pkg is 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]; - function to_call_str( + -- 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; @@ -39,7 +40,7 @@ package python_pkg is identifier : string; arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : string := "" ); - function call_integer( + 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, @@ -158,7 +159,7 @@ package body python_pkg is return result; end; - function to_call_str( + 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 & @@ -174,7 +175,7 @@ package body python_pkg is exec(to_call_str(identifier, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10)); end; - function call_integer( + impure function call_integer( identifier : string; arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : string := "" ) return integer is begin diff --git a/vunit/vhdl/python/src/python.cpp b/vunit/vhdl/python/src/python_vhpi.cpp similarity index 100% rename from vunit/vhdl/python/src/python.cpp rename to vunit/vhdl/python/src/python_vhpi.cpp diff --git a/vunit/vhdl/python/test/tb_python_pkg.vhd b/vunit/vhdl/python/test/tb_python_pkg.vhd index e23067fe5..9bb1a819c 100644 --- a/vunit/vhdl/python/test/tb_python_pkg.vhd +++ b/vunit/vhdl/python/test/tb_python_pkg.vhd @@ -30,6 +30,7 @@ begin 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); @@ -42,68 +43,68 @@ begin -- @formatter:off while test_suite loop - --------------------------------------------------------------------- - -- Test eval of different types - --------------------------------------------------------------------- - if run("Test eval of integer expression") then - check_equal(eval("2**31 - 1"), 2 ** 31 - 1); - check_equal(eval("-2**31"), -2 ** 31); + --------------------------------------------------------------------- + -- Test eval of different types + --------------------------------------------------------------------- + if run("Test eval of integer expression") then + check_equal(eval("2**31 - 1"), 2 ** 31 - 1); + check_equal(eval("-2**31"), -2 ** 31); - 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 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 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 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 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.4028234664e38"), 3.4028234664e38); - check_equal(eval("1.1754943508e-38"), 1.1754943508e-38); - check_equal(eval("-3.4028234664e38"), -3.4028234664e38); - check_equal(eval("-1.1754943508e-38"), -1.1754943508e-38); + 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 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 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 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((0 => 17))) = (0 => 17)); - check(eval(to_py_list_str((-2 ** 31, -1, 0, 1, 2 ** 31 - 1))) = (-2 ** 31, -1, 0, 1, 2 ** 31 - 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'(-2 ** 31, -1, 0, 1, 2 ** 31 - 1))) = integer_vector'(-2 ** 31, -1, 0, 1, 2 ** 31 - 1)); - 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), "[]"); + 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(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]"); + 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((0 => 17))); + 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(eval(to_py_list_str((-2 ** 31, -1, 0, 1, 2 ** 31 - 1)))); + vhdl_integer_vector_ptr := eval(to_py_list_str(integer_vector'(-2 ** 31, -1, 0, 1, 2 ** 31 - 1))); check_equal(get(vhdl_integer_vector_ptr, 0), -2 ** 31); check_equal(get(vhdl_integer_vector_ptr, 1), -1); check_equal(get(vhdl_integer_vector_ptr, 2), 0); @@ -113,21 +114,21 @@ begin elsif run("Test eval of string expression") then check_equal(eval("''"), string'("")); check_equal(eval("'\\'"), string'("\")); - check_equal(eval("'Hello from VUnit'"), "Hello from VUnit"); - + 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("'Hello\\nWorld'"), "Hello\nWorld"); + 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]"); + -- 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((0 => 17.0))) = (0 => 17.0)); + 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)); @@ -207,7 +208,7 @@ begin vhdl_int := eval("10 / 2"); elsif run("Test raising exception") then - -- It fails as expected but the feedback is a bit strange + -- TODO: It fails as expected but the feedback is a bit strange exec("raise RuntimeError('An exception')"); --------------------------------------------------------------------- @@ -217,7 +218,7 @@ begin exec("assert(globals() == locals())"); elsif run("Test print flushing") then - -- Observing that buffer isn't flushed until end of simulation + -- TODO: Observing that buffer isn't flushed until end of simulation exec("from time import sleep"); exec("print('Before sleep', flush=True)"); exec("sleep(5)"); From 51684aa13e3c52c394ed28704ffbe113a17f6e89 Mon Sep 17 00:00:00 2001 From: Lars Asplund Date: Fri, 26 Jan 2024 18:20:11 +0100 Subject: [PATCH 3/8] Refactoring --- examples/vhdl/embedded_python/run.py | 138 +++--- examples/vhdl/embedded_python/tb_example.vhd | 192 ++++----- vunit/builtins.py | 4 +- vunit/python_fli_compile.py | 128 ------ vunit/python_pkg.py | 146 +++++++ vunit/sim_if/activehdl.py | 7 +- vunit/ui/__init__.py | 4 +- vunit/vhdl/python/run.py | 47 +- vunit/vhdl/python/src/python_context.vhd | 2 +- vunit/vhdl/python/src/python_fli.c | 297 ------------- vunit/vhdl/python/src/python_pkg.c | 121 ++++++ vunit/vhdl/python/src/python_pkg.h | 31 ++ vunit/vhdl/python/src/python_pkg.vhd | 35 +- vunit/vhdl/python/src/python_pkg_fli.c | 216 ++++++++++ ...hon_fli_pkg_fli.vhd => python_pkg_fli.vhd} | 5 +- vunit/vhdl/python/src/python_pkg_vhpi.c | 336 +++++++++++++++ ...n_fli_pkg_vhpi.vhd => python_pkg_vhpi.vhd} | 4 +- vunit/vhdl/python/src/python_vhpi.cpp | 401 ------------------ 18 files changed, 1072 insertions(+), 1042 deletions(-) delete mode 100644 vunit/python_fli_compile.py create mode 100644 vunit/python_pkg.py delete mode 100644 vunit/vhdl/python/src/python_fli.c create mode 100644 vunit/vhdl/python/src/python_pkg.c create mode 100644 vunit/vhdl/python/src/python_pkg.h create mode 100644 vunit/vhdl/python/src/python_pkg_fli.c rename vunit/vhdl/python/src/{python_fli_pkg_fli.vhd => python_pkg_fli.vhd} (98%) create mode 100644 vunit/vhdl/python/src/python_pkg_vhpi.c rename vunit/vhdl/python/src/{python_fli_pkg_vhpi.vhd => python_pkg_vhpi.vhd} (97%) delete mode 100644 vunit/vhdl/python/src/python_vhpi.cpp diff --git a/examples/vhdl/embedded_python/run.py b/examples/vhdl/embedded_python/run.py index 127ffce73..e1e043ec1 100644 --- a/examples/vhdl/embedded_python/run.py +++ b/examples/vhdl/embedded_python/run.py @@ -6,83 +6,85 @@ from pathlib import Path from vunit import VUnit -from vunit.python_fli_compile import compile_vhpi_application, compile_fli_application +from vunit.python_pkg import compile_vhpi_application, compile_fli_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 __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() - vu.enable_location_preprocessing() - 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, simulator_name) - elif simulator_name == "modelsim": - compile_fli_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.main() + root = Path(__file__).parent + + vu = VUnit.from_argv() + vu.add_vhdl_builtins() + vu.add_python() + vu.add_random() + vu.enable_location_preprocessing() + 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) + + 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.main() if __name__ == "__main__": diff --git a/examples/vhdl/embedded_python/tb_example.vhd b/examples/vhdl/embedded_python/tb_example.vhd index 0b712da75..1fb8eb23a 100644 --- a/examples/vhdl/embedded_python/tb_example.vhd +++ b/examples/vhdl/embedded_python/tb_example.vhd @@ -7,7 +7,7 @@ library vunit_lib; context vunit_lib.vunit_context; context vunit_lib.python_context; -use vunit_lib.random_pkg.all; +use vunit_lib.random_pkg.all; library ieee; use ieee.std_logic_1164.all; @@ -22,20 +22,20 @@ entity tb_example is 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; + 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; @@ -43,7 +43,7 @@ begin variable input_stimuli : integer_array_t; variable seed : integer; variable test_lengths : integer_vector_ptr_t; - variable vhdl_real :real; + variable vhdl_real : real; procedure set_tcl_installation is begin @@ -62,63 +62,65 @@ begin procedure query_if(expr : boolean; check_result : check_result_t) is variable logger : logger_t; - variable log_level : log_level_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 - 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; - - 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; + 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; - - log_check_result(check_result); 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 ( + impure function new_integer_vector_ptr( vec : integer_vector; - mode : storage_mode_t := internal; - eid : index_t := -1 + 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 @@ -128,8 +130,8 @@ begin -- @formatter:off while test_suite loop - ------------------------------------------------------------------------------------- - -- Basic eval and exec examples + ------------------------------------------------------------------------------------- + -- Basic eval and exec examples ------------------------------------------------------------------------------------- if run("Test basic exec and eval") then -- python_pkg provides the Python functions exec and eval. @@ -138,26 +140,26 @@ begin -- 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):" + @@ -171,9 +173,9 @@ begin " 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 @@ -182,23 +184,23 @@ begin -- 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( @@ -207,7 +209,7 @@ begin 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 @@ -215,44 +217,44 @@ begin -- 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 - -- + + ------------------------------------------------------------------------------------- + -- 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 ------------------------------------------------------------------------------------- @@ -261,15 +263,15 @@ begin "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 + -- 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 @@ -279,7 +281,7 @@ begin ------------------------------------------------------------------------------------- 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 + -- 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"); @@ -291,7 +293,7 @@ begin 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 @@ -302,7 +304,7 @@ begin -- 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; @@ -329,22 +331,22 @@ begin 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, + + -- 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: + -- 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 + -- examples of situations where that ecosystem can be helpful ------------------------------------------------------------------------------------- -- Have you ever wished VHDL had some a of the features that SystemVerilog has? @@ -376,37 +378,37 @@ begin check(eval_integer("foo.x") /= eval_integer("foo.z")); check(eval_integer("foo.y") <= eval_integer("foo.z")); check_equal(eval_integer("foo.x") + eval_integer("foo.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) @@ -439,7 +441,7 @@ begin out_tvalid <= '0'; out_tlast <= '0'; crc_error <= '0'; - + --------------------------------------------------------------------- -- Examples of string manipulation @@ -498,9 +500,9 @@ begin 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); diff --git a/vunit/builtins.py b/vunit/builtins.py index c6d4d3cff..eeea56421 100755 --- a/vunit/builtins.py +++ b/vunit/builtins.py @@ -228,9 +228,9 @@ def _add_python(self): 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_fli_pkg_vhpi.vhd") + 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_fli_pkg_fli.vhd") + self._vunit_lib.add_source_files(VHDL_PATH / "python" / "src" / "python_pkg_fli.vhd") def _add_vhdl_logging(self): """ diff --git a/vunit/python_fli_compile.py b/vunit/python_fli_compile.py deleted file mode 100644 index 90aa93568..000000000 --- a/vunit/python_fli_compile.py +++ /dev/null @@ -1,128 +0,0 @@ -# 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 -from pathlib import Path -from glob import glob -import subprocess -import sys - -def compile_vhpi_application(run_script_root, simulator_name): - path_to_shared_lib = (run_script_root / "vunit_out" / simulator_name / "libraries" / "python").resolve() - if not path_to_shared_lib.exists(): - path_to_shared_lib.mkdir(parents=True, exist_ok=True) - 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]}" - application_path = Path(__file__).parent.resolve() / "vhdl" / "python" / "src" / "python_vhpi.cpp" - - proc = subprocess.run( - [ - "ccomp", - "-vhpi", - "-dbg", - "-verbose", - "-o", - '"' + str(path_to_shared_lib) + '"', - "-l", - python_shared_lib, - "-l", - "_tkinter", - "-I", - '"' + str(path_to_python_include) + '"', - "-L", - '"' + str(path_to_python_libs) + '"', - '"' + str(application_path) + '"', - ], - capture_output=True, - text=True, - ) - - 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): - path_to_simulator = Path(vu._simulator_class.find_prefix()).resolve() - 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, - ) - 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" - application_path = Path(__file__).parent.resolve() / "vhdl" / "python" / "src" / "python_fli.c" - - 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(application_path), - ] - - proc = subprocess.run( - args, - capture_output=True, - text=True, - ) - 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" / "modelsim" / "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_fli.o", - "-l" + python_shared_lib, - "-l_tkinter", - "-L" + str(path_to_simulator), - "-L" + str(path_to_python_libs), - "-lmtipli" - ] - - if proc.returncode != 0: - print(proc.stdout) - print(proc.stderr) - raise RuntimeError("Failed to link FLI application") - diff --git a/vunit/python_pkg.py b/vunit/python_pkg.py new file mode 100644 index 000000000..909625866 --- /dev/null +++ b/vunit/python_pkg.py @@ -0,0 +1,146 @@ +# 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 + +from pathlib import Path +from glob import glob +import subprocess +import sys + + +def compile_vhpi_application(run_script_root, vu): + 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() + 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, + ) + + 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): + path_to_simulator = Path(vu._simulator_class.find_prefix()).resolve() + 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, + ) + 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, + ) + 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, + ) + if proc.returncode != 0: + print(proc.stdout) + print(proc.stderr) + raise RuntimeError("Failed to link FLI application") diff --git a/vunit/sim_if/activehdl.py b/vunit/sim_if/activehdl.py index 1c9dc9908..ceb83c6b8 100644 --- a/vunit/sim_if/activehdl.py +++ b/vunit/sim_if/activehdl.py @@ -65,7 +65,7 @@ def supported_foreign_language_interfaces(cls): Returns set of supported foreign interfaces """ return set(["VHPI"]) - + @classmethod def supports_vhdl_package_generics(cls): """ @@ -135,8 +135,9 @@ def compile_vhdl_file_command(self, source_file): "-j", str(Path(self._library_cfg).parent), ] - +source_file.compile_options.get("activehdl.vcom_flags", []) - +[ + + source_file.compile_options.get("activehdl.vcom_flags", []) + + + [ self._std_str(source_file.get_vhdl_standard()), "-work", source_file.library.name, diff --git a/vunit/ui/__init__.py b/vunit/ui/__init__.py index 915656e41..16a40595b 100644 --- a/vunit/ui/__init__.py +++ b/vunit/ui/__init__.py @@ -117,11 +117,11 @@ 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 diff --git a/vunit/vhdl/python/run.py b/vunit/vhdl/python/run.py index 1137c6eaa..9b10d80c4 100644 --- a/vunit/vhdl/python/run.py +++ b/vunit/vhdl/python/run.py @@ -6,38 +6,39 @@ from pathlib import Path from vunit import VUnit -from vunit.python_fli_compile import compile_vhpi_application, compile_fli_application +from vunit.python_pkg import compile_vhpi_application, compile_fli_application + def remote_test(): - return 2 + return 2 def main(): - root = Path(__file__).parent + root = Path(__file__).parent + + vu = VUnit.from_argv() + vu.add_vhdl_builtins() + vu.add_python() + simulator_name = vu.get_simulator_name() - 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, simulator_name) - elif simulator_name == "modelsim": - compile_fli_application(root, vu) + 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) - lib = vu.add_library("lib") - lib.add_source_files(root / "test" / "*.vhd") + 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_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.main() + vu.main() if __name__ == "__main__": - main() + main() diff --git a/vunit/vhdl/python/src/python_context.vhd b/vunit/vhdl/python/src/python_context.vhd index a488f693a..4e927b792 100644 --- a/vunit/vhdl/python/src/python_context.vhd +++ b/vunit/vhdl/python/src/python_context.vhd @@ -9,5 +9,5 @@ context python_context is library vunit_lib; use vunit_lib.python_pkg.all; - use vunit_lib.python_fli_pkg.all; + use vunit_lib.python_ffi_pkg.all; end context; diff --git a/vunit/vhdl/python/src/python_fli.c b/vunit/vhdl/python/src/python_fli.c deleted file mode 100644 index 126611f9e..000000000 --- a/vunit/vhdl/python/src/python_fli.c +++ /dev/null @@ -1,297 +0,0 @@ -#include -#include -#include "mti.h" -// #include -// #include - -#define PY_SSIZE_T_CLEAN -#include "Python.h" - -#define MAX_VHDL_PARAMETER_STRING_LENGTH 100000 - -static PyObject* globals; -static PyObject* locals; - -void python_cleanup(void); - -static 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; -} - -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 *exc; - - exc = PyErr_GetRaisedException(); - if (exc != NULL) { - reason = get_string(exc); - Py_DECREF(exc); - } - } - - // 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 fli_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()) { - fli_error_handler("Failed to initialize Python", false); - } - - PyObject* main_module = PyImport_AddModule("__main__"); - if (main_module == NULL) { - fli_error_handler("Failed to get the main module", true); - } - - globals = PyModule_GetDict(main_module); - if (globals == NULL) { - fli_error_handler("Failed to get the global dictionary", true); - } - - // globals and locals are the same at the top-level - locals = globals; - - // 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) { - fli_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; -} - -static 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; -} - -static void handle_type_check_error(PyObject* pyobj, const char *context, const char *expr) { - PyObject* type_name = PyType_GetName(Py_TYPE(pyobj)); - if (type_name == NULL) { - py_error_handler(context, expr, "Expression evaluates to an unknown type.", true); - } - - const char* type_name_str = get_string(type_name); - Py_DECREF(type_name); - 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); -} - -static void check_conversion_error(const char *expr) { - PyObject* exc = PyErr_Occurred(); - if (exc != NULL) { - Py_DECREF(exc); - py_error_handler("target type casting evaluation result of", expr, NULL, true); - } -} - -static 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; -} - -static 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; -} - - -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.c b/vunit/vhdl/python/src/python_pkg.c new file mode 100644 index 000000000..0a8a5d1c4 --- /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 fli_error_handler_callback fli_error_handler = NULL; + +void register_py_error_handler(py_error_handler_callback callback) { + py_error_handler = callback; +} + +void register_fli_error_handler(fli_error_handler_callback callback) { + fli_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) { + PyObject* type_name = PyType_GetName(Py_TYPE(pyobj)); + if (type_name == NULL) { + py_error_handler(context, expr, "Expression evaluates to an unknown type.", + true); + } + + const char* type_name_str = get_string(type_name); + Py_DECREF(type_name); + 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..d5eb9a9de --- /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 (*fli_error_handler_callback)(const char*, bool); +void register_fli_error_handler(fli_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 index 3008f5c53..fb3a9c02f 100644 --- a/vunit/vhdl/python/src/python_pkg.vhd +++ b/vunit/vhdl/python/src/python_pkg.vhd @@ -6,7 +6,7 @@ -- -- Copyright (c) 2014-2023, Lars Asplund lars.anders.asplund@gmail.com -use work.python_fli_pkg.all; +use work.python_ffi_pkg.all; use work.path.all; use work.run_pkg.all; use work.runner_pkg.all; @@ -30,7 +30,7 @@ package python_pkg is 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 := "" @@ -44,23 +44,25 @@ package python_pkg is 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]; + 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 & ")"; + 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 run_script_path : string := run_script_path(get_cfg(runner_state)); @@ -78,7 +80,7 @@ package body python_pkg is end if; end loop; deallocate(path_items); - + -- Set module name to script name minus its extension path_items := split(run_script_name.all, "."); deallocate(run_script_name); @@ -145,7 +147,7 @@ package body python_pkg 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; @@ -162,19 +164,18 @@ package body python_pkg is 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 & "')"; + 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 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..0c964b608 --- /dev/null +++ b/vunit/vhdl/python/src/python_pkg_fli.c @@ -0,0 +1,216 @@ +// 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* exc; + + exc = PyErr_GetRaisedException(); + if (exc != NULL) { + reason = get_string(exc); + Py_DECREF(exc); + } + } + + // 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 fli_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()) { + fli_error_handler("Failed to initialize Python", false); + } + + PyObject* main_module = PyImport_AddModule("__main__"); + if (main_module == NULL) { + fli_error_handler("Failed to get the main module", true); + } + + globals = PyModule_GetDict(main_module); + if (globals == NULL) { + fli_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_fli_error_handler(fli_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) { + fli_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_fli_pkg_fli.vhd b/vunit/vhdl/python/src/python_pkg_fli.vhd similarity index 98% rename from vunit/vhdl/python/src/python_fli_pkg_fli.vhd rename to vunit/vhdl/python/src/python_pkg_fli.vhd index b3e4344e9..a2d147701 100644 --- a/vunit/vhdl/python/src/python_fli_pkg_fli.vhd +++ b/vunit/vhdl/python/src/python_pkg_fli.vhd @@ -8,7 +8,7 @@ use std.textio.all; -package python_fli_pkg is +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; @@ -45,7 +45,7 @@ package python_fli_pkg is end package; -package body python_fli_pkg is +package body python_ffi_pkg is procedure python_setup is begin report "ERROR: Failed to call foreign subprogram" severity failure; @@ -117,5 +117,4 @@ package body python_fli_pkg is 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..ace2afd5b --- /dev/null +++ b/vunit/vhdl/python/src/python_pkg_vhpi.c @@ -0,0 +1,336 @@ +// 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* exc; + + exc = PyErr_GetRaisedException(); + if (exc != NULL) { + reason = get_string(exc); + Py_DECREF(exc); + } + } + + // 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 vhpi_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()) { + vhpi_error_handler("Failed to initialize Python", false); + } + + PyObject* main_module = PyImport_AddModule("__main__"); + if (main_module == NULL) { + vhpi_error_handler("Failed to get the main module", true); + } + + globals = PyModule_GetDict(main_module); + if (globals == NULL) { + vhpi_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_fli_error_handler(vhpi_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) { + vhpi_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)) { + vhpi_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_fli_pkg_vhpi.vhd b/vunit/vhdl/python/src/python_pkg_vhpi.vhd similarity index 97% rename from vunit/vhdl/python/src/python_fli_pkg_vhpi.vhd rename to vunit/vhdl/python/src/python_pkg_vhpi.vhd index 393942d81..43abccb43 100644 --- a/vunit/vhdl/python/src/python_fli_pkg_vhpi.vhd +++ b/vunit/vhdl/python/src/python_pkg_vhpi.vhd @@ -8,7 +8,7 @@ use std.textio.all; -package python_fli_pkg is +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. @@ -40,5 +40,5 @@ package python_fli_pkg is attribute foreign of exec : procedure is "VHPI libraries/python; exec"; end package; -package body python_fli_pkg is +package body python_ffi_pkg is end package body; diff --git a/vunit/vhdl/python/src/python_vhpi.cpp b/vunit/vhdl/python/src/python_vhpi.cpp deleted file mode 100644 index 5df8b0c1c..000000000 --- a/vunit/vhdl/python/src/python_vhpi.cpp +++ /dev/null @@ -1,401 +0,0 @@ -#include -#include -#include - -#define PY_SSIZE_T_CLEAN -#include "Python.h" - -#define MAX_VHDL_PARAMETER_STRING_LENGTH 100000 - -static PyObject* globals; -static PyObject* locals; - -using namespace std; - -PLI_VOID python_cleanup(const struct vhpiCbDataS* cb_p); - -static char* get_string(PyObject *pyobj) { - PyObject* str = PyObject_Str(pyobj); - if (str == nullptr) { - return nullptr; - } - - PyObject* str_utf_8 = PyUnicode_AsEncodedString(str, "utf-8", NULL); - Py_DECREF(str); - if (str_utf_8 == nullptr) { - return nullptr; - } - - char* result = PyBytes_AS_STRING(str_utf_8); - Py_DECREF(str_utf_8); - - return result; -} -static void py_error_handler(const char *context, const char *code_or_expr = nullptr, const char *reason = nullptr, bool cleanup = true) { - const char* unknown_error = "Unknown error"; - - // Use provided error reason or try extracting the reason from the Python exception - if (reason == nullptr) { - PyObject *exc; - - exc = PyErr_GetRaisedException(); - if (exc != nullptr) { - reason = get_string(exc); - Py_DECREF(exc); - } - } - - // Clean-up Python session first in case vhpi_assert stops the simulation - if (cleanup) { - python_cleanup(nullptr); - } - - // Output error message - reason = reason == nullptr ? unknown_error : reason; - if (code_or_expr == nullptr) { - 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 vhpi_error_handler(const char* context, bool cleanup = true) { - vhpiErrorInfoT err; - - // Clean-up Python session first in case vhpi_assert stops the simulation - if (cleanup) { - python_cleanup(nullptr); - } - - 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()) { - vhpi_error_handler("Failed to initialize Python", false); - } - - PyObject* main_module = PyImport_AddModule("__main__"); - if (main_module == nullptr) { - vhpi_error_handler("Failed to get the main module"); - } - - globals = PyModule_GetDict(main_module); - if (globals == nullptr) { - vhpi_error_handler("Failed to get the global dictionary"); - } - - // globals and locals are the same at the top-level - locals = globals; -} - -PLI_VOID python_cleanup(const struct vhpiCbDataS* cb_p) { - if (locals != nullptr) { - 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 == nullptr) { - vhpi_error_handler("getting VHDL parameter handle"); - } - - 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)) { - vhpi_error_handler("getting VHDL parameter value"); - } - - return vhdl_parameter_string; -} - -static PyObject* eval(const char *expr) { - PyObject* pyobj = PyRun_String(expr, Py_eval_input, globals, locals); - if (pyobj == nullptr) { - py_error_handler("evaluating", expr); - } - - return pyobj; -} - -static void handle_type_check_error(PyObject* pyobj, const char *context, const char *expr) { - PyObject* type_name = PyType_GetName(Py_TYPE(pyobj)); - if (type_name == nullptr) { - py_error_handler(context, expr, "Expression evaluates to an unknown type."); - } - - const char* type_name_str = get_string(type_name); - Py_DECREF(type_name); - if (type_name_str == nullptr) { - py_error_handler(context, expr, "Expression evaluates to an unknown type."); - } - - string error_message = "Expression evaluates to " + string(type_name_str); - py_error_handler(context, expr, error_message.c_str()); -} - -static void check_conversion_error(const char *expr) { - PyObject* exc = PyErr_Occurred(); - if (exc != nullptr) { - Py_DECREF(exc); - py_error_handler("parsing evaluation result of", expr); - } -} - -static int get_integer(PyObject* pyobj, const char* expr, bool dec_ref_count = true) { - // 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) or (value < -pow(2, 31))) { - py_error_handler("parsing evaluation result of", expr, "Result out of VHDL integer range."); - } - - return int(value); -} - -static double get_real(PyObject* pyobj, const char* expr, bool dec_ref_count = true) { - // 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) or (value < -3.4028234664e38)) { - py_error_handler("parsing evaluation result of", expr, "Result out of VHDL real range."); - } - - return value; -} - - -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); - if (vhpi_put_value(cb_p->obj, &vhdl_result, vhpiDeposit)) { - py_error_handler("returning result for evaluation of", expr); - } -} - -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); - if (vhpi_put_value(cb_p->obj, &vhdl_result, vhpiDeposit)) { - py_error_handler("returning result for evaluation of", expr); - } -} - -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); - } - - if (vhpi_put_value(cb_p->obj, &vhdl_result, vhpiDeposit)) { - free(int_array); - py_error_handler("returning result for evaluation of", expr); - } - - 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); - } - - if (vhpi_put_value(cb_p->obj, &vhdl_result, vhpiDeposit)) { - free(double_array); - py_error_handler("returning result for evaluation of", expr); - } - - 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); - } - - if (vhpi_put_value(cb_p->obj, &vhdl_result, vhpiDeposit)) { - py_error_handler("returning result for evaluation of", expr); - } -} - - -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) == nullptr) { - py_error_handler("executing", code); - } -} - -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, nullptr, python_setup}; - // vhpi_assert doesn't seem to work at this point - assert(vhpi_register_foreignf(&python_setup_data) != nullptr); - - char python_cleanup_name[] = "python_cleanup"; - vhpiForeignDataT python_cleanup_data = {vhpiProcF, library_name, python_cleanup_name, nullptr, python_cleanup}; - assert(vhpi_register_foreignf(&python_cleanup_data) != nullptr); - - char eval_integer_name[] = "eval_integer"; - vhpiForeignDataT eval_integer_data = {vhpiProcF, library_name, eval_integer_name, nullptr, eval_integer}; - assert(vhpi_register_foreignf(&eval_integer_data) != nullptr); - - char eval_real_name[] = "eval_real"; - vhpiForeignDataT eval_real_data = {vhpiProcF, library_name, eval_real_name, nullptr, eval_real}; - assert(vhpi_register_foreignf(&eval_real_data) != nullptr); - - char eval_integer_vector_name[] = "eval_integer_vector"; - vhpiForeignDataT eval_integer_vector_data = {vhpiProcF, library_name, eval_integer_vector_name, nullptr, eval_integer_vector}; - assert(vhpi_register_foreignf(&eval_integer_vector_data) != nullptr); - - char eval_real_vector_name[] = "eval_real_vector"; - vhpiForeignDataT eval_real_vector_data = {vhpiProcF, library_name, eval_real_vector_name, nullptr, eval_real_vector}; - assert(vhpi_register_foreignf(&eval_real_vector_data) != nullptr); - - char eval_string_name[] = "eval_string"; - vhpiForeignDataT eval_string_data = {vhpiProcF, library_name, eval_string_name, nullptr, eval_string}; - assert(vhpi_register_foreignf(&eval_string_data) != nullptr); - - char exec_name[] = "exec"; - vhpiForeignDataT exec_data = {vhpiProcF, library_name, exec_name, nullptr, exec}; - assert(vhpi_register_foreignf(&exec_data) != nullptr); -} - -PLI_VOID (*vhpi_startup_routines[])() = {register_foreign_subprograms, nullptr}; From 50ce6344b46c4caf54087ec2d151118a65d834dd Mon Sep 17 00:00:00 2001 From: Lars Asplund Date: Fri, 26 Jan 2024 21:39:47 +0100 Subject: [PATCH 4/8] Fixed linting --- examples/vhdl/embedded_python/run.py | 10 ++--- vunit/builtins.py | 1 - vunit/python_pkg.py | 67 +++++++++++++++++----------- vunit/sim_if/activehdl.py | 3 +- vunit/test/runner.py | 14 +++--- vunit/test/suites.py | 2 +- vunit/ui/__init__.py | 52 ++++++++++----------- vunit/vhdl/check/src/check.vhd | 14 +++--- vunit/vhdl/python/run.py | 4 +- vunit/vhdl/run/src/run_api.vhd | 2 +- 10 files changed, 90 insertions(+), 79 deletions(-) diff --git a/examples/vhdl/embedded_python/run.py b/examples/vhdl/embedded_python/run.py index e1e043ec1..83f874f9d 100644 --- a/examples/vhdl/embedded_python/run.py +++ b/examples/vhdl/embedded_python/run.py @@ -13,7 +13,7 @@ def hello_world(): print("Hello World") -class Plot(): +class Plot: def __init__(self, x_points, y_limits, title, x_label, y_label): from matplotlib import pyplot as plt @@ -30,7 +30,7 @@ def __init__(self, x_points, y_limits, title, x_label, y_label): 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-') + (line,) = plt.plot(x_vector, y_vector, "r-") fig.canvas.draw() fig.canvas.flush_events() plt.show(block=False) @@ -78,8 +78,8 @@ def main(): 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"]) + 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"]) @@ -88,4 +88,4 @@ def main(): if __name__ == "__main__": - main() + main() diff --git a/vunit/builtins.py b/vunit/builtins.py index eeea56421..cfeaa0668 100755 --- a/vunit/builtins.py +++ b/vunit/builtins.py @@ -219,7 +219,6 @@ def _add_python(self): if not self._vhdl_standard >= VHDL.STD_2008: raise RuntimeError("Python package only supports vhdl 2008 and later") - # TODO: Create enums for FLIs python_package_supported_flis = set(["VHPI", "FLI"]) simulator_supported_flis = self._simulator_class.supported_foreign_language_interfaces() if not python_package_supported_flis & simulator_supported_flis: diff --git a/vunit/python_pkg.py b/vunit/python_pkg.py index 909625866..2ef1aa653 100644 --- a/vunit/python_pkg.py +++ b/vunit/python_pkg.py @@ -4,6 +4,9 @@ # # Copyright (c) 2014-2023, 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 @@ -11,6 +14,9 @@ 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) @@ -20,31 +26,34 @@ def compile_vhpi_application(run_script_root, vu): 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() + 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])], + 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: @@ -53,8 +62,11 @@ def compile_vhpi_application(run_script_root, vu): raise RuntimeError("Failed to compile VHPI application") -def compile_fli_application(run_script_root, vu): - path_to_simulator = Path(vu._simulator_class.find_prefix()).resolve() +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? @@ -63,6 +75,7 @@ def compile_fli_application(run_script_root, vu): [vsim_executable, "-version"], capture_output=True, text=True, + check=False, ) if proc.returncode != 0: print(proc.stderr) @@ -94,16 +107,17 @@ def compile_fli_application(run_script_root, vu): args += ["-ansi", "-pedantic"] args += [ - '-I' + str(path_to_simulator_include), - '-I' + str(path_to_python_include), + "-I" + str(path_to_simulator_include), + "-I" + str(path_to_python_include), "-freg-struct-return", - str(c_file_path) + str(c_file_path), ] proc = subprocess.run( args, capture_output=True, text=True, + check=False, ) if proc.returncode != 0: print(proc.stdout) @@ -139,6 +153,7 @@ def compile_fli_application(run_script_root, vu): args, capture_output=True, text=True, + check=False, ) if proc.returncode != 0: print(proc.stdout) diff --git a/vunit/sim_if/activehdl.py b/vunit/sim_if/activehdl.py index ceb83c6b8..581cdbcbc 100644 --- a/vunit/sim_if/activehdl.py +++ b/vunit/sim_if/activehdl.py @@ -136,8 +136,7 @@ def compile_vhdl_file_command(self, source_file): str(Path(self._library_cfg).parent), ] + source_file.compile_options.get("activehdl.vcom_flags", []) - + - [ + + [ self._std_str(source_file.get_vhdl_standard()), "-work", source_file.library.name, diff --git a/vunit/test/runner.py b/vunit/test/runner.py index 436091593..8ba59c7c9 100644 --- a/vunit/test/runner.py +++ b/vunit/test/runner.py @@ -33,7 +33,7 @@ class TestRunner(object): # pylint: disable=too-many-instance-attributes VERBOSITY_NORMAL = 1 VERBOSITY_VERBOSE = 2 - def __init__(# pylint: disable=too-many-arguments + def __init__( # pylint: disable=too-many-arguments self, report, output_path, @@ -200,7 +200,7 @@ def _add_skipped_tests(self, test_suite, results, start_time, num_tests, output_ results[name] = SKIPPED self._add_results(test_suite, results, start_time, num_tests, output_file_name) - def _run_test_suite(# pylint: disable=too-many-locals + def _run_test_suite( # pylint: disable=too-many-locals self, test_suite, write_stdout, num_tests, output_path, output_file_name ): """ @@ -226,7 +226,7 @@ def _run_test_suite(# pylint: disable=too-many-locals if write_stdout: output_from = self._stdout_ansi else: - color_output_file = Path(color_output_file_name).open(# pylint: disable=consider-using-with + color_output_file = Path(color_output_file_name).open( # pylint: disable=consider-using-with "w", encoding="utf-8" ) output_from = color_output_file @@ -244,9 +244,7 @@ def read_output(): return contents results = test_suite.run( - output_path=output_path, - read_output=read_output, - run_script_path=self._run_script_path + 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) @@ -302,7 +300,7 @@ def _create_test_mapping_file(self, test_suites): mapping.add(f"{Path(test_output).name!s} {test_suite.name!s}") # Sort by everything except hash - mapping = sorted(mapping, key=lambda value: value[value.index(" "):]) + mapping = sorted(mapping, key=lambda value: value[value.index(" ") :]) with mapping_file_name.open("w", encoding="utf-8") as fptr: for value in mapping: @@ -457,7 +455,7 @@ def wrap(file_obj, use_color=True): NOTE: imports colorama here to avoid dependency from setup.py importing VUnit before colorama is installed """ - from colorama import (# type: ignore # pylint: disable=import-outside-toplevel + from colorama import ( # type: ignore # pylint: disable=import-outside-toplevel AnsiToWin32, ) diff --git a/vunit/test/suites.py b/vunit/test/suites.py index a33844baf..60fd3bc9f 100644 --- a/vunit/test/suites.py +++ b/vunit/test/suites.py @@ -260,7 +260,7 @@ def _read_test_results(self, file_name): # pylint: disable=too-many-branches for line in test_results.splitlines(): if line.startswith("test_start:"): - test_name = line[len("test_start:"):] + test_name = line[len("test_start:") :] if test_name not in test_starts: test_starts.append(test_name) diff --git a/vunit/ui/__init__.py b/vunit/ui/__init__.py index 16a40595b..125c2d63f 100644 --- a/vunit/ui/__init__.py +++ b/vunit/ui/__init__.py @@ -60,7 +60,7 @@ class VUnit(object): # pylint: disable=too-many-instance-attributes, too-many-p def from_argv( cls, argv=None, - vhdl_standard: Optional[str]=None, + vhdl_standard: Optional[str] = None, ): """ Create VUnit instance from command line arguments. @@ -91,7 +91,7 @@ def from_argv( def from_args( cls, args, - vhdl_standard: Optional[str]=None, + vhdl_standard: Optional[str] = None, ): """ Create VUnit instance from args namespace. @@ -115,7 +115,7 @@ def from_args( def __init__( self, args, - vhdl_standard: Optional[str]=None, + vhdl_standard: Optional[str] = None, ): self._args = args @@ -217,7 +217,7 @@ def _which_vhdl_standard(self, vhdl_standard: Optional[str]) -> VHDLStandard: return VHDL.standard(vhdl_standard) - def add_external_library(self, library_name, path: Union[str, Path], vhdl_standard: Optional[str]=None): + def add_external_library(self, library_name, path: Union[str, Path], vhdl_standard: Optional[str] = None): """ Add an externally compiled library as a black-box @@ -243,7 +243,7 @@ def add_external_library(self, library_name, path: Union[str, Path], vhdl_standa ) return self.library(library_name) - def add_source_files_from_csv(self, project_csv_path: Union[str, Path], vhdl_standard: Optional[str]=None): + def add_source_files_from_csv(self, project_csv_path: Union[str, Path], vhdl_standard: Optional[str] = None): """ Add a project configuration, mapping all the libraries and files @@ -278,8 +278,8 @@ def add_source_files_from_csv(self, project_csv_path: Union[str, Path], vhdl_sta def add_library( self, library_name: str, - vhdl_standard: Optional[str]=None, - allow_duplicate: Optional[bool]=False, + vhdl_standard: Optional[str] = None, + allow_duplicate: Optional[bool] = False, ): """ Add a library managed by VUnit. @@ -321,7 +321,7 @@ def library(self, library_name: str): def get_libraries( self, pattern="*", - allow_empty: Optional[bool]=False, + allow_empty: Optional[bool] = False, ): """ Get a list of libraries @@ -342,7 +342,7 @@ def get_libraries( return LibraryList(results) - def set_attribute(self, name: str, value: str, allow_empty: Optional[bool]=False): + def set_attribute(self, name: str, value: str, allow_empty: Optional[bool] = False): """ Set a value of attribute in all |configurations| @@ -363,7 +363,7 @@ def set_attribute(self, name: str, value: str, allow_empty: Optional[bool]=False for test_bench in check_not_empty(test_benches, allow_empty, "No test benches found"): test_bench.set_attribute(name, value) - def set_generic(self, name: str, value: str, allow_empty: Optional[bool]=False): + def set_generic(self, name: str, value: str, allow_empty: Optional[bool] = False): """ Set a value of generic in all |configurations| @@ -384,7 +384,7 @@ def set_generic(self, name: str, value: str, allow_empty: Optional[bool]=False): for test_bench in check_not_empty(test_benches, allow_empty, "No test benches found"): test_bench.set_generic(name.lower(), value) - def set_parameter(self, name: str, value: str, allow_empty: Optional[bool]=False): + def set_parameter(self, name: str, value: str, allow_empty: Optional[bool] = False): """ Set value of parameter in all |configurations| @@ -409,8 +409,8 @@ def set_sim_option( self, name: str, value: str, - allow_empty: Optional[bool]=False, - overwrite: Optional[bool]=True, + allow_empty: Optional[bool] = False, + overwrite: Optional[bool] = True, ): """ Set simulation option in all |configurations| @@ -433,7 +433,7 @@ def set_sim_option( for test_bench in check_not_empty(test_benches, allow_empty, "No test benches found"): test_bench.set_sim_option(name, value, overwrite) - def set_compile_option(self, name: str, value: str, allow_empty: Optional[bool]=False): + def set_compile_option(self, name: str, value: str, allow_empty: Optional[bool] = False): """ Set compile option of all files @@ -455,7 +455,7 @@ def set_compile_option(self, name: str, value: str, allow_empty: Optional[bool]= for source_file in check_not_empty(source_files, allow_empty, "No source files found"): source_file.set_compile_option(name, value) - def add_compile_option(self, name: str, value: str, allow_empty: Optional[bool]=False): + def add_compile_option(self, name: str, value: str, allow_empty: Optional[bool] = False): """ Add compile option to all files @@ -470,7 +470,7 @@ def add_compile_option(self, name: str, value: str, allow_empty: Optional[bool]= for source_file in check_not_empty(source_files, allow_empty, "No source files found"): source_file.add_compile_option(name, value) - def get_source_file(self, file_name: Union[str, Path], library_name: Optional[str]=None): + def get_source_file(self, file_name: Union[str, Path], library_name: Optional[str] = None): """ Get a source file @@ -494,8 +494,8 @@ def get_source_file(self, file_name: Union[str, Path], library_name: Optional[st def get_source_files( self, pattern="*", - library_name: Optional[str]=None, - allow_empty: Optional[bool]=False, + library_name: Optional[str] = None, + allow_empty: Optional[bool] = False, ): """ Get a list of source files @@ -523,21 +523,21 @@ def get_source_files( results, allow_empty, f"Pattern {pattern!r} did not match any file" - +(f"within library {library_name!s}" if library_name is not None else ""), + + (f"within library {library_name!s}" if library_name is not None else ""), ) return SourceFileList(results) - def add_source_files(# pylint: disable=too-many-arguments + def add_source_files( # pylint: disable=too-many-arguments self, pattern, library_name: str, preprocessors=None, include_dirs=None, defines=None, - allow_empty: Optional[bool]=False, - vhdl_standard: Optional[str]=None, - no_parse: Optional[bool]=False, + allow_empty: Optional[bool] = False, + vhdl_standard: Optional[str] = None, + no_parse: Optional[bool] = False, file_type=None, ): """ @@ -573,15 +573,15 @@ def add_source_files(# pylint: disable=too-many-arguments file_type=file_type, ) - def add_source_file(# pylint: disable=too-many-arguments + def add_source_file( # pylint: disable=too-many-arguments self, file_name: Union[str, Path], library_name: str, preprocessors=None, include_dirs=None, defines=None, - vhdl_standard: Optional[str]=None, - no_parse: Optional[bool]=False, + vhdl_standard: Optional[str] = None, + no_parse: Optional[bool] = False, file_type=None, ): """ diff --git a/vunit/vhdl/check/src/check.vhd b/vunit/vhdl/check/src/check.vhd index b156f5053..fd883cfc2 100644 --- a/vunit/vhdl/check/src/check.vhd +++ b/vunit/vhdl/check/src/check.vhd @@ -130,37 +130,37 @@ package body check_pkg is end if; 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; - + ----------------------------------------------------------------------------- diff --git a/vunit/vhdl/python/run.py b/vunit/vhdl/python/run.py index 9b10d80c4..7e052d90a 100644 --- a/vunit/vhdl/python/run.py +++ b/vunit/vhdl/python/run.py @@ -31,8 +31,8 @@ def main(): 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"]) + 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"]) diff --git a/vunit/vhdl/run/src/run_api.vhd b/vunit/vhdl/run/src/run_api.vhd index 6d04fc4d6..2866e1646 100644 --- a/vunit/vhdl/run/src/run_api.vhd +++ b/vunit/vhdl/run/src/run_api.vhd @@ -162,7 +162,7 @@ package run_pkg is impure function tb_path ( constant runner_cfg : string) return string; - + impure function run_script_path( constant runner_cfg : string) return string; From 5459e70517637723a4d7b2092d2749e5382c2d9a Mon Sep 17 00:00:00 2001 From: Lars Asplund Date: Fri, 26 Jan 2024 22:32:43 +0100 Subject: [PATCH 5/8] Fixed unit tests. --- tests/unit/test_test_runner.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) 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 """ From c27dc294ce9c4aced5dd45285a0f3364e8b0641e Mon Sep 17 00:00:00 2001 From: Lars Asplund Date: Sat, 3 Feb 2024 23:09:39 +0100 Subject: [PATCH 6/8] Added support for NVC --- examples/vhdl/embedded_python/run.py | 5 +- examples/vhdl/embedded_python/tb_example.vhd | 35 ++- vunit/builtins.py | 4 +- vunit/python_pkg.py | 53 ++++ vunit/sim_if/nvc.py | 7 + vunit/vhdl/python/run.py | 5 +- vunit/vhdl/python/src/python_pkg.c | 6 +- vunit/vhdl/python/src/python_pkg.h | 4 +- vunit/vhdl/python/src/python_pkg.vhd | 28 +- vunit/vhdl/python/src/python_pkg_fli.c | 23 +- vunit/vhdl/python/src/python_pkg_vhpi.c | 25 +- .../vhdl/python/src/python_pkg_vhpidirect.vhd | 111 ++++++++ .../python/src/python_pkg_vhpidirect_nvc.c | 207 +++++++++++++++ vunit/vhdl/python/test/tb_python_pkg.vhd | 251 +++++++++--------- 14 files changed, 576 insertions(+), 188 deletions(-) create mode 100644 vunit/vhdl/python/src/python_pkg_vhpidirect.vhd create mode 100644 vunit/vhdl/python/src/python_pkg_vhpidirect_nvc.c diff --git a/examples/vhdl/embedded_python/run.py b/examples/vhdl/embedded_python/run.py index 83f874f9d..1f8cb447a 100644 --- a/examples/vhdl/embedded_python/run.py +++ b/examples/vhdl/embedded_python/run.py @@ -6,7 +6,7 @@ from pathlib import Path from vunit import VUnit -from vunit.python_pkg import compile_vhpi_application, compile_fli_application +from vunit.python_pkg import compile_vhpi_application, compile_fli_application, compile_vhpidirect_nvc_application def hello_world(): @@ -74,6 +74,8 @@ def main(): compile_vhpi_application(root, vu) elif simulator_name == "modelsim": compile_fli_application(root, vu) + elif simulator_name == "nvc": + compile_vhpidirect_nvc_application(root, vu) lib = vu.add_library("lib") lib.add_source_files(root / "*.vhd") @@ -83,6 +85,7 @@ def main(): # 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() diff --git a/examples/vhdl/embedded_python/tb_example.vhd b/examples/vhdl/embedded_python/tb_example.vhd index 1fb8eb23a..21bf78721 100644 --- a/examples/vhdl/embedded_python/tb_example.vhd +++ b/examples/vhdl/embedded_python/tb_example.vhd @@ -354,30 +354,27 @@ begin -- 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. - -- The example is taken directly from the examples provided with the Python package. elsif run("Test constraint solving") then - exec("from cocotb_coverage import crv"); -- Install with pip install cocotb-coverage + exec("from constraint import Problem"); -- Install with pip install python-constraint + exec("from random import choice"); exec( - "class RandExample(crv.Randomized):" + - " def __init__(self, z):" + - " crv.Randomized.__init__(self)" + - " self.x = 0" + - " self.y = 0" + - " self.z = z" + - " self.x_c = lambda x, z: x > z" + - " self.add_rand('x', list(range(16)))" + - " self.add_rand('y', list(range(16)))" + - " self.add_constraint(lambda x, z : x != z)" + - " self.add_constraint(lambda y, z : y <= z)" + - " self.add_constraint(lambda x, y : x + y == 8)" + "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)" ); - exec("foo = RandExample(5)"); - exec("foo.randomize()"); -- Check that the constraints were met - check(eval_integer("foo.x") /= eval_integer("foo.z")); - check(eval_integer("foo.y") <= eval_integer("foo.z")); - check_equal(eval_integer("foo.x") + eval_integer("foo.y"), 8); + 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 diff --git a/vunit/builtins.py b/vunit/builtins.py index cfeaa0668..46467a3b0 100755 --- a/vunit/builtins.py +++ b/vunit/builtins.py @@ -219,7 +219,7 @@ def _add_python(self): 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"]) + python_package_supported_flis = set(["VHPI", "FLI", "VHPIDIRECT"]) 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)}") @@ -230,6 +230,8 @@ def _add_python(self): 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" in simulator_supported_flis: + self._vunit_lib.add_source_files(VHDL_PATH / "python" / "src" / "python_pkg_vhpidirect.vhd") def _add_vhdl_logging(self): """ diff --git a/vunit/python_pkg.py b/vunit/python_pkg.py index 2ef1aa653..8c3a5f7ad 100644 --- a/vunit/python_pkg.py +++ b/vunit/python_pkg.py @@ -159,3 +159,56 @@ def compile_fli_application(run_script_root, vu): # pylint: disable=too-many-lo 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") diff --git a/vunit/sim_if/nvc.py b/vunit/sim_if/nvc.py index 4081944dc..4c6cf212d 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"]) + def setup_library_mapping(self, project): """ Setup library mapping diff --git a/vunit/vhdl/python/run.py b/vunit/vhdl/python/run.py index 7e052d90a..9ebf86c35 100644 --- a/vunit/vhdl/python/run.py +++ b/vunit/vhdl/python/run.py @@ -6,7 +6,7 @@ from pathlib import Path from vunit import VUnit -from vunit.python_pkg import compile_vhpi_application, compile_fli_application +from vunit.python_pkg import compile_vhpi_application, compile_fli_application, compile_vhpidirect_nvc_application def remote_test(): @@ -27,6 +27,8 @@ def main(): compile_vhpi_application(root, vu) elif simulator_name == "modelsim": compile_fli_application(root, vu) + elif simulator_name == "nvc": + compile_vhpidirect_nvc_application(root, vu) lib = vu.add_library("lib") lib.add_source_files(root / "test" / "*.vhd") @@ -36,6 +38,7 @@ def main(): # 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() diff --git a/vunit/vhdl/python/src/python_pkg.c b/vunit/vhdl/python/src/python_pkg.c index 0a8a5d1c4..058730f6a 100644 --- a/vunit/vhdl/python/src/python_pkg.c +++ b/vunit/vhdl/python/src/python_pkg.c @@ -9,14 +9,14 @@ #include "python_pkg.h" static py_error_handler_callback py_error_handler = NULL; -static fli_error_handler_callback fli_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_fli_error_handler(fli_error_handler_callback callback) { - fli_error_handler = callback; +void register_ffi_error_handler(ffi_error_handler_callback callback) { + ffi_error_handler = callback; } char* get_string(PyObject* pyobj) { diff --git a/vunit/vhdl/python/src/python_pkg.h b/vunit/vhdl/python/src/python_pkg.h index d5eb9a9de..950a8d555 100644 --- a/vunit/vhdl/python/src/python_pkg.h +++ b/vunit/vhdl/python/src/python_pkg.h @@ -19,8 +19,8 @@ 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 (*fli_error_handler_callback)(const char*, bool); -void register_fli_error_handler(fli_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); diff --git a/vunit/vhdl/python/src/python_pkg.vhd b/vunit/vhdl/python/src/python_pkg.vhd index fb3a9c02f..34e6316ce 100644 --- a/vunit/vhdl/python/src/python_pkg.vhd +++ b/vunit/vhdl/python/src/python_pkg.vhd @@ -45,9 +45,15 @@ package python_pkg is ) 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 + function ssin (v : real) return real is + begin + assert false severity failure; + end; + -- @formatter:off procedure import_module_from_file(module_path, as_module_name : string) is constant spec_name : string := "__" & as_module_name & "_spec"; @@ -65,16 +71,16 @@ package body python_pkg is -- @formatter:on procedure import_run_script(module_name : string := "") is - constant run_script_path : string := run_script_path(get_cfg(runner_state)); + constant script_path : string := run_script_path(get_cfg(runner_state)); variable path_items : lines_t; - variable run_script_name : line; + variable script_name : line; begin if module_name = "" then -- Extract the last item in the full path - path_items := split(run_script_path, "/"); + path_items := split(script_path, "/"); for idx in path_items'range loop if idx = path_items'right then - run_script_name := path_items(idx); + script_name := path_items(idx); else deallocate(path_items(idx)); end if; @@ -82,17 +88,17 @@ package body python_pkg is deallocate(path_items); -- Set module name to script name minus its extension - path_items := split(run_script_name.all, "."); - deallocate(run_script_name); + 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(run_script_path, path_items(idx).all); + 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(run_script_path, module_name); + import_module_from_file(script_path, module_name); end if; end; @@ -131,9 +137,9 @@ package body python_pkg is begin swrite(l, "["); for idx in vec'range loop - -- to_string of real seems to express an integer real as an integer: to_string(1.0) is "1". - -- use real'image instead. - swrite(l, real'image(vec(idx))); + -- 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; diff --git a/vunit/vhdl/python/src/python_pkg_fli.c b/vunit/vhdl/python/src/python_pkg_fli.c index 0c964b608..0b7b7d6ac 100644 --- a/vunit/vhdl/python/src/python_pkg_fli.c +++ b/vunit/vhdl/python/src/python_pkg_fli.c @@ -26,13 +26,12 @@ static void py_error_handler(const char* context, const char* code_or_expr, // Use provided error reason or try extracting the reason from the Python // exception if (reason == NULL) { - PyObject* exc; - - exc = PyErr_GetRaisedException(); - if (exc != NULL) { - reason = get_string(exc); - Py_DECREF(exc); + 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 @@ -50,7 +49,7 @@ static void py_error_handler(const char* context, const char* code_or_expr, mti_FatalError(); } -static void fli_error_handler(const char* context, bool cleanup) { +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(); @@ -63,24 +62,24 @@ static void fli_error_handler(const char* context, bool cleanup) { void python_setup(void) { Py_Initialize(); if (!Py_IsInitialized()) { - fli_error_handler("Failed to initialize Python", false); + ffi_error_handler("Failed to initialize Python", false); } PyObject* main_module = PyImport_AddModule("__main__"); if (main_module == NULL) { - fli_error_handler("Failed to get the main module", true); + ffi_error_handler("Failed to get the main module", true); } globals = PyModule_GetDict(main_module); if (globals == NULL) { - fli_error_handler("Failed to get the global dictionary", true); + 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_fli_error_handler(fli_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 @@ -101,7 +100,7 @@ class __EvalResult__():\n\ __eval_result__=__EvalResult__()\n"; if (PyRun_String(code, Py_file_input, globals, locals) == NULL) { - fli_error_handler("Failed to initialize predefined Python objects", true); + ffi_error_handler("Failed to initialize predefined Python objects", true); } } diff --git a/vunit/vhdl/python/src/python_pkg_vhpi.c b/vunit/vhdl/python/src/python_pkg_vhpi.c index ace2afd5b..e9860a014 100644 --- a/vunit/vhdl/python/src/python_pkg_vhpi.c +++ b/vunit/vhdl/python/src/python_pkg_vhpi.c @@ -26,13 +26,12 @@ static void py_error_handler(const char* context, const char* code_or_expr, // Use provided error reason or try extracting the reason from the Python // exception if (reason == NULL) { - PyObject* exc; - - exc = PyErr_GetRaisedException(); - if (exc != NULL) { - reason = get_string(exc); - Py_DECREF(exc); + 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 @@ -53,7 +52,7 @@ static void py_error_handler(const char* context, const char* code_or_expr, vhpi_control(vhpiStop); } -static void vhpi_error_handler(const char* context, bool cleanup) { +static void ffi_error_handler(const char* context, bool cleanup) { vhpiErrorInfoT err; // Clean-up Python session first in case vhpi_assert stops the simulation @@ -76,24 +75,24 @@ static void vhpi_error_handler(const char* context, bool cleanup) { PLI_VOID python_setup(const struct vhpiCbDataS* cb_p) { Py_Initialize(); if (!Py_IsInitialized()) { - vhpi_error_handler("Failed to initialize Python", false); + ffi_error_handler("Failed to initialize Python", false); } PyObject* main_module = PyImport_AddModule("__main__"); if (main_module == NULL) { - vhpi_error_handler("Failed to get the main module", true); + ffi_error_handler("Failed to get the main module", true); } globals = PyModule_GetDict(main_module); if (globals == NULL) { - vhpi_error_handler("Failed to get the global dictionary", true); + 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_fli_error_handler(vhpi_error_handler); + register_ffi_error_handler(ffi_error_handler); } PLI_VOID python_cleanup(const struct vhpiCbDataS* cb_p) { @@ -111,7 +110,7 @@ static const char* get_parameter(const struct vhpiCbDataS* cb_p) { vhpiHandleT parameter_handle = vhpi_handle_by_index(vhpiParamDecls, cb_p->obj, 0); if (parameter_handle == NULL) { - vhpi_error_handler("getting VHDL parameter handle", true); + ffi_error_handler("getting VHDL parameter handle", true); } vhpiValueT parameter; @@ -122,7 +121,7 @@ static const char* get_parameter(const struct vhpiCbDataS* cb_p) { parameter.format = vhpiStrVal; if (vhpi_get_value(parameter_handle, ¶meter)) { - vhpi_error_handler("getting VHDL parameter value", true); + ffi_error_handler("getting VHDL parameter value", true); } return vhdl_parameter_string; diff --git a/vunit/vhdl/python/src/python_pkg_vhpidirect.vhd b/vunit/vhdl/python/src/python_pkg_vhpidirect.vhd new file mode 100644 index 000000000..5c695f4c2 --- /dev/null +++ b/vunit/vhdl/python/src/python_pkg_vhpidirect.vhd @@ -0,0 +1,111 @@ +-- 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 + +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]; + + procedure exec(code : string); + attribute foreign of exec : procedure is "VHPIDIRECT exec"; +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 + attribute foreign of get_integer_vector : procedure is "VHPIDIRECT get_integer_vector"; + 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 + attribute foreign of get_real_vector : procedure is "VHPIDIRECT get_real_vector"; + 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 + attribute foreign of get_py_string : procedure is "VHPIDIRECT get_py_string"; + 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..e1fd85694 --- /dev/null +++ b/vunit/vhdl/python/src/python_pkg_vhpidirect_nvc.c @@ -0,0 +1,207 @@ +// 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 + +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/test/tb_python_pkg.vhd b/vunit/vhdl/python/test/tb_python_pkg.vhd index 9bb1a819c..be3789fda 100644 --- a/vunit/vhdl/python/test/tb_python_pkg.vhd +++ b/vunit/vhdl/python/test/tb_python_pkg.vhd @@ -98,131 +98,132 @@ begin 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'(-2 ** 31, -1, 0, 1, 2 ** 31 - 1))); - check_equal(get(vhdl_integer_vector_ptr, 0), -2 ** 31); - 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), 2 ** 31 - 1); - - 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 buffer isn't flushed until end of simulation - exec("from time import sleep"); - exec("print('Before sleep', flush=True)"); - exec("sleep(5)"); - exec("print('After sleep')"); + 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'(-2 ** 31, -1, 0, 1, 2 ** 31 - 1))); + check_equal(get(vhdl_integer_vector_ptr, 0), -2 ** 31); + 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), 2 ** 31 - 1); + + 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; From 9718ffe9f7e5408d6ef731e1f33c41009048b9a7 Mon Sep 17 00:00:00 2001 From: Lars Asplund Date: Sun, 4 Feb 2024 17:55:18 +0100 Subject: [PATCH 7/8] Updated to support Python 3.10 (at least). --- vunit/vhdl/python/src/python_pkg.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/vunit/vhdl/python/src/python_pkg.c b/vunit/vhdl/python/src/python_pkg.c index 058730f6a..577018848 100644 --- a/vunit/vhdl/python/src/python_pkg.c +++ b/vunit/vhdl/python/src/python_pkg.c @@ -47,14 +47,14 @@ void check_conversion_error(const char* expr) { void handle_type_check_error(PyObject* pyobj, const char* context, const char* expr) { - PyObject* type_name = PyType_GetName(Py_TYPE(pyobj)); - if (type_name == NULL) { + 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); - const char* type_name_str = get_string(type_name); - Py_DECREF(type_name); if (type_name_str == NULL) { py_error_handler(context, expr, "Expression evaluates to an unknown type.", true); From 9db2cdd96d8e4fb60b6f84dd941d3f1dd5aae6a2 Mon Sep 17 00:00:00 2001 From: Lars Asplund Date: Sat, 24 Feb 2024 15:08:03 +0100 Subject: [PATCH 8/8] Added support for GHDL. --- examples/vhdl/embedded_python/run.py | 12 +- examples/vhdl/embedded_python/tb_example.vhd | 18 +- vunit/builtins.py | 8 +- vunit/python_pkg.py | 58 ++- vunit/sim_if/ghdl.py | 7 + vunit/sim_if/nvc.py | 2 +- vunit/vhdl/python/run.py | 14 +- vunit/vhdl/python/src/python_context.vhd | 2 +- vunit/vhdl/python/src/python_pkg.vhd | 9 +- vunit/vhdl/python/src/python_pkg_fli.vhd | 2 +- vunit/vhdl/python/src/python_pkg_vhpi.vhd | 2 +- .../python/src/python_pkg_vhpidirect_ghdl.c | 220 +++++++++++ .../python/src/python_pkg_vhpidirect_ghdl.vhd | 118 ++++++ .../python/src/python_pkg_vhpidirect_nvc.c | 5 +- ...rect.vhd => python_pkg_vhpidirect_nvc.vhd} | 16 +- vunit/vhdl/python/test/tb_python_pkg.vhd | 363 +++++++++--------- 16 files changed, 644 insertions(+), 212 deletions(-) create mode 100644 vunit/vhdl/python/src/python_pkg_vhpidirect_ghdl.c create mode 100644 vunit/vhdl/python/src/python_pkg_vhpidirect_ghdl.vhd rename vunit/vhdl/python/src/{python_pkg_vhpidirect.vhd => python_pkg_vhpidirect_nvc.vhd} (86%) diff --git a/examples/vhdl/embedded_python/run.py b/examples/vhdl/embedded_python/run.py index 1f8cb447a..d72094724 100644 --- a/examples/vhdl/embedded_python/run.py +++ b/examples/vhdl/embedded_python/run.py @@ -2,11 +2,16 @@ # 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 +# 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 +from vunit.python_pkg import ( + compile_vhpi_application, + compile_fli_application, + compile_vhpidirect_nvc_application, + compile_vhpidirect_ghdl_application, +) def hello_world(): @@ -65,7 +70,6 @@ def main(): vu.add_vhdl_builtins() vu.add_python() vu.add_random() - vu.enable_location_preprocessing() simulator_name = vu.get_simulator_name() if simulator_name in ["rivierapro", "activehdl"]: @@ -76,6 +80,8 @@ def main(): 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") diff --git a/examples/vhdl/embedded_python/tb_example.vhd b/examples/vhdl/embedded_python/tb_example.vhd index 21bf78721..739b96a1d 100644 --- a/examples/vhdl/embedded_python/tb_example.vhd +++ b/examples/vhdl/embedded_python/tb_example.vhd @@ -2,7 +2,7 @@ -- 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 +-- Copyright (c) 2014-2024, Lars Asplund lars.anders.asplund@gmail.com library vunit_lib; context vunit_lib.vunit_context; @@ -51,8 +51,18 @@ begin exec("from sys import prefix"); exec("from pathlib import Path"); exec("old_environ = environ"); - exec("environ['TCL_LIBRARY'] = str(Path(prefix) / 'tcl' / 'tcl8.6')"); - exec("environ['TK_LIBRARY'] = str(Path(prefix) / 'tcl' / 'tk8.6')"); + 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 @@ -209,7 +219,7 @@ begin 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 diff --git a/vunit/builtins.py b/vunit/builtins.py index 46467a3b0..c825e6d4e 100755 --- a/vunit/builtins.py +++ b/vunit/builtins.py @@ -219,7 +219,7 @@ def _add_python(self): 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"]) + 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)}") @@ -230,8 +230,10 @@ def _add_python(self): 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" in simulator_supported_flis: - self._vunit_lib.add_source_files(VHDL_PATH / "python" / "src" / "python_pkg_vhpidirect.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): """ diff --git a/vunit/python_pkg.py b/vunit/python_pkg.py index 8c3a5f7ad..7ccd3359d 100644 --- a/vunit/python_pkg.py +++ b/vunit/python_pkg.py @@ -2,7 +2,7 @@ # 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 +# Copyright (c) 2014-2024, Lars Asplund lars.anders.asplund@gmail.com """ Temporary helper module to compile C-code used by python_pkg. @@ -212,3 +212,59 @@ def compile_vhpidirect_nvc_application(run_script_root, vu): 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/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/nvc.py b/vunit/sim_if/nvc.py index 4c6cf212d..4ac4173a4 100644 --- a/vunit/sim_if/nvc.py +++ b/vunit/sim_if/nvc.py @@ -149,7 +149,7 @@ def supported_foreign_language_interfaces(cls): """ Returns set of supported foreign interfaces """ - return set(["VHPIDIRECT"]) + return set(["VHPIDIRECT_NVC"]) def setup_library_mapping(self, project): """ diff --git a/vunit/vhdl/python/run.py b/vunit/vhdl/python/run.py index 9ebf86c35..1d6577825 100644 --- a/vunit/vhdl/python/run.py +++ b/vunit/vhdl/python/run.py @@ -2,11 +2,18 @@ # 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 +# 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 +from vunit.python_pkg import ( + compile_vhpi_application, + compile_fli_application, + compile_vhpidirect_nvc_application, + compile_vhpidirect_ghdl_application, +) def remote_test(): @@ -15,7 +22,6 @@ def remote_test(): def main(): root = Path(__file__).parent - vu = VUnit.from_argv() vu.add_vhdl_builtins() vu.add_python() @@ -29,6 +35,8 @@ def main(): 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") diff --git a/vunit/vhdl/python/src/python_context.vhd b/vunit/vhdl/python/src/python_context.vhd index 4e927b792..0b7b79b87 100644 --- a/vunit/vhdl/python/src/python_context.vhd +++ b/vunit/vhdl/python/src/python_context.vhd @@ -4,7 +4,7 @@ -- 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 +-- Copyright (c) 2014-2024, Lars Asplund lars.anders.asplund@gmail.com context python_context is library vunit_lib; diff --git a/vunit/vhdl/python/src/python_pkg.vhd b/vunit/vhdl/python/src/python_pkg.vhd index 34e6316ce..25438d727 100644 --- a/vunit/vhdl/python/src/python_pkg.vhd +++ b/vunit/vhdl/python/src/python_pkg.vhd @@ -4,7 +4,7 @@ -- 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 +-- Copyright (c) 2014-2024, Lars Asplund lars.anders.asplund@gmail.com use work.python_ffi_pkg.all; use work.path.all; @@ -49,11 +49,6 @@ package python_pkg is end package; package body python_pkg is - function ssin (v : real) return real is - begin - assert false severity failure; - end; - -- @formatter:off procedure import_module_from_file(module_path, as_module_name : string) is constant spec_name : string := "__" & as_module_name & "_spec"; @@ -64,7 +59,7 @@ package body python_pkg is 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 & ")"; + spec_name & ".loader.exec_module(" & as_module_name & ")"; begin exec(code); end; diff --git a/vunit/vhdl/python/src/python_pkg_fli.vhd b/vunit/vhdl/python/src/python_pkg_fli.vhd index a2d147701..3c9fc1501 100644 --- a/vunit/vhdl/python/src/python_pkg_fli.vhd +++ b/vunit/vhdl/python/src/python_pkg_fli.vhd @@ -4,7 +4,7 @@ -- 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 +-- Copyright (c) 2014-2024, Lars Asplund lars.anders.asplund@gmail.com use std.textio.all; diff --git a/vunit/vhdl/python/src/python_pkg_vhpi.vhd b/vunit/vhdl/python/src/python_pkg_vhpi.vhd index 43abccb43..a675eb37c 100644 --- a/vunit/vhdl/python/src/python_pkg_vhpi.vhd +++ b/vunit/vhdl/python/src/python_pkg_vhpi.vhd @@ -4,7 +4,7 @@ -- 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 +-- Copyright (c) 2014-2024, Lars Asplund lars.anders.asplund@gmail.com use std.textio.all; 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 index e1fd85694..45d403901 100644 --- a/vunit/vhdl/python/src/python_pkg_vhpidirect_nvc.c +++ b/vunit/vhdl/python/src/python_pkg_vhpidirect_nvc.c @@ -6,10 +6,6 @@ // // Copyright (c) 2014-2023, Lars Asplund lars.anders.asplund@gmail.com -#include -#include -#include - #include "python_pkg.h" PyObject* globals = NULL; @@ -99,6 +95,7 @@ class __EvalResult__():\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); } diff --git a/vunit/vhdl/python/src/python_pkg_vhpidirect.vhd b/vunit/vhdl/python/src/python_pkg_vhpidirect_nvc.vhd similarity index 86% rename from vunit/vhdl/python/src/python_pkg_vhpidirect.vhd rename to vunit/vhdl/python/src/python_pkg_vhpidirect_nvc.vhd index 5c695f4c2..609da8dcd 100644 --- a/vunit/vhdl/python/src/python_pkg_vhpidirect.vhd +++ b/vunit/vhdl/python/src/python_pkg_vhpidirect_nvc.vhd @@ -4,7 +4,7 @@ -- 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 +-- Copyright (c) 2014-2024, Lars Asplund lars.anders.asplund@gmail.com use std.textio.all; @@ -31,8 +31,19 @@ package python_ffi_pkg is 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 @@ -57,7 +68,6 @@ package body python_ffi_pkg is end; procedure get_integer_vector(vec : out integer_vector) is - attribute foreign of get_integer_vector : procedure is "VHPIDIRECT get_integer_vector"; begin report "ERROR: Failed to call foreign subprogram" severity failure; end; @@ -72,7 +82,6 @@ package body python_ffi_pkg is end; procedure get_real_vector(vec : out real_vector) is - attribute foreign of get_real_vector : procedure is "VHPIDIRECT get_real_vector"; begin report "ERROR: Failed to call foreign subprogram" severity failure; end; @@ -87,7 +96,6 @@ package body python_ffi_pkg is end; procedure get_py_string(vec : out string) is - attribute foreign of get_py_string : procedure is "VHPIDIRECT get_py_string"; begin report "ERROR: Failed to call foreign subprogram" severity failure; end; diff --git a/vunit/vhdl/python/test/tb_python_pkg.vhd b/vunit/vhdl/python/test/tb_python_pkg.vhd index be3789fda..07f60916b 100644 --- a/vunit/vhdl/python/test/tb_python_pkg.vhd +++ b/vunit/vhdl/python/test/tb_python_pkg.vhd @@ -2,7 +2,7 @@ -- 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 +-- Copyright (c) 2014-2024, Lars Asplund lars.anders.asplund@gmail.com library vunit_lib; context vunit_lib.vunit_context; @@ -25,6 +25,8 @@ begin 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; @@ -46,184 +48,187 @@ begin --------------------------------------------------------------------- -- Test eval of different types --------------------------------------------------------------------- - if run("Test eval of integer expression") then - check_equal(eval("2**31 - 1"), 2 ** 31 - 1); - check_equal(eval("-2**31"), -2 ** 31); - - 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'(-2 ** 31, -1, 0, 1, 2 ** 31 - 1))) = integer_vector'(-2 ** 31, -1, 0, 1, 2 ** 31 - 1)); - - 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'(-2 ** 31, -1, 0, 1, 2 ** 31 - 1))); - check_equal(get(vhdl_integer_vector_ptr, 0), -2 ** 31); - 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), 2 ** 31 - 1); - - 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')"); + 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;