From 1cf1d2142c672cb0ecd65ddde251c4a2d1250759 Mon Sep 17 00:00:00 2001 From: Sebastian Hellgren Date: Sat, 17 Oct 2026 18:01:08 +0200 Subject: [PATCH 1/6] Add record_dict_pkg for converting any record to a dict_t record_dict_pkg uses VHDL-2019 reflection to convert a value of any record type to a dict_t, so no conversion function has to be written or generated for each record. It is opt-in through add_record_dict(), which requires VHDL-2019 and a simulator whose supports_vhdl_reflection() is true, currently NVC. Co-Authored-By: Claude Opus 5.5 --- docs/hdl_libraries.rst | 30 ++++ tests/acceptance/test_external_run_scripts.py | 4 + tests/unit/test_builtins.py | 26 +++- vunit/builtins.py | 15 ++ vunit/sim_if/__init__.py | 7 + vunit/sim_if/nvc.py | 7 + vunit/ui/__init__.py | 8 ++ vunit/vhdl/record_dict/run.py | 18 +++ .../vhdl/record_dict/src/record_dict_pkg.vhd | 132 ++++++++++++++++++ .../record_dict/test/tb_record_dict_pkg.vhd | 130 +++++++++++++++++ 10 files changed, 376 insertions(+), 1 deletion(-) create mode 100644 vunit/vhdl/record_dict/run.py create mode 100644 vunit/vhdl/record_dict/src/record_dict_pkg.vhd create mode 100644 vunit/vhdl/record_dict/test/tb_record_dict_pkg.vhd diff --git a/docs/hdl_libraries.rst b/docs/hdl_libraries.rst index a613755f6..983b2a19c 100644 --- a/docs/hdl_libraries.rst +++ b/docs/hdl_libraries.rst @@ -61,6 +61,36 @@ and OSVVM. See :meth:`add_random() `. +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. + +.. code-block:: vhdl + + library vunit_lib; + use vunit_lib.record_dict_pkg.all; + use std.reflection.all; + ... + variable mirror : value_mirror; + variable dict : dict_t; + ... + mirror := packet'reflect; -- mirrors are access types, so this must be a variable + dict := to_dict(mirror); + check_equal(get_integer(dict, "addr"), 5); + +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. + +See :meth:`add_record_dict() `. It requires VHDL-2019 and a simulator with +reflection, currently NVC. + OSVVM ----- diff --git a/tests/acceptance/test_external_run_scripts.py b/tests/acceptance/test_external_run_scripts.py index 36fb5b352..be71be129 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"), reason="Requires VHDL-2019 reflection") + def test_record_dict_vhdl_2019(self): + self.check(VHDL_PATH / "record_dict/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..e00709032 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,30 @@ 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_record_dict_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("record_dict_pkg only supports VHDL-2019 and later")): + builtins.add("record_dict") + + def test_record_dict_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("record_dict_pkg requires VHDL-2019 reflection, which ghdl does not support") + ): + builtins.add("record_dict") + + def test_record_dict_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("record_dict") + self.library_mock.add_source_files.assert_called_with(VHDL_PATH / "record_dict" / "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..8d82c3976 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("record_dict") def add(self, name, args=None): self._builtins_adder.add(name, args) @@ -753,6 +754,20 @@ def _add_random(self): self._vunit_lib.add_source_files(VHDL_PATH / "random" / "src" / "*.vhd") + def _add_record_dict(self): + """ + Add record_dict_pkg + """ + if not self._vhdl_standard >= VHDL.STD_2019: + raise RuntimeError("record_dict_pkg only supports VHDL-2019 and later") + + if not self._simulator_class.supports_vhdl_reflection(): + raise RuntimeError( + f"record_dict_pkg requires VHDL-2019 reflection, which {self._simulator_class.name} does not support" + ) + + self._vunit_lib.add_source_files(VHDL_PATH / "record_dict" / "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/ui/__init__.py b/vunit/ui/__init__.py index 17503a6c8..53d370d4d 100644 --- a/vunit/ui/__init__.py +++ b/vunit/ui/__init__.py @@ -1335,6 +1335,14 @@ def add_random(self): """ self._builtins.add("random") + def add_record_dict(self): + """ + Add record_dict_pkg, which converts any record to a dict_t with VHDL-2019 reflection. + + Requires VHDL-2019 and a simulator that supports reflection (NVC). + """ + self._builtins.add("record_dict") + def add_verification_components(self): """ Add verification component library diff --git a/vunit/vhdl/record_dict/run.py b/vunit/vhdl/record_dict/run.py new file mode 100644 index 000000000..6c618fc9a --- /dev/null +++ b/vunit/vhdl/record_dict/run.py @@ -0,0 +1,18 @@ +# 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_record_dict() + +UI.library("vunit_lib").add_source_files(ROOT / "test" / "*.vhd") + +UI.main() diff --git a/vunit/vhdl/record_dict/src/record_dict_pkg.vhd b/vunit/vhdl/record_dict/src/record_dict_pkg.vhd new file mode 100644 index 000000000..5dbb213e8 --- /dev/null +++ b/vunit/vhdl/record_dict/src/record_dict_pkg.vhd @@ -0,0 +1,132 @@ +-- 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; + +package record_dict_pkg is + constant record_dict_logger : logger_t := get_logger("vunit_lib:record_dict_pkg"); + + -- Mirrors are access types, so the mirror of the record must be a variable: + -- + -- mirror := my_record'reflect; + -- dict := to_dict(mirror); + impure function to_dict(variable value : inout value_mirror) return dict_t; +end package; + +package body record_dict_pkg is + 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 := value.get(subtype_mirror.left + index(idx)); + result(idx) := std_ulogic'val(element.to_enumeration.pos); + 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 := value.get(subtype_mirror.left + index(idx - 1)); + result(idx) := character'val(element.to_enumeration.pos); + end loop; + return result; + end; + + procedure set(dict : dict_t; key : string; variable value : inout value_mirror) is + constant type_name : string := value.get_subtype_mirror.simple_name; + 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 => + set_integer(dict, key, value.to_integer.value); + + when class_floating => + set_real(dict, key, value.to_floating.value); + + when class_physical => + if type_name = "TIME" then + set_time(dict, key, value.to_physical.value * 1 fs); + 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 to_dict(value) as an actual of set_dict_t_ref + nested := 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 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 " & value.get_subtype_mirror.simple_name & "."); + 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; +end package body; diff --git a/vunit/vhdl/record_dict/test/tb_record_dict_pkg.vhd b/vunit/vhdl/record_dict/test/tb_record_dict_pkg.vhd new file mode 100644 index 000000000..e7ef7de66 --- /dev/null +++ b/vunit/vhdl/record_dict/test/tb_record_dict_pkg.vhd @@ -0,0 +1,130 @@ +-- 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; + +use std.reflection.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; +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 mirror : value_mirror; + variable dict, inner : dict_t; + begin + test_runner_setup(runner, runner_cfg); + + while test_suite loop + if run("Test scalar elements") then + mirror := scalars'reflect; + dict := to_dict(mirror); + 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 + mirror := arrays'reflect; + dict := to_dict(mirror); + 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 + mirror := outer'reflect; + dict := to_dict(mirror); + inner := get_dict_t_ref(dict, "inner"); + check_equal(get_integer(inner, "level"), 4); + + elsif run("Test keys are lower case") then + mirror := outer'reflect; + dict := to_dict(mirror); + check_equal(get_integer(dict, "mixedcase"), 1); + + elsif run("Test an unsupported element fails with its name") then + mock(record_dict_logger, failure); + mirror := unsupported'reflect; + dict := to_dict(mirror); + -- 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 => 100, 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); + mirror := int'reflect; + dict := to_dict(mirror); + check_only_log( + record_dict_logger, "to_dict expects a record, got INTEGER.", failure, + line_num => 120, file_name => "record_dict_pkg.vhd" + ); + unmock(record_dict_logger); + end if; + end loop; + + test_runner_cleanup(runner); + end process; +end architecture; From 7b673973a13999a4d6e79ae58ee4291e611271aa Mon Sep 17 00:00:00 2001 From: Sebastian Hellgren Date: Sat, 17 Oct 2026 18:01:50 +0200 Subject: [PATCH 2/6] Add a news fragment Co-Authored-By: Claude Opus 5.5 --- docs/news.d/1239.feature.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 docs/news.d/1239.feature.rst diff --git a/docs/news.d/1239.feature.rst b/docs/news.d/1239.feature.rst new file mode 100644 index 000000000..f10fc8a3b --- /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_record_dict() ` and requires a simulator with VHDL-2019 +reflection, currently NVC. From a920011d87b8df55f435eea258481549459a6263 Mon Sep 17 00:00:00 2001 From: Sebastian Hellgren Date: Sat, 17 Oct 2026 18:13:14 +0200 Subject: [PATCH 3/6] Take the record itself in to_dict to_dict is now a generic function, instantiated once per record type and called with the record: function to_dict is new to_dict generic map (record_t => packet_t); dict := to_dict(packet); A function can't take packet'reflect directly. A mirror is an access type, and an access type parameter must be of class variable, whose actual must be a variable name, not an expression. Before, every call needed a mirror variable and std.reflection. Now the function reflects its own parameter, so each call is one expression and users never see mirrors. The instantiation remains since VHDL has no implicit instantiation of generic subprograms. Co-Authored-By: Claude Opus 5.5 --- docs/hdl_libraries.rst | 14 ++++++--- .../vhdl/record_dict/src/record_dict_pkg.vhd | 27 +++++++++++----- .../record_dict/test/tb_record_dict_pkg.vhd | 31 +++++++++---------- 3 files changed, 43 insertions(+), 29 deletions(-) diff --git a/docs/hdl_libraries.rst b/docs/hdl_libraries.rst index 983b2a19c..d6e977592 100644 --- a/docs/hdl_libraries.rst +++ b/docs/hdl_libraries.rst @@ -69,19 +69,23 @@ VHDL-2019 reflection, so no conversion function has to be written or generated f ``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; - use std.reflection.all; ... - variable mirror : value_mirror; - variable dict : dict_t; + function to_dict is new to_dict generic map (record_t => packet_t); ... - mirror := packet'reflect; -- mirrors are access types, so this must be a variable - dict := to_dict(mirror); + 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 diff --git a/vunit/vhdl/record_dict/src/record_dict_pkg.vhd b/vunit/vhdl/record_dict/src/record_dict_pkg.vhd index 5dbb213e8..e97ba229a 100644 --- a/vunit/vhdl/record_dict/src/record_dict_pkg.vhd +++ b/vunit/vhdl/record_dict/src/record_dict_pkg.vhd @@ -20,14 +20,21 @@ use work.string_ops.lower; package record_dict_pkg is constant record_dict_logger : logger_t := get_logger("vunit_lib:record_dict_pkg"); - -- Mirrors are access types, so the mirror of the record must be a variable: + -- Instantiate once per record type, then call with the record: -- - -- mirror := my_record'reflect; - -- dict := to_dict(mirror); - impure function to_dict(variable value : inout value_mirror) return dict_t; + -- 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; + 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; @@ -101,8 +108,8 @@ package body record_dict_pkg is end if; when class_record => - -- NVC rejects to_dict(value) as an actual of set_dict_t_ref - nested := to_dict(value); + -- 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 => @@ -110,7 +117,7 @@ package body record_dict_pkg is end case; end; - impure function to_dict(variable value : inout value_mirror) return dict_t is + 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; @@ -129,4 +136,10 @@ package body record_dict_pkg is 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/record_dict/test/tb_record_dict_pkg.vhd b/vunit/vhdl/record_dict/test/tb_record_dict_pkg.vhd index e7ef7de66..9dfde06c4 100644 --- a/vunit/vhdl/record_dict/test/tb_record_dict_pkg.vhd +++ b/vunit/vhdl/record_dict/test/tb_record_dict_pkg.vhd @@ -12,8 +12,6 @@ library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all; -use std.reflection.all; - entity tb_record_dict_pkg is generic ( runner_cfg : string); @@ -53,6 +51,12 @@ architecture a of tb_record_dict_pkg is 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 @@ -63,15 +67,13 @@ begin variable outer : outer_t := (inner => (level => 4), MixedCase => 1); variable unsupported : unsupported_t := (values => (1, 2)); variable int : integer := 17; - variable mirror : value_mirror; variable dict, inner : dict_t; begin test_runner_setup(runner, runner_cfg); while test_suite loop if run("Test scalar elements") then - mirror := scalars'reflect; - dict := to_dict(mirror); + 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); @@ -83,8 +85,7 @@ begin check_equal(get_string(dict, "state"), "busy"); elsif run("Test array elements") then - mirror := arrays'reflect; - dict := to_dict(mirror); + 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")); @@ -92,34 +93,30 @@ begin check_equal(get_numeric_std_signed(dict, "offset"), -2); elsif run("Test a nested record becomes a nested dict") then - mirror := outer'reflect; - dict := to_dict(mirror); + 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 - mirror := outer'reflect; - dict := to_dict(mirror); + 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); - mirror := unsupported'reflect; - dict := to_dict(mirror); + 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 => 100, file_name => "record_dict_pkg.vhd" + line_num => 107, 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); - mirror := int'reflect; - dict := to_dict(mirror); + dict := to_dict(int); check_only_log( record_dict_logger, "to_dict expects a record, got INTEGER.", failure, - line_num => 120, file_name => "record_dict_pkg.vhd" + line_num => 127, file_name => "record_dict_pkg.vhd" ); unmock(record_dict_logger); end if; From 22b652c9eda7df7bc650b19f1508f94efcc15102 Mon Sep 17 00:00:00 2001 From: Sebastian Hellgren Date: Sat, 17 Oct 2026 19:17:53 +0200 Subject: [PATCH 4/6] Enable record_dict_pkg on Riviera-PRO Riviera-PRO supports VHDL-2019 reflection, so it reports supports_vhdl_reflection() and runs the acceptance test. Co-Authored-By: Claude Opus 5.5 --- docs/hdl_libraries.rst | 2 +- docs/news.d/1239.feature.rst | 2 +- tests/acceptance/test_external_run_scripts.py | 2 +- vunit/sim_if/rivierapro.py | 7 +++++++ vunit/ui/__init__.py | 2 +- 5 files changed, 11 insertions(+), 4 deletions(-) diff --git a/docs/hdl_libraries.rst b/docs/hdl_libraries.rst index d6e977592..2776942e9 100644 --- a/docs/hdl_libraries.rst +++ b/docs/hdl_libraries.rst @@ -93,7 +93,7 @@ type, other enumerations are stored as the name of their value, and a nested rec conversion is one-way, since reflection cannot create a record value. See :meth:`add_record_dict() `. It requires VHDL-2019 and a simulator with -reflection, currently NVC. +reflection, currently NVC and Riviera-PRO. OSVVM ----- diff --git a/docs/news.d/1239.feature.rst b/docs/news.d/1239.feature.rst index f10fc8a3b..00a256852 100644 --- a/docs/news.d/1239.feature.rst +++ b/docs/news.d/1239.feature.rst @@ -1,3 +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_record_dict() ` and requires a simulator with VHDL-2019 -reflection, currently NVC. +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 be71be129..b8bcf39f5 100644 --- a/tests/acceptance/test_external_run_scripts.py +++ b/tests/acceptance/test_external_run_scripts.py @@ -265,7 +265,7 @@ 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"), reason="Requires VHDL-2019 reflection") + @mark.skipif(not simulator_is("nvc", "rivierapro"), reason="Requires VHDL-2019 reflection") def test_record_dict_vhdl_2019(self): self.check(VHDL_PATH / "record_dict/run.py", vhdl_standard="2019") 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 53d370d4d..ea33231d5 100644 --- a/vunit/ui/__init__.py +++ b/vunit/ui/__init__.py @@ -1339,7 +1339,7 @@ def add_record_dict(self): """ Add record_dict_pkg, which converts any record to a dict_t with VHDL-2019 reflection. - Requires VHDL-2019 and a simulator that supports reflection (NVC). + Requires VHDL-2019 and a simulator that supports reflection (NVC or Riviera-PRO). """ self._builtins.add("record_dict") From e1ea017e7495d7be02cdd9ca6614adf551ce6a02 Mon Sep 17 00:00:00 2001 From: Sebastian Hellgren Date: Sat, 17 Oct 2026 18:26:36 +0200 Subject: [PATCH 5/6] Support Riviera-PRO in record_dict_pkg Riviera-PRO rejects chained mirror calls, returns simple_name in lower case, gives time values in units of the resolution limit and indexes descending arrays by their actual index, where NVC indexes from 'left upwards. Handle each so the package runs on both simulators. Co-Authored-By: Claude Opus 5.5 --- vunit/vhdl/record_dict/run.py | 2 + .../vhdl/record_dict/src/record_dict_pkg.vhd | 54 +++++++++++++++---- .../record_dict/test/tb_record_dict_pkg.vhd | 4 +- 3 files changed, 49 insertions(+), 11 deletions(-) diff --git a/vunit/vhdl/record_dict/run.py b/vunit/vhdl/record_dict/run.py index 6c618fc9a..898f83dc3 100644 --- a/vunit/vhdl/record_dict/run.py +++ b/vunit/vhdl/record_dict/run.py @@ -14,5 +14,7 @@ UI.add_record_dict() 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/record_dict/src/record_dict_pkg.vhd b/vunit/vhdl/record_dict/src/record_dict_pkg.vhd index e97ba229a..b38aac67c 100644 --- a/vunit/vhdl/record_dict/src/record_dict_pkg.vhd +++ b/vunit/vhdl/record_dict/src/record_dict_pkg.vhd @@ -16,6 +16,7 @@ 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"); @@ -35,14 +36,42 @@ 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 := value.get(subtype_mirror.left + index(idx)); - result(idx) := std_ulogic'val(element.to_enumeration.pos); + element := get_element(value, idx); + result(idx) := std_ulogic'val(get_pos(element)); end loop; return result; end; @@ -53,28 +82,35 @@ package body record_dict_pkg is variable result : string(1 to integer(subtype_mirror.length)); begin for idx in result'range loop - element := value.get(subtype_mirror.left + index(idx - 1)); - result(idx) := character'val(element.to_enumeration.pos); + 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 := value.get_subtype_mirror.simple_name; + 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 => - set_integer(dict, key, value.to_integer.value); + integer_value := value.to_integer; + set_integer(dict, key, integer_value.value); when class_floating => - set_real(dict, key, value.to_floating.value); + floating_value := value.to_floating; + set_real(dict, key, floating_value.value); when class_physical => if type_name = "TIME" then - set_time(dict, key, value.to_physical.value * 1 fs); + -- 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; @@ -124,7 +160,7 @@ package body record_dict_pkg is 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 " & value.get_subtype_mirror.simple_name & "."); + failure(record_dict_logger, "to_dict expects a record, got " & get_type_name(value) & "."); return dict; end if; diff --git a/vunit/vhdl/record_dict/test/tb_record_dict_pkg.vhd b/vunit/vhdl/record_dict/test/tb_record_dict_pkg.vhd index 9dfde06c4..6b381996f 100644 --- a/vunit/vhdl/record_dict/test/tb_record_dict_pkg.vhd +++ b/vunit/vhdl/record_dict/test/tb_record_dict_pkg.vhd @@ -107,7 +107,7 @@ begin -- 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 => 107, file_name => "record_dict_pkg.vhd" + line_num => 143, file_name => "record_dict_pkg.vhd" ); unmock(record_dict_logger); @@ -116,7 +116,7 @@ begin dict := to_dict(int); check_only_log( record_dict_logger, "to_dict expects a record, got INTEGER.", failure, - line_num => 127, file_name => "record_dict_pkg.vhd" + line_num => 163, file_name => "record_dict_pkg.vhd" ); unmock(record_dict_logger); end if; From 816f3ece8a04defc135cc5a6a2f60093de907338 Mon Sep 17 00:00:00 2001 From: Sebastian Hellgren Date: Tue, 20 Oct 2026 21:17:40 +0200 Subject: [PATCH 6/6] Rename record_dict to reflection Move the library to vunit/vhdl/reflection and rename add_record_dict() to add_reflection(), so the opt-in covers further packages built on VHDL-2019 reflection rather than only record_dict_pkg. The package name and its API are unchanged. Co-Authored-By: Claude Opus 5.5 --- docs/hdl_libraries.rst | 12 +++++++---- docs/news.d/1239.feature.rst | 2 +- tests/acceptance/test_external_run_scripts.py | 4 ++-- tests/unit/test_builtins.py | 20 ++++++++++--------- vunit/builtins.py | 13 ++++++------ vunit/ui/__init__.py | 7 ++++--- vunit/vhdl/{record_dict => reflection}/run.py | 2 +- .../src/record_dict_pkg.vhd | 0 .../test/tb_record_dict_pkg.vhd | 0 9 files changed, 34 insertions(+), 26 deletions(-) rename vunit/vhdl/{record_dict => reflection}/run.py (96%) rename vunit/vhdl/{record_dict => reflection}/src/record_dict_pkg.vhd (100%) rename vunit/vhdl/{record_dict => reflection}/test/tb_record_dict_pkg.vhd (100%) diff --git a/docs/hdl_libraries.rst b/docs/hdl_libraries.rst index 2776942e9..00eb78ac8 100644 --- a/docs/hdl_libraries.rst +++ b/docs/hdl_libraries.rst @@ -61,8 +61,15 @@ 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 @@ -92,9 +99,6 @@ type, other enumerations are stored as the name of their value, and a nested rec ``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. -See :meth:`add_record_dict() `. It requires VHDL-2019 and a simulator with -reflection, currently NVC and Riviera-PRO. - OSVVM ----- diff --git a/docs/news.d/1239.feature.rst b/docs/news.d/1239.feature.rst index 00a256852..7ede9be8c 100644 --- a/docs/news.d/1239.feature.rst +++ b/docs/news.d/1239.feature.rst @@ -1,3 +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_record_dict() ` and requires a simulator with VHDL-2019 +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 b8bcf39f5..b8d566a02 100644 --- a/tests/acceptance/test_external_run_scripts.py +++ b/tests/acceptance/test_external_run_scripts.py @@ -266,8 +266,8 @@ 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_record_dict_vhdl_2019(self): - self.check(VHDL_PATH / "record_dict/run.py", vhdl_standard="2019") + 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 e00709032..92bccb82a 100644 --- a/tests/unit/test_builtins.py +++ b/tests/unit/test_builtins.py @@ -83,29 +83,31 @@ 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_record_dict_requires_vhdl_2019(self): + 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("record_dict_pkg only supports VHDL-2019 and later")): - builtins.add("record_dict") + with self.assertRaisesRegex( + RuntimeError, re.escape("The reflection packages only support VHDL-2019 and later") + ): + builtins.add("reflection") - def test_record_dict_requires_reflection(self): + 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("record_dict_pkg requires VHDL-2019 reflection, which ghdl does not support") + RuntimeError, re.escape("The reflection packages require VHDL-2019 reflection, which ghdl does not support") ): - builtins.add("record_dict") + builtins.add("reflection") - def test_record_dict_adds_its_sources(self): + 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("record_dict") - self.library_mock.add_source_files.assert_called_with(VHDL_PATH / "record_dict" / "src" / "*.vhd") + 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 ( diff --git a/vunit/builtins.py b/vunit/builtins.py index 8d82c3976..42b2ae8c4 100755 --- a/vunit/builtins.py +++ b/vunit/builtins.py @@ -181,7 +181,7 @@ def add(name, deps=tuple()): add("verification_components", ["com", "osvvm"]) add("osvvm") add("random", ["osvvm"]) - add("record_dict") + add("reflection") def add(self, name, args=None): self._builtins_adder.add(name, args) @@ -754,19 +754,20 @@ def _add_random(self): self._vunit_lib.add_source_files(VHDL_PATH / "random" / "src" / "*.vhd") - def _add_record_dict(self): + def _add_reflection(self): """ - Add record_dict_pkg + Add the reflection packages """ if not self._vhdl_standard >= VHDL.STD_2019: - raise RuntimeError("record_dict_pkg only supports VHDL-2019 and later") + raise RuntimeError("The reflection packages only support VHDL-2019 and later") if not self._simulator_class.supports_vhdl_reflection(): raise RuntimeError( - f"record_dict_pkg requires VHDL-2019 reflection, which {self._simulator_class.name} does not support" + "The reflection packages require VHDL-2019 reflection, " + f"which {self._simulator_class.name} does not support" ) - self._vunit_lib.add_source_files(VHDL_PATH / "record_dict" / "src" / "*.vhd") + self._vunit_lib.add_source_files(VHDL_PATH / "reflection" / "src" / "*.vhd") def _add_com(self): """ diff --git a/vunit/ui/__init__.py b/vunit/ui/__init__.py index ea33231d5..c970121cb 100644 --- a/vunit/ui/__init__.py +++ b/vunit/ui/__init__.py @@ -1335,13 +1335,14 @@ def add_random(self): """ self._builtins.add("random") - def add_record_dict(self): + def add_reflection(self): """ - Add record_dict_pkg, which converts any record to a dict_t with VHDL-2019 reflection. + 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("record_dict") + self._builtins.add("reflection") def add_verification_components(self): """ diff --git a/vunit/vhdl/record_dict/run.py b/vunit/vhdl/reflection/run.py similarity index 96% rename from vunit/vhdl/record_dict/run.py rename to vunit/vhdl/reflection/run.py index 898f83dc3..b5ff31cac 100644 --- a/vunit/vhdl/record_dict/run.py +++ b/vunit/vhdl/reflection/run.py @@ -11,7 +11,7 @@ UI = VUnit.from_argv() UI.add_vhdl_builtins() -UI.add_record_dict() +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 diff --git a/vunit/vhdl/record_dict/src/record_dict_pkg.vhd b/vunit/vhdl/reflection/src/record_dict_pkg.vhd similarity index 100% rename from vunit/vhdl/record_dict/src/record_dict_pkg.vhd rename to vunit/vhdl/reflection/src/record_dict_pkg.vhd diff --git a/vunit/vhdl/record_dict/test/tb_record_dict_pkg.vhd b/vunit/vhdl/reflection/test/tb_record_dict_pkg.vhd similarity index 100% rename from vunit/vhdl/record_dict/test/tb_record_dict_pkg.vhd rename to vunit/vhdl/reflection/test/tb_record_dict_pkg.vhd