diff --git a/docs/hdl_libraries.rst b/docs/hdl_libraries.rst index a613755f6..00eb78ac8 100644 --- a/docs/hdl_libraries.rst +++ b/docs/hdl_libraries.rst @@ -61,6 +61,44 @@ and OSVVM. See :meth:`add_random() `. +Reflection +---------- + +These packages use VHDL-2019 reflection to work on values of any type, so no code has to be written or +generated for each type. Add them with :meth:`add_reflection() `. They +require VHDL-2019 and a simulator with reflection, currently NVC and Riviera-PRO. + +Record Dictionaries +~~~~~~~~~~~~~~~~~~~ + +``record_dict_pkg`` converts a value of any record type to a :ref:`dict_t ` with +VHDL-2019 reflection, so no conversion function has to be written or generated for each record type. A +``dict_t`` can be printed, compared, queued or passed to code that takes key-value data, such as the +``dict_t`` arguments of the VUnit Python bridge. + +``to_dict`` is a generic function, instantiated once for each record type and then called with the record: + +.. code-block:: vhdl + + library vunit_lib; + use vunit_lib.record_dict_pkg.all; + ... + function to_dict is new to_dict generic map (record_t => packet_t); + ... + dict := to_dict(packet); + check_equal(get_integer(dict, "addr"), 5); + +Instances for different record types can share the name ``to_dict``. The instantiation is needed since +VHDL has no implicit instantiation of generic subprograms, and a function can't take ``packet'reflect`` +directly: a mirror is an access type, which a parameter can only take as a variable. ``to_dict`` +reflects its own parameter instead. + +Element names become lower case keys. ``integer``, ``real``, ``boolean``, ``bit``, ``std_ulogic``, +``character``, ``time``, ``string``, ``std_ulogic_vector``, ``unsigned`` and ``signed`` elements keep their +type, other enumerations are stored as the name of their value, and a nested record becomes a nested +``dict_t``. Any other element type fails on ``record_dict_logger`` with the name of the element. The +conversion is one-way, since reflection cannot create a record value. + OSVVM ----- diff --git a/docs/news.d/1239.feature.rst b/docs/news.d/1239.feature.rst new file mode 100644 index 000000000..7ede9be8c --- /dev/null +++ b/docs/news.d/1239.feature.rst @@ -0,0 +1,3 @@ +Added ``record_dict_pkg``, which converts a value of any record type to a ``dict_t`` with VHDL-2019 reflection. It is +opt-in through :meth:`add_reflection() ` and requires a simulator with VHDL-2019 +reflection, currently NVC and Riviera-PRO. diff --git a/tests/acceptance/test_external_run_scripts.py b/tests/acceptance/test_external_run_scripts.py index 36fb5b352..b8d566a02 100644 --- a/tests/acceptance/test_external_run_scripts.py +++ b/tests/acceptance/test_external_run_scripts.py @@ -265,6 +265,10 @@ def test_data_types_vhdl_93(self): def test_random_vhdl_2008(self): self.check(VHDL_PATH / "random/run.py") + @mark.skipif(not simulator_is("nvc", "rivierapro"), reason="Requires VHDL-2019 reflection") + def test_reflection_vhdl_2019(self): + self.check(VHDL_PATH / "reflection/run.py", vhdl_standard="2019") + def test_check_vhdl_2008(self): self.check(VHDL_PATH / "check/run.py") diff --git a/tests/unit/test_builtins.py b/tests/unit/test_builtins.py index d265a6b53..92bccb82a 100644 --- a/tests/unit/test_builtins.py +++ b/tests/unit/test_builtins.py @@ -15,7 +15,7 @@ from pathlib import Path from unittest import mock from vunit import VUnit -from vunit.builtins import Builtins, BuiltinsAdder +from vunit.builtins import Builtins, BuiltinsAdder, VHDL_PATH from vunit.about import version from vunit.sim_if.activehdl import ActiveHDLVersion from vunit.sim_if.ghdl import GHDLVersion @@ -83,6 +83,32 @@ def _assertLogContent(self, log, level, msg): def _write_toml(self, tempdir, text: str): (tempdir / "vunit_pkg.toml").write_text(text, encoding="utf-8") + def test_reflection_requires_vhdl_2019(self): + simulator_class = mock.Mock() + simulator_class.supports_vhdl_reflection.return_value = True + builtins = Builtins(self.vu, VHDL.STD_2008, simulator_class) + with self.assertRaisesRegex( + RuntimeError, re.escape("The reflection packages only support VHDL-2019 and later") + ): + builtins.add("reflection") + + def test_reflection_requires_reflection(self): + simulator_class = mock.Mock() + simulator_class.name = "ghdl" + simulator_class.supports_vhdl_reflection.return_value = False + builtins = Builtins(self.vu, VHDL.STD_2019, simulator_class) + with self.assertRaisesRegex( + RuntimeError, re.escape("The reflection packages require VHDL-2019 reflection, which ghdl does not support") + ): + builtins.add("reflection") + + def test_reflection_adds_its_sources(self): + simulator_class = mock.Mock() + simulator_class.supports_vhdl_reflection.return_value = True + builtins = Builtins(self.vu, VHDL.STD_2019, simulator_class) + builtins.add("reflection") + self.library_mock.add_source_files.assert_called_with(VHDL_PATH / "reflection" / "src" / "*.vhd") + def test_raises_if_package_not_found(self): with ( mock.patch("vunit.builtins.importlib.util.find_spec", return_value=None), diff --git a/vunit/builtins.py b/vunit/builtins.py index 8674bed4c..42b2ae8c4 100755 --- a/vunit/builtins.py +++ b/vunit/builtins.py @@ -181,6 +181,7 @@ def add(name, deps=tuple()): add("verification_components", ["com", "osvvm"]) add("osvvm") add("random", ["osvvm"]) + add("reflection") def add(self, name, args=None): self._builtins_adder.add(name, args) @@ -753,6 +754,21 @@ def _add_random(self): self._vunit_lib.add_source_files(VHDL_PATH / "random" / "src" / "*.vhd") + def _add_reflection(self): + """ + Add the reflection packages + """ + if not self._vhdl_standard >= VHDL.STD_2019: + raise RuntimeError("The reflection packages only support VHDL-2019 and later") + + if not self._simulator_class.supports_vhdl_reflection(): + raise RuntimeError( + "The reflection packages require VHDL-2019 reflection, " + f"which {self._simulator_class.name} does not support" + ) + + self._vunit_lib.add_source_files(VHDL_PATH / "reflection" / "src" / "*.vhd") + def _add_com(self): """ Add com library diff --git a/vunit/sim_if/__init__.py b/vunit/sim_if/__init__.py index 53b2ea2a1..865c7ca0c 100644 --- a/vunit/sim_if/__init__.py +++ b/vunit/sim_if/__init__.py @@ -108,6 +108,13 @@ def supports_vhdl_call_paths(cls): """ return False + @classmethod + def supports_vhdl_reflection(cls): + """ + Returns True when this simulator supports VHDL-2019 reflection (std.reflection) + """ + return False + @staticmethod def find_executable(executable): """ diff --git a/vunit/sim_if/nvc.py b/vunit/sim_if/nvc.py index b1d742b72..afdc4f359 100644 --- a/vunit/sim_if/nvc.py +++ b/vunit/sim_if/nvc.py @@ -145,6 +145,13 @@ def supports_vhdl_call_paths(cls): """ return True + @classmethod + def supports_vhdl_reflection(cls): + """ + Returns True when this simulator supports VHDL-2019 reflection (std.reflection) + """ + return True + @classmethod def supports_vhdl_package_generics(cls): """ diff --git a/vunit/sim_if/rivierapro.py b/vunit/sim_if/rivierapro.py index b70b89150..afabc9394 100644 --- a/vunit/sim_if/rivierapro.py +++ b/vunit/sim_if/rivierapro.py @@ -108,6 +108,13 @@ def supports_vhdl_call_paths(cls): """ return True + @classmethod + def supports_vhdl_reflection(cls): + """ + Returns True when this simulator supports VHDL-2019 reflection (std.reflection) + """ + return True + @classmethod def supports_vhdl_package_generics(cls): """ diff --git a/vunit/ui/__init__.py b/vunit/ui/__init__.py index 17503a6c8..c970121cb 100644 --- a/vunit/ui/__init__.py +++ b/vunit/ui/__init__.py @@ -1335,6 +1335,15 @@ def add_random(self): """ self._builtins.add("random") + def add_reflection(self): + """ + Add the packages built on VHDL-2019 reflection, such as record_dict_pkg, which converts any record to a + dict_t. + + Requires VHDL-2019 and a simulator that supports reflection (NVC or Riviera-PRO). + """ + self._builtins.add("reflection") + def add_verification_components(self): """ Add verification component library diff --git a/vunit/vhdl/reflection/run.py b/vunit/vhdl/reflection/run.py new file mode 100644 index 000000000..b5ff31cac --- /dev/null +++ b/vunit/vhdl/reflection/run.py @@ -0,0 +1,20 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. +# +# Copyright (c) 2014-2026, Lars Asplund lars.anders.asplund@gmail.com + +from pathlib import Path +from vunit import VUnit + +ROOT = Path(__file__).parent + +UI = VUnit.from_argv() +UI.add_vhdl_builtins() +UI.add_reflection() + +UI.library("vunit_lib").add_source_files(ROOT / "test" / "*.vhd") +# Riviera-PRO needs debug information for the log locations the tests check +UI.set_compile_option("rivierapro.vcom_flags", ["-dbg"]) + +UI.main() diff --git a/vunit/vhdl/reflection/src/record_dict_pkg.vhd b/vunit/vhdl/reflection/src/record_dict_pkg.vhd new file mode 100644 index 000000000..b38aac67c --- /dev/null +++ b/vunit/vhdl/reflection/src/record_dict_pkg.vhd @@ -0,0 +1,181 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this file, +-- You can obtain one at http://mozilla.org/MPL/2.0/. +-- +-- Copyright (c) 2014-2026, Lars Asplund lars.anders.asplund@gmail.com +-- +-- record_dict_pkg converts a value of any record type to a dict_t, using VHDL-2019 +-- reflection. Element names become lower case keys, a nested record becomes a nested +-- dict_t. + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; +use std.reflection.all; + +use work.dict_pkg.all; +use work.logger_pkg.all; +use work.string_ops.lower; +use work.string_ops.upper; + +package record_dict_pkg is + constant record_dict_logger : logger_t := get_logger("vunit_lib:record_dict_pkg"); + + -- Instantiate once per record type, then call with the record: + -- + -- function to_dict is new to_dict generic map (record_t => packet_t); + -- ... + -- dict := to_dict(packet); + -- + -- The generic type is needed since a mirror is an access type, which a parameter can only take as a + -- variable, so my_record'reflect can't be passed to a subprogram. The function reflects its own + -- parameter instead. + impure function to_dict generic (type record_t) parameter (value : record_t) return dict_t; +end package; + +package body record_dict_pkg is + impure function mirror_to_dict(variable value : inout value_mirror) return dict_t; + + -- Simulators differ in the case of simple_name. Riviera-PRO rejects chained mirror calls, so these + -- helpers go through a variable. + impure function get_type_name(variable value : inout value_mirror) return string is + variable subtype_mirror : subtype_mirror := value.get_subtype_mirror; + begin + return upper(subtype_mirror.simple_name); + end; + + impure function get_pos(variable value : inout value_mirror) return integer is + variable enumeration : enumeration_value_mirror := value.to_enumeration; + begin + return enumeration.pos; + end; + + -- Element offset positions from the left. NVC indexes from 'left upwards whatever the direction + impure function get_element(variable value : inout array_value_mirror; offset : natural) return value_mirror is + variable subtype_mirror : array_subtype_mirror := value.get_subtype_mirror; + begin +`if TOOL_NAME = "nvc" then + return value.get(subtype_mirror.left + index(offset)); +`else + if subtype_mirror.ascending then + return value.get(subtype_mirror.left + index(offset)); + end if; + return value.get(subtype_mirror.left - index(offset)); +`end if + end; + + impure function to_std_ulogic_vector(variable value : inout array_value_mirror) return std_ulogic_vector is + variable subtype_mirror : array_subtype_mirror := value.get_subtype_mirror; + variable element : value_mirror; + variable result : std_ulogic_vector(0 to integer(subtype_mirror.length) - 1); + begin + for idx in result'range loop + element := get_element(value, idx); + result(idx) := std_ulogic'val(get_pos(element)); + end loop; + return result; + end; + + impure function to_string(variable value : inout array_value_mirror) return string is + variable subtype_mirror : array_subtype_mirror := value.get_subtype_mirror; + variable element : value_mirror; + variable result : string(1 to integer(subtype_mirror.length)); + begin + for idx in result'range loop + element := get_element(value, idx - 1); + result(idx) := character'val(get_pos(element)); + end loop; + return result; + end; + + procedure set(dict : dict_t; key : string; variable value : inout value_mirror) is + constant type_name : string := get_type_name(value); + variable integer_value : integer_value_mirror; + variable floating_value : floating_value_mirror; + variable physical_value : physical_value_mirror; + variable enumeration : enumeration_value_mirror; + variable array_value : array_value_mirror; + variable nested : dict_t; + begin + case value.get_value_class is + when class_integer => + integer_value := value.to_integer; + set_integer(dict, key, integer_value.value); + + when class_floating => + floating_value := value.to_floating; + set_real(dict, key, floating_value.value); + + when class_physical => + if type_name = "TIME" then + -- The value is in units of the resolution limit on Riviera-PRO, fs on NVC + physical_value := value.to_physical; + set_time(dict, key, physical_value.value * std.env.resolution_limit); + else + failure(record_dict_logger, key & ": physical type " & type_name & " is not supported."); + end if; + + when class_enumeration => + enumeration := value.to_enumeration; + if type_name = "BOOLEAN" then + set_boolean(dict, key, boolean'val(enumeration.pos)); + elsif type_name = "BIT" then + set_bit(dict, key, bit'val(enumeration.pos)); + elsif type_name = "STD_ULOGIC" or type_name = "STD_LOGIC" then + set_std_ulogic(dict, key, std_ulogic'val(enumeration.pos)); + elsif type_name = "CHARACTER" then + set_character(dict, key, character'val(enumeration.pos)); + else + set_string(dict, key, enumeration.image); + end if; + + when class_array => + array_value := value.to_array; + if type_name = "STRING" then + set_string(dict, key, to_string(array_value)); + elsif type_name = "UNSIGNED" or type_name = "UNRESOLVED_UNSIGNED" then + set_numeric_std_unsigned(dict, key, unsigned(to_std_ulogic_vector(array_value))); + elsif type_name = "SIGNED" or type_name = "UNRESOLVED_SIGNED" then + set_numeric_std_signed(dict, key, signed(to_std_ulogic_vector(array_value))); + elsif type_name = "STD_ULOGIC_VECTOR" or type_name = "STD_LOGIC_VECTOR" then + set_std_ulogic_vector(dict, key, to_std_ulogic_vector(array_value)); + else + failure(record_dict_logger, key & ": array type " & type_name & " is not supported."); + end if; + + when class_record => + -- NVC rejects mirror_to_dict(value) as an actual of set_dict_t_ref + nested := mirror_to_dict(value); + set_dict_t_ref(dict, key, nested); + + when others => + failure(record_dict_logger, key & ": type " & type_name & " is not supported."); + end case; + end; + + impure function mirror_to_dict(variable value : inout value_mirror) return dict_t is + variable record_value : record_value_mirror; + variable subtype_mirror : record_subtype_mirror; + variable element : value_mirror; + constant dict : dict_t := new_dict; + begin + if value.get_value_class /= class_record then + failure(record_dict_logger, "to_dict expects a record, got " & get_type_name(value) & "."); + return dict; + end if; + + record_value := value.to_record; + subtype_mirror := record_value.get_subtype_mirror; + for idx in 0 to subtype_mirror.length - 1 loop + element := record_value.get(idx); + set(dict, lower(subtype_mirror.element_name(idx)), element); + end loop; + return dict; + end; + + impure function to_dict generic (type record_t) parameter (value : record_t) return dict_t is + variable mirror : value_mirror := value'reflect; + begin + return mirror_to_dict(mirror); + end; +end package body; diff --git a/vunit/vhdl/reflection/test/tb_record_dict_pkg.vhd b/vunit/vhdl/reflection/test/tb_record_dict_pkg.vhd new file mode 100644 index 000000000..6b381996f --- /dev/null +++ b/vunit/vhdl/reflection/test/tb_record_dict_pkg.vhd @@ -0,0 +1,127 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this file, +-- You can obtain one at http://mozilla.org/MPL/2.0/. +-- +-- Copyright (c) 2014-2026, Lars Asplund lars.anders.asplund@gmail.com + +library vunit_lib; +context vunit_lib.vunit_context; +use vunit_lib.record_dict_pkg.all; + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; + +entity tb_record_dict_pkg is + generic ( + runner_cfg : string); +end entity; + +architecture a of tb_record_dict_pkg is + type state_t is (idle, busy); + + type scalars_t is record + int : integer; + float : real; + flag : boolean; + b : bit; + sl : std_logic; + char : character; + delay : time; + state : state_t; + end record; + + type arrays_t is record + name : string(1 to 3); + data : std_ulogic_vector(7 downto 0); + ascending : std_logic_vector(0 to 3); + count : unsigned(3 downto 0); + offset : signed(3 downto 0); + end record; + + type inner_t is record + level : natural; + end record; + + type outer_t is record + inner : inner_t; + MixedCase : integer; + end record; + + type unsupported_t is record + values : integer_vector(0 to 1); + end record; + + function to_dict is new to_dict generic map (record_t => scalars_t); + function to_dict is new to_dict generic map (record_t => arrays_t); + function to_dict is new to_dict generic map (record_t => outer_t); + function to_dict is new to_dict generic map (record_t => unsupported_t); + function to_dict is new to_dict generic map (record_t => integer); +begin + + main : process + variable scalars : scalars_t := ( + int => -7, float => 2.5, flag => true, b => '1', sl => 'Z', char => 'x', delay => 3 ns, state => busy); + variable arrays : arrays_t := ( + name => "abc", data => x"a5", ascending => "0011", count => x"9", offset => "1110"); + variable outer : outer_t := (inner => (level => 4), MixedCase => 1); + variable unsupported : unsupported_t := (values => (1, 2)); + variable int : integer := 17; + variable dict, inner : dict_t; + begin + test_runner_setup(runner, runner_cfg); + + while test_suite loop + if run("Test scalar elements") then + dict := to_dict(scalars); + check_equal(num_keys(dict), 8); + check_equal(get_integer(dict, "int"), -7); + check_equal(get_real(dict, "float"), 2.5); + check_true(get_boolean(dict, "flag")); + check(get_bit(dict, "b") = '1'); + check_equal(get_std_ulogic(dict, "sl"), 'Z'); + check(get_character(dict, "char") = 'x'); + check_equal(get_time(dict, "delay"), 3 ns); + check_equal(get_string(dict, "state"), "busy"); + + elsif run("Test array elements") then + dict := to_dict(arrays); + check_equal(get_string(dict, "name"), "abc"); + check_equal(get_std_ulogic_vector(dict, "data"), std_ulogic_vector'(x"a5")); + check_equal(get_std_ulogic_vector(dict, "ascending"), std_ulogic_vector'("0011")); + check_equal(get_numeric_std_unsigned(dict, "count"), 9); + check_equal(get_numeric_std_signed(dict, "offset"), -2); + + elsif run("Test a nested record becomes a nested dict") then + dict := to_dict(outer); + inner := get_dict_t_ref(dict, "inner"); + check_equal(get_integer(inner, "level"), 4); + + elsif run("Test keys are lower case") then + dict := to_dict(outer); + check_equal(get_integer(dict, "mixedcase"), 1); + + elsif run("Test an unsupported element fails with its name") then + mock(record_dict_logger, failure); + dict := to_dict(unsupported); + -- VHDL-2019 call paths add the location of the failure to the log item + check_only_log( + record_dict_logger, "values: array type INTEGER_VECTOR is not supported.", failure, + line_num => 143, file_name => "record_dict_pkg.vhd" + ); + unmock(record_dict_logger); + + elsif run("Test a value that is not a record fails") then + mock(record_dict_logger, failure); + dict := to_dict(int); + check_only_log( + record_dict_logger, "to_dict expects a record, got INTEGER.", failure, + line_num => 163, file_name => "record_dict_pkg.vhd" + ); + unmock(record_dict_logger); + end if; + end loop; + + test_runner_cleanup(runner); + end process; +end architecture;