Skip to content

Add check_equal for any type using VHDL-2019 reflection - #1242

Open
ru551n wants to merge 3 commits into
VUnit:masterfrom
ru551n:reflection-check-equal
Open

ru551n wants to merge 3 commits into
VUnit:masterfrom
ru551n:reflection-check-equal

Conversation

@ru551n

@ru551n ru551n commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

This adds check_equal for values of any type: records, arrays, enumerations and so on. It uses VHDL-2019 reflection to name every element that differs when the check fails. It is opt-in through vu.add_reflection(). This PR now also brings the reflection plumbing from #1239, which is closed. Its to_dict was one-way, since reflection can't create values.

package packet_checks is new vunit_lib.reflection_check_generic_pkg generic map (t => packet_t);
use packet_checks.all;
...
check_equal(got_packet, expected_packet);
check_equal(my_checker, got_packet, expected_packet, "After reset");
0 fs - check - ERROR - Equality check failed (tb_packet.vhd:21)
                         addr: got 6, expected 5
                         data(0): got '1', expected '0'

Motivation

Comparing records is the most common check in a transaction-level testbench. got = expected works for any record, but on a mismatch it only says that something differs. The usual fix is a hand-written to_string per record type, which goes stale when a field is added. Reflection can walk both values and report exactly which fields differ, for any type, without per-type code.

Why opt-in

  • It needs VHDL-2019 and a simulator with std.reflection. NVC has had 'reflect since 1.11.0 (2023-12), and Riviera-PRO 2026.04 supports it. GHDL 7.0.0-dev and Questa FSE 26.1 don't provide std.reflection (checked locally).
  • So it isn't part of add_vhdl_builtins(). add_reflection() fails early, with a clear message, when the standard is older than 2019 or the simulator lacks reflection.
  • The gating follows the existing VHDL-2019 call path support: a new supports_vhdl_reflection() capability on the simulator interface, False by default and True for NVC and Riviera-PRO. Other simulators only need to flip the flag when they gain support.

API

  • reflection_check_generic_pkg: one instantiation per type gives both check_equal(got, expected, msg, level, path_offset, line_num, file_name) and check_equal(checker, got, expected, ...), with the same parameters as check_pkg. Instances for different types overload on the type.
  • reflection_check_pkg: the same two procedures as plain generic procedures, check_equal and check_equal_with_checker. The generic package is just these two instantiated.

The generic procedures are for when a package instance doesn't fit. A procedure instance can be declared in a process, a subprogram or a block, as well as in an architecture or a package. NVC only accepts a package instance in an architecture or a package. Each procedure is instantiated on its own, so a process can take only the variant it uses:

use vunit_lib.reflection_check_pkg.all;
...
main : process
  -- Default checker: check_equal(got, expected, msg, level, path_offset, line_num, file_name)
  procedure check_equal is new check_equal generic map (t => packet_t);
  -- Explicit checker: check_equal(checker, got, expected, msg, level, path_offset, line_num, file_name)
  procedure check_equal is new check_equal_with_checker generic map (t => packet_t);
begin
  check_equal(rx_packet, exp_packet);
  check_equal(my_checker, rx_packet, exp_packet, "After reset", warning);

To share the instances between testbenches, instantiate them in the package that declares the types, and use work.types_pkg.packet_checks.all in each testbench. This works on NVC and Riviera-PRO and is documented.

The checker variant needs its own name in the package, check_equal_with_checker. Both uninstantiated procedures take the generic type t, so their signatures can't tell them apart, and procedure check_equal is new check_equal would be ambiguous. The instance can still be named check_equal, so the call sites look like check_pkg's in both forms.

One line per type is the minimum the language allows. A mirror can only be passed as a variable, so packet'reflect can't be an argument, and VHDL has no implicit instantiation of generic subprograms.

Possible follow-up: a package with several type generics, such as reflection_check_3_pkg generic map (t1 => packet_t, t2 => config_t, t3 => state_t), would cover all types of a testbench in one instantiation. Generic types can't have defaults, so this needs one package per number of types. Prototyped on NVC and Riviera-PRO: it works with named association. With positional association, NVC counts the implicit "=" generic after each type as a slot and rejects the map, while Riviera-PRO accepts it.

Behaviour

  • The check uses the predefined "=" of the type. The mirrors are only built when the check fails.
  • The message lists every differing leaf on a row of its own, below the message. VUnit's log handler indents the rows and keeps the location on the first line. A custom msg replaces "Equality check failed", as in check_pkg. Paths use .element for record elements and (index) for array elements, with the declared index for both ascending and descending ranges. Arrays of different lengths report 'length instead of their elements.
  • Scalars print with the mirror's image. time is printed with time'image, scaled from the resolution limit, since Riviera-PRO gives the value in units of the resolution limit and NVC in fs.
  • Location: the path_offset chain matches check_pkg, so with VHDL-2019 call paths the log item points at the caller's check_equal line.
  • Don't instantiate it for a type that check_pkg already covers, such as integer: the calls become ambiguous. This is documented.

Other changes

  • The mirror helpers and the diff walk are in an internal reflection_util_pkg, for further reflection packages to share.
  • Dependency scanner fix (separate commit): use my_inst.all for a package instantiated in the same file produced failed to find library 'my_inst'. That affects any testbench with a local package instance, not only this feature. Use clauses that name a local package instance are now skipped. There is a new unit test.

Simulator notes

  • NVC (1.23-devel and current master): a call to an instance of a generic subprogram that leaves out a parameter with a string constant default fails at runtime with missing body for ...$default_MSG, reported as Instance of a generic subprogram fails with "missing body for ...$default_MSG" when a string constant default is used nickg/nvc#1695. So the msg default is written as the literal value of check_result_tag, "<+/->", with a comment linking the issue.
  • Riviera-PRO 2026.04, library-level instance: an instance that is a design unit of its own (package packet_checks is new vunit_lib.reflection_check_generic_pkg ... at library level) doesn't work. check_equal through it silently does nothing: nothing is logged and the check isn't counted. In a minimal reproduction without VUnit, with the generic package in another library, the simulator crashes at startup ("The simulator process terminated unexpectedly"). With both in the same library it works. NVC handles both. The docs say to instantiate it inside a package or an architecture instead. I'll report it to Aldec.
  • Riviera-PRO needs -dbg (rivierapro.vcom_flags) to record the caller's location in VHDL-2019 call paths. The reflection run.py sets it.
  • NVC, arrays of records: reflecting an element of an array whose element type is a record crashes NVC 1.23-devel and current master with an internal error, unhandled type HEADER_T in type_bit_width in get_value_mirror (src/type.c:1025). Riviera-PRO handles it. Since the mirrors are only built when the check fails, a passing check is unaffected. The test is skipped on NVC through conditional analysis, and the docs mention it.
  • Physical types: NVC gives the unit name in upper case (2000 MM), Riviera-PRO in lower case. The message lower-cases it, as for enumeration values.
  • A custom "=" is not supported. I tried a function "=" ... is <> generic. NVC rejects passing it on from the generic package ("=" => "="), and without the association Riviera-PRO uses the predefined "=" instead. It can be added once the simulators agree.

Testing

  • vunit/vhdl/reflection/run.py with VHDL-2019: 11/11 on NVC 1.23-devel and on Riviera-PRO 2026.04. They cover an array of records (skipped on NVC, see below), user integer and physical types, and: element paths, all-differences messages, scalar types, arrays (ascending, descending, other element types, length mismatch), a scalar t, a custom checker with message and level, procedures instantiated in a process.
  • GHDL 7.0.0-dev and Questa FSE 26.1 still don't provide std.reflection (checked with a direct compile). add_reflection() refuses them with its clear error, and test_reflection_vhdl_2019 is skipped for them.
  • tests/unit: 643 passed. tests/acceptance/test_artificial.py and test_dependencies.py pass on GHDL and NVC, as a check of the scanner change.
  • pycodestyle, the license check and pylint (10/10) pass on the changed modules.

This PR was written with the help of an AI assistant (Claude).

🤖 Generated with Claude Code

ru551n and others added 2 commits October 20, 2026 22:54
A package instantiated in an architecture is made visible with a use
clause naming the instance, such as use my_inst.all. The dependency
scanner read my_inst as a library and warned "failed to find library
'my_inst'". Skip use clauses that name a package instantiated in the same
file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
reflection_check_pkg has generic check_equal and check_equal_with_checker
procedures with the parameters of check_pkg's check_equal. They check
with the predefined "=" of the type, and on a mismatch walk the
reflection mirrors of both values and list every element that differs,
one per row:

  Equality check failed (tb_packet.vhd:21)
    hdr.addr: got 6, expected 5
    data(0): got '1', expected '0'

reflection_check_generic_pkg instantiates both for one type, so one
package instantiation gives the same call shape as check_pkg:

  package packet_checks is new vunit_lib.reflection_check_generic_pkg
    generic map (t => packet_t);

The packages are opt-in through vu.add_reflection(), which fails early
when the standard is older than VHDL-2019 or the simulator lacks
reflection. A new supports_vhdl_reflection() simulator capability is True
for NVC and Riviera-PRO. Riviera-PRO needs -dbg for the call path
locations the tests check.

The default of msg is written as the value of check_result_tag, since
NVC can't call an instance of a generic subprogram that leaves out a
parameter with a string constant default (nickg/nvc#1695).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Physical values print with the unit in lower case on all simulators, since NVC gives it in upper case. An array of records crashes NVC when it reflects an element, so that test is skipped on NVC and the limitation is documented.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant