Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions docs/hdl_libraries.rst
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,44 @@ and OSVVM.

See :meth:`add_random() <vunit.ui.VUnit.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() <vunit.ui.VUnit.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 <data_types_library>` 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
-----

Expand Down
3 changes: 3 additions & 0 deletions docs/news.d/1239.feature.rst
Original file line number Diff line number Diff line change
@@ -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() <vunit.ui.VUnit.add_reflection>` and requires a simulator with VHDL-2019
reflection, currently NVC and Riviera-PRO.
4 changes: 4 additions & 0 deletions tests/acceptance/test_external_run_scripts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
28 changes: 27 additions & 1 deletion tests/unit/test_builtins.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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),
Expand Down
16 changes: 16 additions & 0 deletions vunit/builtins.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions vunit/sim_if/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down
7 changes: 7 additions & 0 deletions vunit/sim_if/nvc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down
7 changes: 7 additions & 0 deletions vunit/sim_if/rivierapro.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down
9 changes: 9 additions & 0 deletions vunit/ui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 20 additions & 0 deletions vunit/vhdl/reflection/run.py
Original file line number Diff line number Diff line change
@@ -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()
181 changes: 181 additions & 0 deletions vunit/vhdl/reflection/src/record_dict_pkg.vhd
Original file line number Diff line number Diff line change
@@ -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;
Loading
Loading